fix(api): stop library reads from resetting enabled flags (#3321)

This commit is contained in:
fallenbagel
2026-08-16 18:31:43 +05:00
committed by GitHub
parent 39ff48c650
commit 985ddef3f0
5 changed files with 255 additions and 159 deletions
+101 -27
View File
@@ -2321,21 +2321,6 @@ paths:
description: Returns a list of Jellyfin libraries in a JSON array.
tags:
- settings
parameters:
- in: query
name: sync
description: Syncs the current libraries with the current Jellyfin server
schema:
type: string
nullable: true
- in: query
name: enable
explode: false
allowReserved: true
description: Comma separated list of libraries to enable. Any libraries not passed will be disabled!
schema:
type: string
nullable: true
responses:
'200':
description: 'Jellyfin libraries returned'
@@ -2345,6 +2330,60 @@ paths:
type: array
items:
$ref: '#/components/schemas/JellyfinLibrary'
/settings/jellyfin/library/{libraryId}:
put:
summary: Update a single Jellyfin library
description: Updates a single Jellyfin library with the provided values.
tags:
- settings
parameters:
- in: path
name: libraryId
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- enabled
properties:
enabled:
type: boolean
example: true
responses:
'200':
description: 'Jellyfin library returned'
content:
application/json:
schema:
$ref: '#/components/schemas/JellyfinLibrary'
'400':
description: 'Invalid request body'
'404':
description: 'Library does not exist'
/settings/jellyfin/library/sync:
post:
summary: Sync Jellyfin libraries
description: Syncs the current libraries with the current Jellyfin server. Enabled flags are preserved.
tags:
- settings
responses:
'200':
description: 'Jellyfin libraries returned'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/JellyfinLibrary'
'404':
description: 'No libraries found'
'501':
description: 'Automatic library grouping is not supported'
/settings/jellyfin/users:
get:
summary: Get Jellyfin Users
@@ -2474,23 +2513,58 @@ paths:
get:
summary: Get Plex libraries
description: Returns a list of Plex libraries in a JSON array.
tags:
- settings
responses:
'200':
description: 'Plex libraries returned'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PlexLibrary'
/settings/plex/library/{libraryId}:
put:
summary: Update a single Plex library
description: Updates a single Plex library with the provided values.
tags:
- settings
parameters:
- in: query
name: sync
description: Syncs the current libraries with the current Plex server
- in: path
name: libraryId
required: true
schema:
type: string
nullable: true
- in: query
name: enable
explode: false
allowReserved: true
description: Comma separated list of libraries to enable. Any libraries not passed will be disabled!
schema:
type: string
nullable: true
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- enabled
properties:
enabled:
type: boolean
example: true
responses:
'200':
description: 'Plex library returned'
content:
application/json:
schema:
$ref: '#/components/schemas/PlexLibrary'
'400':
description: 'Invalid request body'
'404':
description: 'Library does not exist'
/settings/plex/library/sync:
post:
summary: Sync Plex libraries
description: Syncs the current libraries with the current Plex server. Enabled flags are preserved.
tags:
- settings
responses:
'200':
description: 'Plex libraries returned'
+112 -68
View File
@@ -39,6 +39,7 @@ import { rescheduleJob } from 'node-schedule';
import path from 'path';
import semver from 'semver';
import { URL } from 'url';
import { z } from 'zod';
import metadataRoutes from './metadata';
import notificationRoutes from './notifications';
import radarrRoutes from './radarr';
@@ -52,6 +53,10 @@ settingsRoutes.use('/sonarr', sonarrRoutes);
settingsRoutes.use('/discover', discoverSettingRoutes);
settingsRoutes.use('/metadatas', metadataRoutes);
const libraryUpdateSchema = z.object({
enabled: z.boolean(),
});
const filteredMainSettings = (
user: User,
main: MainSettings
@@ -229,28 +234,47 @@ settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
}
});
settingsRoutes.get('/plex/library', async (req, res) => {
settingsRoutes.get('/plex/library', (_req, res) => {
const settings = getSettings();
if (req.query.sync) {
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: { id: true, plexToken: true },
where: { id: 1 },
});
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
return res.status(200).json(settings.plex.libraries);
});
await plexapi.syncLibraries();
settingsRoutes.put('/plex/library/:libraryId', async (req, res, next) => {
const settings = getSettings();
const bodyResult = libraryUpdateSchema.safeParse(req.body);
if (!bodyResult.success) {
return next({ status: 400, message: 'Invalid request body.' });
}
const enabledLibraries = req.query.enable
? (req.query.enable as string).split(',')
: [];
settings.plex.libraries = settings.plex.libraries.map((library) => ({
...library,
enabled: enabledLibraries.includes(library.id),
}));
const library = settings.plex.libraries.find(
(l) => l.id === req.params.libraryId
);
if (!library) {
return next({ status: 404, message: 'Library does not exist.' });
}
library.enabled = bodyResult.data.enabled;
await settings.save();
return res.status(200).json(library);
});
settingsRoutes.post('/plex/library/sync', async (_req, res) => {
const settings = getSettings();
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: { id: true, plexToken: true },
where: { id: 1 },
});
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
await plexapi.syncLibraries();
return res.status(200).json(settings.plex.libraries);
});
@@ -330,66 +354,86 @@ settingsRoutes.post('/jellyfin', async (req, res, next) => {
return res.status(200).json(settings.jellyfin);
});
settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
settingsRoutes.get('/jellyfin/library', (_req, res) => {
const settings = getSettings();
if (req.query.sync) {
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'],
where: { id: 1 },
order: { id: 'ASC' },
});
const jellyfinClient = new JellyfinAPI(
getHostname(),
settings.jellyfin.apiKey,
admin.jellyfinDeviceId ?? ''
);
return res.status(200).json(settings.jellyfin.libraries);
});
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
settingsRoutes.put('/jellyfin/library/:libraryId', async (req, res, next) => {
const settings = getSettings();
const libraries = await jellyfinClient.getLibraries();
const bodyResult = libraryUpdateSchema.safeParse(req.body);
if (libraries.length === 0) {
// Check if no libraries are found due to the fallback to user views
// This only affects LDAP users
const account = await jellyfinClient.getUser();
// Automatic Library grouping is not supported when user views are used to get library
if (account.Configuration.GroupedFolders?.length > 0) {
return next({
status: 501,
message: ApiErrorCode.SyncErrorGroupedFolders,
});
}
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
}
const newLibraries: Library[] = libraries.map((library) => {
const existing = settings.jellyfin.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
};
});
settings.jellyfin.libraries = newLibraries;
if (!bodyResult.success) {
return next({ status: 400, message: 'Invalid request body.' });
}
const enabledLibraries = req.query.enable
? (req.query.enable as string).split(',')
: [];
settings.jellyfin.libraries = settings.jellyfin.libraries.map((library) => ({
...library,
enabled: enabledLibraries.includes(library.id),
}));
const library = settings.jellyfin.libraries.find(
(l) => l.id === req.params.libraryId
);
if (!library) {
return next({ status: 404, message: 'Library does not exist.' });
}
library.enabled = bodyResult.data.enabled;
await settings.save();
return res.status(200).json(library);
});
settingsRoutes.post('/jellyfin/library/sync', async (_req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: ['id', 'jellyfinDeviceId', 'jellyfinUserId'],
where: { id: 1 },
order: { id: 'ASC' },
});
const jellyfinClient = new JellyfinAPI(
getHostname(),
settings.jellyfin.apiKey,
admin.jellyfinDeviceId ?? ''
);
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
const libraries = await jellyfinClient.getLibraries();
if (libraries.length === 0) {
// Check if no libraries are found due to the fallback to user views
// This only affects LDAP users
const account = await jellyfinClient.getUser();
// Automatic Library grouping is not supported when user views are used to get library
if (account.Configuration.GroupedFolders?.length > 0) {
return next({
status: 501,
message: ApiErrorCode.SyncErrorGroupedFolders,
});
}
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
}
const newLibraries: Library[] = libraries.map((library) => {
const existing = settings.jellyfin.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
type: library.type,
};
});
settings.jellyfin.libraries = newLibraries;
await settings.save();
return res.status(200).json(settings.jellyfin.libraries);
});
+15 -31
View File
@@ -47,6 +47,7 @@ const messages = defineMessages('components.Settings', {
'Custom authentication with Automatic Library Grouping not supported',
jellyfinSyncFailedGenericError:
'Something went wrong while syncing libraries',
toggleLibraryFailure: 'Failed to update library.',
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
syncing: 'Syncing',
syncJellyfin: 'Sync Libraries',
@@ -161,18 +162,8 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
const syncLibraries = async () => {
setIsSyncing(true);
const params: { sync: boolean; enable?: string } = {
sync: true,
};
if (activeLibraries.length > 0) {
params.enable = activeLibraries.join(',');
}
try {
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
await axios.post('/api/v1/settings/jellyfin/library/sync');
setIsSyncing(false);
revalidate();
} catch (e) {
@@ -221,30 +212,23 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
const toggleLibrary = async (libraryId: string) => {
setIsSyncing(true);
if (activeLibraries.includes(libraryId)) {
const params: { enable?: string } = {};
try {
await axios.put(`/api/v1/settings/jellyfin/library/${libraryId}`, {
enabled: !activeLibraries.includes(libraryId),
});
if (activeLibraries.length > 1) {
params.enable = activeLibraries
.filter((id) => id !== libraryId)
.join(',');
if (onComplete) {
onComplete();
}
await axios.get('/api/v1/settings/jellyfin/library', {
params,
});
} else {
await axios.get('/api/v1/settings/jellyfin/library', {
params: {
enable: [...activeLibraries, libraryId].join(','),
},
} catch {
addToast(intl.formatMessage(messages.toggleLibraryFailure), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setIsSyncing(false);
revalidate();
}
if (onComplete) {
onComplete();
}
setIsSyncing(false);
revalidate();
};
if (!data && !error) {
+25 -33
View File
@@ -41,6 +41,8 @@ const messages = defineMessages('components.Settings', {
toastPlexRefresh: 'Retrieving server list from Plex…',
toastPlexRefreshSuccess: 'Plex server list retrieved successfully!',
toastPlexRefreshFailure: 'Failed to retrieve Plex server list.',
toastPlexSyncFailure: 'Failed to sync Plex libraries.',
toggleLibraryFailure: 'Failed to update library.',
toastPlexConnecting: 'Attempting to connect to Plex…',
toastPlexConnectingSuccess: 'Plex connection established successfully!',
toastPlexConnectingFailure: 'Failed to connect to Plex.',
@@ -240,19 +242,17 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
const syncLibraries = async () => {
setIsSyncing(true);
const params: { sync: boolean; enable?: string } = {
sync: true,
};
if (activeLibraries.length > 0) {
params.enable = activeLibraries.join(',');
try {
await axios.post('/api/v1/settings/plex/library/sync');
} catch {
addToast(intl.formatMessage(messages.toastPlexSyncFailure), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setIsSyncing(false);
revalidate();
}
await axios.get('/api/v1/settings/plex/library', {
params,
});
setIsSyncing(false);
revalidate();
};
const refreshPresetServers = async () => {
@@ -311,31 +311,23 @@ const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
const toggleLibrary = async (libraryId: string) => {
setIsSyncing(true);
if (activeLibraries.includes(libraryId)) {
const params: { enable?: string } = {};
try {
await axios.put(`/api/v1/settings/plex/library/${libraryId}`, {
enabled: !activeLibraries.includes(libraryId),
});
if (activeLibraries.length > 1) {
params.enable = activeLibraries
.filter((id) => id !== libraryId)
.join(',');
if (onComplete) {
onComplete();
}
await axios.get('/api/v1/settings/plex/library', {
params,
});
} else {
await axios.get('/api/v1/settings/plex/library', {
params: {
enable: [...activeLibraries, libraryId].join(','),
},
} catch {
addToast(intl.formatMessage(messages.toggleLibraryFailure), {
autoDismiss: true,
appearance: 'error',
});
} finally {
setIsSyncing(false);
revalidate();
}
if (onComplete) {
onComplete();
}
setIsSyncing(false);
revalidate();
};
if ((!data || !dataTautulli) && !error) {
+2
View File
@@ -1302,8 +1302,10 @@
"components.Settings.toastPlexRefresh": "Retrieving server list from Plex…",
"components.Settings.toastPlexRefreshFailure": "Failed to retrieve Plex server list.",
"components.Settings.toastPlexRefreshSuccess": "Plex server list retrieved successfully!",
"components.Settings.toastPlexSyncFailure": "Failed to sync Plex libraries.",
"components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.",
"components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!",
"components.Settings.toggleLibraryFailure": "Failed to update library.",
"components.Settings.tvdbProviderDoesnotWork": "TVDB provider does not work, please select another metadata provider",
"components.Settings.urlBase": "URL Base",
"components.Settings.validationApiKey": "You must provide an API key",