🔀 Merge dev to auth branch

This commit is contained in:
Manuel
2023-09-10 13:38:53 +02:00
617 changed files with 8473 additions and 1499 deletions

View File

@@ -43,7 +43,7 @@ export const appRouter = createTRPCRouter({
}
if (error.code === 'ECONNABORTED') {
Consola.error(`Ping timed out for app with id : ${input} (url: ${app.url})`);
Consola.error(`Ping timed out for app with id '${input.id}' in config '${input.configName}' -> url: ${app.url})`);
throw new TRPCError({
code: 'TIMEOUT',
cause: input,

View File

@@ -16,6 +16,7 @@ export const calendarRouter = createTRPCRouter({
year: z.number().min(1900).max(2300),
options: z.object({
useSonarrv4: z.boolean().optional().default(false),
showUnmonitored: z.boolean().optional().default(false),
}),
})
)
@@ -64,7 +65,9 @@ export const calendarRouter = createTRPCRouter({
if (!apiKey) return { type: integration.type, items: [], success: false };
return axios
.get(
`${origin}${endpoint}?apiKey=${apiKey}&end=${end.toISOString()}&start=${start.toISOString()}&includeSeries=true&includeEpisodeFile=true&includeEpisodeImages=true`
`${origin}${endpoint}?apiKey=${apiKey}&end=${end.toISOString()}&start=${start.toISOString()}&includeSeries=true&includeEpisodeFile=true&includeEpisodeImages=true&&unmonitored=${
input.options.showUnmonitored
}`
)
.then((x) => ({ type: integration.type, items: x.data as any[], success: true }))
.catch((err) => {

View File

@@ -67,6 +67,12 @@ export const configRouter = createTRPCRouter({
})
)
.mutation(async ({ input }) => {
if (process.env.DISABLE_EDIT_MODE?.toLowerCase() === 'true') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: 'Edit is not allowed, because edit mode is disabled'
});
}
Consola.info(`Saving updated configuration of '${input.name}' config.`);
const previousConfig = getConfig(input.name);

View File

@@ -1,3 +1,4 @@
import Consola from 'consola';
import { z } from 'zod';
import { findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
@@ -14,19 +15,25 @@ export const dnsHoleRouter = createTRPCRouter({
z.object({
action: z.enum(['enable', 'disable']),
configName: z.string(),
appsToChange: z.optional(z.array(z.string())),
})
)
.mutation(async ({ input }) => {
const config = getConfig(input.configName);
const applicableApps = config.apps.filter(
(x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type)
(app) =>
app.id &&
app.integration?.type &&
input.appsToChange?.includes(app.id) &&
['pihole', 'adGuardHome'].includes(app.integration?.type)
);
await Promise.all(
applicableApps.map(async (app) => {
if (app.integration?.type === 'pihole') {
await processPiHole(app, input.action === 'enable');
return;
}
@@ -72,8 +79,6 @@ export const dnsHoleRouter = createTRPCRouter({
}
);
//const data: AdStatistics = ;
data.adsBlockedTodayPercentage = data.adsBlockedToday / data.dnsQueriesToday;
if (Number.isNaN(data.adsBlockedTodayPercentage)) {
data.adsBlockedTodayPercentage = 0;
@@ -90,22 +95,38 @@ const processAdGuard = async (app: ConfigAppType, enable: boolean) => {
);
if (enable) {
await adGuard.disable();
try {
await adGuard.enable();
} catch (error) {
Consola.error((error as Error).message);
}
return;
}
await adGuard.enable();
try {
await adGuard.disable();
} catch (error) {
Consola.error((error as Error).message);
}
};
const processPiHole = async (app: ConfigAppType, enable: boolean) => {
const pihole = new PiHoleClient(app.url, findAppProperty(app, 'apiKey'));
if (enable) {
await pihole.enable();
try {
await pihole.enable();
} catch (error) {
Consola.error((error as Error).message);
}
return;
}
await pihole.disable();
try {
await pihole.disable();
} catch (error) {
Consola.error((error as Error).message);
}
};
const collectPiHoleSummary = async (app: ConfigAppType) => {

View File

@@ -3,15 +3,18 @@ import { z } from 'zod';
import { checkIntegrationsType } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { MediaRequestListWidget } from '~/widgets/media-requests/MediaRequestListTile';
import { MediaRequest } from '~/widgets/media-requests/media-request-types';
import { MediaRequest, Users } from '~/widgets/media-requests/media-request-types';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { MediaRequestStatsWidget } from '~/widgets/media-requests/MediaRequestStatsTile';
import { removeTrailingSlash } from 'next/dist/shared/lib/router/utils/remove-trailing-slash';
export const mediaRequestsRouter = createTRPCRouter({
all: publicProcedure
allMedia: publicProcedure
.input(
z.object({
configName: z.string(),
widget: z.custom<MediaRequestListWidget>().or(z.custom<MediaRequestStatsWidget>()),
})
)
.query(async ({ input }) => {
@@ -21,8 +24,6 @@ export const mediaRequestsRouter = createTRPCRouter({
checkIntegrationsType(app.integration, ['overseerr', 'jellyseerr'])
);
Consola.log(`Retrieving media requests from ${apps.length} apps`);
const promises = apps.map((app): Promise<MediaRequest[]> => {
const apiKey =
app.integration?.properties.find((prop) => prop.field === 'apiKey')?.value ?? '';
@@ -32,17 +33,12 @@ export const mediaRequestsRouter = createTRPCRouter({
})
.then(async (response) => {
const body = (await response.json()) as OverseerrResponse;
const mediaWidget = config.widgets.find((x) => x.type === 'media-requests-list') as
| MediaRequestListWidget
| undefined;
if (!mediaWidget) {
Consola.log('No media-requests-list found');
return Promise.resolve([]);
}
const appUrl = mediaWidget.properties.replaceLinksWithExternalHost
let appUrl = input.widget.properties.replaceLinksWithExternalHost && app.behaviour.externalUrl?.length > 0
? app.behaviour.externalUrl
: app.url;
appUrl = removeTrailingSlash(appUrl);
const requests = await Promise.all(
body.results.map(async (item): Promise<MediaRequest> => {
const genericItem = await retrieveDetailsForItem(
@@ -59,8 +55,9 @@ export const mediaRequestsRouter = createTRPCRouter({
type: item.type,
name: genericItem.name,
userName: item.requestedBy.displayName,
userProfilePicture: constructAvatarUrl(appUrl, item),
userProfilePicture: constructAvatarUrl(appUrl, item.requestedBy.avatar),
userLink: `${appUrl}/users/${item.requestedBy.id}`,
userRequestCount: item.requestedBy.requestCount,
airDate: genericItem.airDate,
status: item.status,
backdropPath: `https://image.tmdb.org/t/p/original/${genericItem.backdropPath}`,
@@ -85,17 +82,66 @@ export const mediaRequestsRouter = createTRPCRouter({
return mediaRequests;
}),
users: publicProcedure
.input(
z.object({
configName: z.string(),
widget: z.custom<MediaRequestListWidget>().or(z.custom<MediaRequestStatsWidget>()),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const apps = config.apps.filter((app) =>
checkIntegrationsType(app.integration, ['overseerr', 'jellyseerr'])
);
const promises = apps.map((app): Promise<Users[]> => {
const apiKey =
app.integration?.properties.find((prop) => prop.field === 'apiKey')?.value ?? '';
const headers: HeadersInit = { 'X-Api-Key': apiKey };
return fetch(`${app.url}/api/v1/user?take=25&skip=0&sort=requests`, {
headers,
})
.then(async (response) => {
const body = (await response.json()) as OverseerrUsers;
const appUrl = input.widget.properties.replaceLinksWithExternalHost
? app.behaviour.externalUrl
: app.url;
const users = await Promise.all(
body.results.map(async (user): Promise<Users> => {
return {
app: app.integration?.type ?? 'overseerr',
id: user.id,
userName: user.displayName,
userProfilePicture: constructAvatarUrl(appUrl, user.avatar),
userLink: `${appUrl}/users/${user.id}`,
userRequestCount: user.requestCount,
};
})
);
return Promise.resolve(users);
})
.catch((err) => {
Consola.error(`Failed to request users from Overseerr: ${err}`);
return Promise.resolve([]);
});
});
const users = (await Promise.all(promises)).reduce((prev, cur) => prev.concat(cur), []);
return users;
}),
});
const constructAvatarUrl = (appUrl: string, item: OverseerrResponseItem) => {
const isAbsolute =
item.requestedBy.avatar.startsWith('http://') || item.requestedBy.avatar.startsWith('https://');
const constructAvatarUrl = (appUrl: string, avatar: string) => {
const isAbsolute = avatar.startsWith('http://') || avatar.startsWith('https://');
if (isAbsolute) {
return item.requestedBy.avatar;
return avatar;
}
return `${appUrl}/${item.requestedBy.avatar}`;
return `${appUrl}/${avatar}`;
};
const retrieveDetailsForItem = async (
@@ -117,7 +163,7 @@ const retrieveDetailsForItem = async (
backdropPath: series.backdropPath,
posterPath: series.backdropPath,
};
}
};
const movieResponse = await fetch(`${baseUrl}/api/v1/movie/${id}`, {
headers,
@@ -158,6 +204,10 @@ type OverseerrResponse = {
results: OverseerrResponseItem[];
};
type OverseerrUsers = {
results: OverseerrResponseItemUser[];
};
type OverseerrResponseItem = {
id: number;
status: number;
@@ -176,4 +226,5 @@ type OverseerrResponseItemUser = {
id: number;
displayName: string;
avatar: string;
requestCount: number;
};

View File

@@ -100,72 +100,74 @@ const handleServer = async (app: ConfigAppType): Promise<GenericMediaServer | un
const infoApi = await getSystemApi(api).getPublicSystemInfo();
await api.authenticateUserByName(username, password);
const sessionApi = await getSessionApi(api);
const sessions = await sessionApi.getSessions();
const { data: sessions } = await sessionApi.getSessions();
return {
type: 'jellyfin',
appId: app.id,
serverAddress: app.url,
version: infoApi.data.Version ?? undefined,
sessions: sessions.data.map(
(session): GenericSessionInfo => ({
id: session.Id ?? '?',
username: session.UserName ?? undefined,
sessionName: `${session.Client} (${session.DeviceName})`,
supportsMediaControl: session.SupportsMediaControl ?? false,
currentlyPlaying: session.NowPlayingItem
? {
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
seasonName: session.NowPlayingItem.SeasonName as string,
episodeName: session.NowPlayingItem.Name as string,
albumName: session.NowPlayingItem.Album as string,
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
metadata: {
video:
session.NowPlayingItem &&
session.NowPlayingItem.Width &&
session.NowPlayingItem.Height
sessions: sessions
.filter((session) => session.NowPlayingItem)
.map(
(session): GenericSessionInfo => ({
id: session.Id ?? '?',
username: session.UserName ?? undefined,
sessionName: `${session.Client} (${session.DeviceName})`,
supportsMediaControl: session.SupportsMediaControl ?? false,
currentlyPlaying: session.NowPlayingItem
? {
name: `${session.NowPlayingItem.SeriesName ?? session.NowPlayingItem.Name}`,
seasonName: session.NowPlayingItem.SeasonName as string,
episodeName: session.NowPlayingItem.Name as string,
albumName: session.NowPlayingItem.Album as string,
episodeCount: session.NowPlayingItem.EpisodeCount ?? undefined,
metadata: {
video:
session.NowPlayingItem &&
session.NowPlayingItem.Width &&
session.NowPlayingItem.Height
? {
videoCodec: undefined,
width: session.NowPlayingItem.Width ?? undefined,
height: session.NowPlayingItem.Height ?? undefined,
bitrate: undefined,
videoFrameRate: session.TranscodingInfo?.Framerate
? String(session.TranscodingInfo?.Framerate)
: undefined,
}
: undefined,
audio: session.TranscodingInfo
? {
videoCodec: undefined,
width: session.NowPlayingItem.Width ?? undefined,
height: session.NowPlayingItem.Height ?? undefined,
bitrate: undefined,
videoFrameRate: session.TranscodingInfo?.Framerate
? String(session.TranscodingInfo?.Framerate)
: undefined,
audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
}
: undefined,
audio: session.TranscodingInfo
? {
audioChannels: session.TranscodingInfo.AudioChannels ?? undefined,
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
}
: undefined,
transcoding: session.TranscodingInfo
? {
audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
container: session.TranscodingInfo.Container ?? undefined,
width: session.TranscodingInfo.Width ?? undefined,
height: session.TranscodingInfo.Height ?? undefined,
videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
audioDecision: undefined,
context: undefined,
duration: undefined,
error: undefined,
sourceAudioCodec: undefined,
sourceVideoCodec: undefined,
timeStamp: undefined,
transcodeHwRequested: undefined,
videoDecision: undefined,
}
: undefined,
},
type: convertJellyfinType(session.NowPlayingItem.Type),
}
: undefined,
userProfilePicture: undefined,
})
),
transcoding: session.TranscodingInfo
? {
audioChannels: session.TranscodingInfo.AudioChannels ?? -1,
audioCodec: session.TranscodingInfo.AudioCodec ?? undefined,
container: session.TranscodingInfo.Container ?? undefined,
width: session.TranscodingInfo.Width ?? undefined,
height: session.TranscodingInfo.Height ?? undefined,
videoCodec: session.TranscodingInfo?.VideoCodec ?? undefined,
audioDecision: undefined,
context: undefined,
duration: undefined,
error: undefined,
sourceAudioCodec: undefined,
sourceVideoCodec: undefined,
timeStamp: undefined,
transcodeHwRequested: undefined,
videoDecision: undefined,
}
: undefined,
},
type: convertJellyfinType(session.NowPlayingItem.Type),
}
: undefined,
userProfilePicture: undefined,
})
),
success: true,
};
}

View File

@@ -13,7 +13,7 @@ export const notebookRouter = createTRPCRouter({
.input(z.object({ widgetId: z.string(), content: z.string(), configName: z.string() }))
.mutation(async ({ input }) => {
//TODO: #1305 Remove use of DISABLE_EDIT_MODE for auth update
if (!process.env.DISABLE_EDIT_MODE) {
if (process.env.DISABLE_EDIT_MODE?.toLowerCase() === 'true') {
throw new TRPCError({
code: 'METHOD_NOT_SUPPORTED',
message: 'Edit is not allowed, because edit mode is disabled'

View File

@@ -62,13 +62,17 @@ export const rssRouter = createTRPCRouter({
| IRssWidget
| undefined;
if (!rssWidget || input.feedUrls.length === 0) {
if (!rssWidget) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'required widget does not exist',
});
}
if (input.feedUrls.length === 0) {
return []
}
const result = await Promise.all(
input.feedUrls.map(async (feedUrl) =>
getFeedUrl(feedUrl, rssWidget.properties.dangerousAllowSanitizedItemContent)

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import { find } from 'geo-tz'
const GeoTz = require('browser-geo-tz/dist/geotz.js');
import { createTRPCRouter, publicProcedure } from '../trpc';
@@ -12,6 +12,7 @@ export const timezoneRouter = createTRPCRouter({
})
)
.query(async ({ input }) => {
return find(input.latitude,input.longitude)[0];
const timezone = GeoTz.find(input.latitude,input.longitude);
return Array.isArray(timezone) ? timezone[0] : timezone;
}),
})