From fa3e99a93166da9cbce3913b8daf6170682057f8 Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:12:11 +0500 Subject: [PATCH] fix(jellyfinapi): refactors jellyfin library sync to support automatic grouping and collections Previously, #450 added support for automatic library grouping. However, some users reported that they were getting a 401 when using custom authentication such as LDAP. Therefore, that PR was reverted (#524). This PR adds back the support for automatic library grouping for jellyfin authentication users using the endpoint `/Library/MediaFolders` and fallsback to User views endpoint if they're unable to sync the libraries (some, not all LDAP users had issues. Some reported that it worked despite having custom authentication). Once it falls back to user views endpoint for syncing, now it will detect if automatic grouping is enabled giving a warning that its not supported when using some custom authentication methods. This PR also fixed collection syncing by expanding the boxsets when syncing. fix #256, fix #489, re #450, #524, fix #515, fix #474, fix #473 --- server/api/jellyfin.ts | 77 +++++++++++++------- server/routes/settings/index.ts | 17 ++++- src/components/Settings/SettingsJellyfin.tsx | 42 +++++++++-- 3 files changed, 103 insertions(+), 33 deletions(-) diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 768398a06..8fb829e17 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -9,6 +9,9 @@ export interface JellyfinUserResponse { ServerId: string; ServerName: string; Id: string; + Configuration: { + GroupedFolders: string[]; + }; Policy: { IsAdministrator: boolean; }; @@ -24,6 +27,13 @@ export interface JellyfinUserListResponse { users: JellyfinUserResponse[]; } +interface JellyfinMediaFolder { + Name: string; + Id: string; + Type: string; + CollectionType: string; +} + export interface JellyfinLibrary { type: 'show' | 'movie'; key: string; @@ -175,24 +185,46 @@ class JellyfinAPI { public async getLibraries(): Promise { try { - // TODO: Try to fix automatic grouping without fucking up LDAP users - // const libraries = await this.axios.get('/Library/VirtualFolders'); - - const account = await this.axios.get( - `/Users/${this.userId ?? 'Me'}/Views` - ); + const mediaFolders = await this.axios.get(`/Library/MediaFolders`); + console.log(mediaFolders.data.Items); + + return this.mapLibraries(mediaFolders.data.Items); + } catch (mediaFoldersError) { + // fallback to user views to get libraries + // this only affects LDAP users + try { + const mediaFolders = await this.axios.get( + `/Users/${this.userId}/Views` + ); + + return this.mapLibraries(mediaFolders.data.Items); + } catch (e) { + logger.error( + `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`, + { label: 'Jellyfin API' } + ); + return []; + } + } + } - const response: JellyfinLibrary[] = account.data.Items.filter( - (Item: any) => { - return ( - Item.Type === 'CollectionFolder' && - Item.CollectionType !== 'music' && - Item.CollectionType !== 'books' && - Item.CollectionType !== 'musicvideos' && - Item.CollectionType !== 'homevideos' - ); - } - ).map((Item: any) => { + private mapLibraries(mediaFolders: JellyfinMediaFolder[]): JellyfinLibrary[] { + const excludedTypes = [ + 'music', + 'books', + 'musicvideos', + 'homevideos', + 'boxsets', + ]; + + return mediaFolders + .filter((Item: JellyfinMediaFolder) => { + return ( + Item.Type === 'CollectionFolder' && + !excludedTypes.includes(Item.CollectionType) + ); + }) + .map((Item: JellyfinMediaFolder) => { return { key: Item.Id, title: Item.Name, @@ -200,21 +232,12 @@ class JellyfinAPI { agent: 'jellyfin', }; }); - - return response; - } catch (e) { - logger.error( - `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`, - { label: 'Jellyfin API' } - ); - return []; - } } public async getLibraryContents(id: string): Promise { try { const contents = await this.axios.get( - `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}` + `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false` ); return contents.data.Items.filter( diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index de86ed71b..9a66409b9 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -261,7 +261,7 @@ settingsRoutes.post('/jellyfin', (req, res) => { return res.status(200).json(settings.jellyfin); }); -settingsRoutes.get('/jellyfin/library', async (req, res) => { +settingsRoutes.get('/jellyfin/library', async (req, res, next) => { const settings = getSettings(); if (req.query.sync) { @@ -281,6 +281,21 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => { 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(); + + console.log(account.Configuration.GroupedFolders.length); + + // 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: 'SYNC_ERROR_GROUPED_FOLDERS' }); + } + + return next({ status: 404, message: 'SYNC_ERROR_NO_LIBRARIES' }); + } + const newLibraries: Library[] = libraries.map((library) => { const existing = settings.jellyfin.libraries.find( (l) => l.id === library.key && l.name === library.title diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index 584a98fde..79265e5a9 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -45,6 +45,10 @@ const messages = defineMessages({ librariesRemaining: 'Libraries Remaining: {count}', startscan: 'Start Scan', cancelscan: 'Cancel Scan', + syncFailedNoLibrariesFound: 'No libraries were found', + syncFailedAutomaticGroupedFolders: + 'Custom authentication with Automatic Library Grouping not supported', + syncFailedGenericError: 'Something went wrong while syncing libraries', }); interface Library { @@ -70,6 +74,7 @@ const SettingsJellyfin: React.FC = ({ showAdvancedSettings, }) => { const [isSyncing, setIsSyncing] = useState(false); + const toasts = useToasts(); const { data, @@ -117,11 +122,38 @@ const SettingsJellyfin: React.FC = ({ params.enable = activeLibraries.join(','); } - await axios.get('/api/v1/settings/jellyfin/library', { - params, - }); - setIsSyncing(false); - revalidate(); + try { + await axios.get('/api/v1/settings/jellyfin/library', { + params, + }); + setIsSyncing(false); + revalidate(); + } catch (e) { + if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') { + toasts.addToast( + intl.formatMessage(messages.syncFailedAutomaticGroupedFolders), + { + autoDismiss: true, + appearance: 'warning', + } + ); + } else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') { + toasts.addToast( + intl.formatMessage(messages.syncFailedNoLibrariesFound), + { + autoDismiss: true, + appearance: 'error', + } + ); + } else { + toasts.addToast(intl.formatMessage(messages.syncFailedGenericError), { + autoDismiss: true, + appearance: 'error', + }); + } + setIsSyncing(false); + revalidate(); + } }; const startScan = async () => {