From 850f9f081a14606a5c0d0b652a2cf6446d1036e7 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 3 Feb 2026 18:51:28 +0000 Subject: [PATCH 1/2] fix(jellyfin): Allow all media types when allowUnknown is true Fixes #445 --- config/jellyfin.json.example | 2 +- .../docs/configuration/sources/jellyfin.mdx | 33 ++++++------ .../infrastructure/config/source/jellyfin.ts | 17 +++++- src/backend/sources/JellyfinApiSource.ts | 54 ++++++++++++++++--- src/backend/sources/ScrobbleSources.ts | 3 +- src/backend/tests/jellyfin/jellyfin.test.ts | 12 ++++- 6 files changed, 96 insertions(+), 25 deletions(-) diff --git a/config/jellyfin.json.example b/config/jellyfin.json.example index a56a5e5e..0565d204 100644 --- a/config/jellyfin.json.example +++ b/config/jellyfin.json.example @@ -16,7 +16,7 @@ "librariesAllow": ["GoodMusic"], "librariesBlock": ["BadMusic"], "additionalAllowedLibraryTypes": ["musicvideos"], - "allowUnknown": false, + "allowMediaTypes": ["audio"], }, "options": { "logPayload": true, diff --git a/docsite/docs/configuration/sources/jellyfin.mdx b/docsite/docs/configuration/sources/jellyfin.mdx index db8117d4..c06f7458 100644 --- a/docsite/docs/configuration/sources/jellyfin.mdx +++ b/docsite/docs/configuration/sources/jellyfin.mdx @@ -27,9 +27,11 @@ By default... * Allowed Users (`usersAllow` or `JELLYFIN_USERS_ALLOW`) are only necessary if you want to scrobble for additional users. * multi-scrobbler will **only** scrobble media found in Jellyfin libraries that were labelled as **Music.** * `librariesAllow` or `JELLYFIN_LIBRARIES_ALLOW` will override this - * OR use `additionalAllowedLibraryTypes` to allow more types (like `mixed` or `book` for audiobooks) -* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio.** - * To force multi-scrobbler to scrobble when media is detected as **Unknown** use `"allowUnknown": true` in file/aio configuration. + * OR use `additionalAllowedLibraryTypes` to allow more *library* types (like those labelled as `mixed` or `book` for audiobooks) +* multi-scrobbler will **only** scrobble media Jellyfin detects as **Audio**. + * To allow other media types to be scrobbled set `allowMediaTypes` or `JELLYFIN_MEDIATYPES_ALLOW` with a list of [official types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22). + * One or more of: `Unknown`, `Video`, `Audio`, `Photo`, `Book` + * If you use this setting **you must explicitly specify `Audio`**, it will no longer be provided as a default @@ -37,18 +39,19 @@ By default... - | Environmental Variable | Required? | Default | Description | - | -------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------ | - | `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` | - | `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API | - | `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** | - | `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** | - | `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for | - | `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for | - | `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from | - | `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from | - | `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from | - | `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from | + | Environmental Variable | Required? | Default | Description | + | --------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` | + | `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API | + | `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** | + | `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** | + | `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for | + | `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for | + | `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from | + | `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from | + | `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from | + | `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from | + | `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` | diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index 1e491d87..466a3867 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -1,7 +1,9 @@ import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./index.js"; import { // @ts-expect-error weird typings? - CollectionType + CollectionType, + // @ts-expect-error weird typings? + MediaType, } from "@jellyfin/sdk/lib/generated-client/index.js"; export interface JellyApiData extends CommonSourceData { @@ -66,9 +68,22 @@ export interface JellyApiData extends CommonSourceData { * Force media with a type of "Unknown" to be counted as Audio * * @default false + * @deprecated use allowMediaTypes instead */ allowUnknown?: boolean + /** + * Allow these media types to be scrobbled. + * + * If not defined or empty, uses 'Audio' as default. If non-empty then *only* uses these types (make sure you include Audio!) + * + * Values are case-insensitive. + * + * See https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types + * + */ + allowMediaTypes?: MediaType[] | string + /** * HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) * If unspecified it will use the normal server HOST and PORT from the `url` diff --git a/src/backend/sources/JellyfinApiSource.ts b/src/backend/sources/JellyfinApiSource.ts index d5525d23..e641923e 100644 --- a/src/backend/sources/JellyfinApiSource.ts +++ b/src/backend/sources/JellyfinApiSource.ts @@ -83,6 +83,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource { librariesAllow: string[] = []; librariesBlock: string[] = []; allowedLibraryTypes: CollectionType[] = []; + allowedMediaTypes: MediaType = [MediaType.Audio]; logFilterFailure: false | 'debug' | 'warn'; @@ -128,6 +129,8 @@ export default class JellyfinApiSource extends MemoryPositionalSource { librariesAllow = [], librariesBlock = [], additionalAllowedLibraryTypes = [], + allowMediaTypes = [], + allowUnknown } = {}, options: { logFilterFailure = (isDebugMode() ? 'debug' : 'warn') @@ -160,6 +163,25 @@ export default class JellyfinApiSource extends MemoryPositionalSource { this.librariesAllow = parseArrayFromMaybeString(librariesAllow, {lower: true}); this.librariesBlock = parseArrayFromMaybeString(librariesBlock, {lower: true}); this.allowedLibraryTypes = Array.from(new Set(['music', ...parseArrayFromMaybeString(additionalAllowedLibraryTypes, {lower: true})])); + let mt = parseArrayFromMaybeString(allowMediaTypes, {lower: true}); + if(mt.length > 0) { + this.allowedMediaTypes = []; + for(const a of allowMediaTypes) { + try { + this.allowedMediaTypes.push(stringToMediaType(a)); + } catch (e) { + throw e; + } + } + } + if(allowUnknown !== undefined) { + this.logger.warn('allowUnknown is deprecated! use allowMediaTypes (`JELLYFIN_MEDIATYPES_ALLOW`) instead.'); + if(mt.length > 0) { + this.logger.warn('allowUnknown and allowMediaTypes are both set. Ignoring allowUnknown.') + } else if(allowUnknown === true) { + this.allowedMediaTypes.push(MediaType.Unknown); + } + } return true; } @@ -342,12 +364,11 @@ export default class JellyfinApiSource extends MemoryPositionalSource { } if(state.play !== undefined) { - if(state.play.meta.mediaType !== MediaType.Audio - && (state.play.meta.mediaType !== MediaType.Unknown - || state.play.meta.mediaType === MediaType.Unknown && !this.config.data.allowUnknown - ) - ) { - return `media detected as ${state.play.meta.mediaType} (MediaType) is not allowed`; + if(state.play.meta?.mediaType === undefined && !this.allowedMediaTypes.includes(MediaType.Unknown)) { + return `media without a MediaType detected is not allowed (Unknown not included in allowMediaTypes)`; + } + if(!this.allowedMediaTypes.includes(state.play.meta.mediaType)) { + return `media detected as ${state.play.meta.mediaType} (MediaType) is not included in allowMediaTypes`; } } @@ -577,3 +598,24 @@ const ticksToMs = (ticks: number) => { return ticks; // Undefined or not a number. } } + +const stringToMediaType = (str: string): MediaType => { + switch(str.trim().toLocaleLowerCase()) { + case 'unknown': + return MediaType.Unknown; + case 'video': + case 'videos': + return MediaType.Video; + case 'audio': + case 'music': + return MediaType.Audio; + case 'photo': + case 'photos': + return MediaType.Photo; + case 'book': + case 'books': + return MediaType.Book; + default: + throw new Error(`Not a valid MediaType: ${str}`); + } +} \ No newline at end of file diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 45d38a6c..dc82d2e0 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -334,7 +334,8 @@ export default class ScrobbleSources { deviceBlock: process.env.JELLYFIN_DEVICES_BLOCK, librariesAllow: process.env.JELLYFIN_LIBRARIES_ALLOW, librariesBlock: process.env.JELLYFIN_LIBRARIES_BLOCK, - frontendUrlOverride: process.env.JELLYFIN_FRONTEND_URL_OVERRIDE + frontendUrlOverride: process.env.JELLYFIN_FRONTEND_URL_OVERRIDE, + allowMediaTypes: process.env.JELLYFIN_MEDIATYPES_ALLOW }; if (!Object.values(j).every(x => x === undefined)) { configs.push({ diff --git a/src/backend/tests/jellyfin/jellyfin.test.ts b/src/backend/tests/jellyfin/jellyfin.test.ts index cb01abac..0b14a33b 100644 --- a/src/backend/tests/jellyfin/jellyfin.test.ts +++ b/src/backend/tests/jellyfin/jellyfin.test.ts @@ -73,6 +73,7 @@ describe("Jellyfin API Source", function() { librariesAllow: ['MuSiCoNe'], librariesBlock: ['MuSiCbAd'], additionalAllowedLibraryTypes: ['TVShowS'], + allowMediaTypes: ['Unknown'], ...defaultJfApiCreds}); await jf.buildInitData(); @@ -83,6 +84,7 @@ describe("Jellyfin API Source", function() { expect(jf.librariesAllow).to.be.eql(['musicone']); expect(jf.librariesBlock).to.be.eql(['musicbad']); expect(jf.allowedLibraryTypes).to.be.eql(['music','tvshows']); + expect(jf.allowedMediaTypes).to.be.eql(['Unknown']); await jf.destroy(); }); @@ -343,7 +345,7 @@ describe("Jellyfin API Source", function() { await jf.destroy(); }); - it('Should allow Play with unknown mediaType if specified in options', async function () { + it('Should allow Play with unknown mediaType if specified in deprecated allowUnknown option', async function () { const jf = createJfApi({...defaultJfApiCreds}); jf.config.data.allowUnknown = true; await jf.buildInitData(); @@ -351,6 +353,14 @@ describe("Jellyfin API Source", function() { expect(jf.isActivityValid(playWithMeta({mediaType: 'Unknown'}), validSession)).to.be.true; await jf.destroy(); }); + + it('Should allow Play with unknown mediaType if specified in allowMediaTypes option', async function () { + const jf = createJfApi({...defaultJfApiCreds, allowMediaTypes: ['Unknown','Audio']}); + await jf.buildInitData(); + + expect(jf.isActivityValid(playWithMeta({mediaType: 'Unknown'}), validSession)).to.be.true; + await jf.destroy(); + }); it('Should disallow NowPlayingItem that is a theme song (ExtraType)', async function () { const jf = createJfApi({...defaultJfApiCreds}); -- 2.51.2 From eab33559ca4b39bd5435f55dea3cadd7b9bcebcd Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 10 Feb 2026 19:39:35 +0000 Subject: [PATCH 2/2] fix: allow musicvideo media type and use allowedMediaTypes for NowPlayingItem check --- src/backend/sources/JellyfinApiSource.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/backend/sources/JellyfinApiSource.ts b/src/backend/sources/JellyfinApiSource.ts index e641923e..84fa5a44 100644 --- a/src/backend/sources/JellyfinApiSource.ts +++ b/src/backend/sources/JellyfinApiSource.ts @@ -378,8 +378,11 @@ export default class JellyfinApiSource extends MemoryPositionalSource { (play.data.artists === undefined || play.data.artists.length === 0) */) { return `media detected as a ThemeSong (ExtraType) is not allowed`; } - if(session.NowPlayingItem.Type !== 'Audio') { - return `media detected as a ${session.NowPlayingItem.Type} (Type) is not allowed`; + if(session.NowPlayingItem === undefined && !this.allowedMediaTypes.includes(MediaType.Unknown)) { + return `media without a Type detected is not allowed (Unknown not included in allowMediaTypes)`; + } + if(!this.allowedMediaTypes.includes(session.NowPlayingItem.Type)) { + return `media detected as a ${session.NowPlayingItem.Type} (Type) is not allowed`; } } @@ -615,6 +618,9 @@ const stringToMediaType = (str: string): MediaType => { case 'book': case 'books': return MediaType.Book; + case 'musicvideo': + case 'musicvideos': + return 'MusicVideo' as MediaType; default: throw new Error(`Not a valid MediaType: ${str}`); } -- 2.51.2