mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
🔀 Merge dev to auth branch
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user