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
This commit is contained in:
Fallenbagel
2024-04-16 00:04:11 +05:00
committed by GitHub
parent 0900a95532
commit 3856061fe1
5 changed files with 109 additions and 33 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
github: [Fallenbagel] github: [Fallenbagel]

View File

@@ -9,6 +9,9 @@ export interface JellyfinUserResponse {
ServerId: string; ServerId: string;
ServerName: string; ServerName: string;
Id: string; Id: string;
Configuration: {
GroupedFolders: string[];
};
Policy: { Policy: {
IsAdministrator: boolean; IsAdministrator: boolean;
}; };
@@ -24,6 +27,13 @@ export interface JellyfinUserListResponse {
users: JellyfinUserResponse[]; users: JellyfinUserResponse[];
} }
interface JellyfinMediaFolder {
Name: string;
Id: string;
Type: string;
CollectionType: string;
}
export interface JellyfinLibrary { export interface JellyfinLibrary {
type: 'show' | 'movie'; type: 'show' | 'movie';
key: string; key: string;
@@ -175,24 +185,45 @@ class JellyfinAPI {
public async getLibraries(): Promise<JellyfinLibrary[]> { public async getLibraries(): Promise<JellyfinLibrary[]> {
try { try {
// TODO: Try to fix automatic grouping without fucking up LDAP users const mediaFolders = await this.axios.get<any>(`/Library/MediaFolders`);
// const libraries = await this.axios.get<any>('/Library/VirtualFolders');
const account = await this.axios.get<any>( return this.mapLibraries(mediaFolders.data.Items);
`/Users/${this.userId ?? 'Me'}/Views` } catch (mediaFoldersError) {
); // fallback to user views to get libraries
// this only affects LDAP users
try {
const mediaFolders = await this.axios.get<any>(
`/Users/${this.userId ?? 'Me'}/Views`
);
const response: JellyfinLibrary[] = account.data.Items.filter( return this.mapLibraries(mediaFolders.data.Items);
(Item: any) => { } catch (e) {
return ( logger.error(
Item.Type === 'CollectionFolder' && `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
Item.CollectionType !== 'music' && { label: 'Jellyfin API' }
Item.CollectionType !== 'books' && );
Item.CollectionType !== 'musicvideos' && return [];
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 <JellyfinLibrary>{ return <JellyfinLibrary>{
key: Item.Id, key: Item.Id,
title: Item.Name, title: Item.Name,
@@ -200,21 +231,12 @@ class JellyfinAPI {
agent: 'jellyfin', 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<JellyfinLibraryItem[]> { public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/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( return contents.data.Items.filter(

View File

@@ -261,7 +261,7 @@ settingsRoutes.post('/jellyfin', (req, res) => {
return res.status(200).json(settings.jellyfin); 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(); const settings = getSettings();
if (req.query.sync) { if (req.query.sync) {
@@ -281,6 +281,19 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
const libraries = await jellyfinClient.getLibraries(); 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 newLibraries: Library[] = libraries.map((library) => {
const existing = settings.jellyfin.libraries.find( const existing = settings.jellyfin.libraries.find(
(l) => l.id === library.key && l.name === library.title (l) => l.id === library.key && l.name === library.title

View File

@@ -34,6 +34,11 @@ const messages = defineMessages({
externalUrl: 'External URL', externalUrl: 'External URL',
internalUrl: 'Internal URL', internalUrl: 'Internal URL',
jellyfinForgotPasswordUrl: 'Forgot Password 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', validationUrl: 'You must provide a valid URL',
syncing: 'Syncing', syncing: 'Syncing',
syncJellyfin: 'Sync Libraries', syncJellyfin: 'Sync Libraries',
@@ -70,6 +75,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
showAdvancedSettings, showAdvancedSettings,
}) => { }) => {
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const toasts = useToasts();
const { const {
data, data,
@@ -117,11 +123,43 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
params.enable = activeLibraries.join(','); params.enable = activeLibraries.join(',');
} }
await axios.get('/api/v1/settings/jellyfin/library', { try {
params, await axios.get('/api/v1/settings/jellyfin/library', {
}); params,
setIsSyncing(false); });
revalidate(); 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 () => { const startScan = async () => {

View File

@@ -946,6 +946,9 @@
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.", "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.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.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.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.manualscan": "Manual Library Scan", "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!", "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!",