From 11dbdaf6ccb8415d5f1b5b1469f9aed4d71e971a Mon Sep 17 00:00:00 2001 From: wolffshots <16850875+wolffshots@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:53:51 +0200 Subject: [PATCH] chore(JellyfinApiSource): extract URL replacement to func and add tests --- src/backend/sources/JellyfinApiSource.ts | 32 ++++----- src/backend/tests/jellyfin/jellyfin.test.ts | 76 +++++++++++++++++++++ 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/src/backend/sources/JellyfinApiSource.ts b/src/backend/sources/JellyfinApiSource.ts index 8d080eb0..97f63853 100644 --- a/src/backend/sources/JellyfinApiSource.ts +++ b/src/backend/sources/JellyfinApiSource.ts @@ -364,6 +364,18 @@ export default class JellyfinApiSource extends MemoryPositionalSource { return true; } + replaceUrlIfNeeded = (url: string): string => { + if( + this.config.data.frontendUrlOverride !== undefined && + this.config.data.frontendUrlOverride.length > 0 && + url !== undefined && + url.length > 0 + ) { + return url.replace(this.config.data.url, this.config.data.frontendUrlOverride); + } + return url; + } + formatPlayObjAware(obj: BaseItemDto, options: FormatPlayObjectOptions = {}): PlayObject { const play = JellyfinApiSource.formatPlayObj(obj, options); @@ -377,15 +389,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource { if(AlbumId !== undefined && AlbumPrimaryImageTag !== undefined) { const existingArt = play.meta?.art || {}; - existingArt.album = this.imageApi.getItemImageUrlById(AlbumId, undefined, {maxHeight: 500}); - if( - this.config.data.frontendUrlOverride !== undefined && - this.config.data.frontendUrlOverride.length > 0 && - existingArt.album !== undefined && - existingArt.album.length > 0 - ) { - existingArt.album = existingArt.album.replace(this.config.data.url, this.config.data.frontendUrlOverride); - } + existingArt.album = this.replaceUrlIfNeeded(this.imageApi.getItemImageUrlById(AlbumId, undefined, {maxHeight: 500})); play.meta.art = existingArt; } if(ParentId !== undefined) { @@ -394,15 +398,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource { u.searchParams.append('serviceId', ServerId); play.meta.url = { ...(play.meta?.url || {}), - web: u.toString().replace('%23', '#') - } - if( - this.config.data.frontendUrlOverride !== undefined && - this.config.data.frontendUrlOverride.length > 0 && - play.meta.url.web !== undefined && - play.meta.url.web.length > 0 - ) { - play.meta.url.web = play.meta.url.web.replace(this.config.data.url, this.config.data.frontendUrlOverride); + web: this.replaceUrlIfNeeded(u.toString().replace('%23', '#')) } } diff --git a/src/backend/tests/jellyfin/jellyfin.test.ts b/src/backend/tests/jellyfin/jellyfin.test.ts index ead84c00..a26e3aa4 100644 --- a/src/backend/tests/jellyfin/jellyfin.test.ts +++ b/src/backend/tests/jellyfin/jellyfin.test.ts @@ -172,6 +172,82 @@ describe("Jellyfin API Source", function() { }); }); + describe('Correctly replaces URLs with frontendUrlOverride', function () { + + const sourceUrl = 'http://192.168.10.11:8096'; + const frontendUrlOverride = 'https://myjellyfin.com'; + + it('Should return original URL when frontendUrlOverride is not set', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl}); + await jf.buildInitData(); + + const testUrl = `${sourceUrl}/Items/123/Images/Primary`; + expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl); + + await jf.destroy(); + }); + + it('Should return original URL when frontendUrlOverride is empty string', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: ''}); + await jf.buildInitData(); + + const testUrl = `${sourceUrl}/Items/123/Images/Primary`; + expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl); + + await jf.destroy(); + }); + + it('Should replace source URL with frontendUrlOverride when set', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride}); + await jf.buildInitData(); + + const testUrl = `${sourceUrl}/Items/123/Images/Primary`; + const expectedUrl = `${frontendUrlOverride}/Items/123/Images/Primary`; + expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(expectedUrl); + + await jf.destroy(); + }); + + it('Should return original URL when input URL is undefined', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride}); + await jf.buildInitData(); + + expect(jf.replaceUrlIfNeeded(undefined)).to.be.undefined; + + await jf.destroy(); + }); + + it('Should return original URL when input URL is empty string', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride}); + await jf.buildInitData(); + + expect(jf.replaceUrlIfNeeded('')).to.be.eql(''); + + await jf.destroy(); + }); + + it('Should not replace URL when source URL is not present', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride}); + await jf.buildInitData(); + + const testUrl = 'https://some-other-domain.com/Items/123/Images/Primary'; + expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl); + + await jf.destroy(); + }); + + it('Should not replace multiple occurrences of source URL', async function () { + const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride}); + await jf.buildInitData(); + + const testUrl = `${sourceUrl}/redirect?url=${sourceUrl}/Items/123`; + const expectedUrl = `${frontendUrlOverride}/redirect?url=${sourceUrl}/Items/123`; + expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(expectedUrl); + + await jf.destroy(); + }); + }); + describe('Correctly detects activity as valid/invalid', function() { describe('Filters from Configuration', function() { -- 2.51.2