From 3856061fe1ee4d3457996586b4979ad9dd60765a Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Tue, 16 Apr 2024 00:04:11 +0500 Subject: [PATCH] fix(jellyfinapi): refactors jellyfin library sync to support automatic grouping and collections (#700) * 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 * refactor(i18n): adds the suffix "jellyfin" to jellyfin library sync message keys * refactor(i18n): extract translation keys * refactor: remove console logs * refactor: remove more console logs * refactor: apply review suggestions * chore: fix prettier failing on .github file --- .github/FUNDING.yml | 2 +- server/api/jellyfin.ts | 76 +++++++++++++------- server/routes/settings/index.ts | 15 +++- src/components/Settings/SettingsJellyfin.tsx | 48 +++++++++++-- src/i18n/locale/en.json | 3 + 5 files changed, 110 insertions(+), 34 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0189cafb4..6e08dfb2a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [Fallenbagel] \ No newline at end of file +github: [Fallenbagel] diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 768398a06..cd7fb1cfc 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,45 @@ 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`); + + 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 ?? 'Me'}/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 +231,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..41821dcac 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,19 @@ 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(); + + // 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..180010c93 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -34,6 +34,11 @@ const messages = defineMessages({ externalUrl: 'External URL', internalUrl: 'Internal URL', jellyfinForgotPasswordUrl: 'Forgot Password URL', + jellyfinSyncFailedNoLibrariesFound: 'No libraries were found', + jellyfinSyncFailedAutomaticGroupedFolders: + 'Custom authentication with Automatic Library Grouping not supported', + jellyfinSyncFailedGenericError: + 'Something went wrong while syncing libraries', validationUrl: 'You must provide a valid URL', syncing: 'Syncing', syncJellyfin: 'Sync Libraries', @@ -70,6 +75,7 @@ const SettingsJellyfin: React.FC = ({ showAdvancedSettings, }) => { const [isSyncing, setIsSyncing] = useState(false); + const toasts = useToasts(); const { data, @@ -117,11 +123,43 @@ 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.jellyfinSyncFailedAutomaticGroupedFolders + ), + { + autoDismiss: true, + appearance: 'warning', + } + ); + } else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') { + toasts.addToast( + intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound), + { + autoDismiss: true, + appearance: 'error', + } + ); + } else { + toasts.addToast( + intl.formatMessage(messages.jellyfinSyncFailedGenericError), + { + autoDismiss: true, + appearance: 'error', + } + ); + } + setIsSyncing(false); + revalidate(); + } }; const startScan = async () => { diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 9ebbed252..73e6170fb 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -946,6 +946,9 @@ "components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.", "components.Settings.jellyfinsettings": "{mediaServerName} Settings", "components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.", + "components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found", + "components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported", + "components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries", "components.Settings.librariesRemaining": "Libraries Remaining: {count}", "components.Settings.manualscan": "Manual Library Scan", "components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",