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 () => {