diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0189cafb..6e08dfb2 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 768398a0..cd7fb1cf 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 mediaFolders = await this.axios.get(`/Library/MediaFolders`); - const account = await this.axios.get( - `/Users/${this.userId ?? 'Me'}/Views` - ); + 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` + ); - 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) => { + 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 []; + } + } + } + + 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 de86ed71..41821dca 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 584a98fd..180010c9 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 9ebbed25..73e6170f 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!",