Skip to content

Commit ac6b04f

Browse files
committed
feat(JellyfinApiSource): add option to override the Jellyfin base url used in the frontend
1 parent 0790da2 commit ac6b04f

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

src/backend/common/infrastructure/config/source/jellyfin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ export interface JellyApiData extends CommonSourceData {
8787
* @default false
8888
*/
8989
allowUnknown?: boolean
90+
91+
/**
92+
* HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)
93+
* If unspecified it will use the normal server HOST and PORT from the `url`
94+
* Necessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly
95+
* */
96+
frontendUrlOverride?: string
9097
}
9198

9299
export interface JellyApiOptions extends CommonSourceOptions {

src/backend/sources/JellyfinApiSource.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,14 @@ export default class JellyfinApiSource extends MemoryPositionalSource {
378378
if(AlbumId !== undefined && AlbumPrimaryImageTag !== undefined) {
379379
const existingArt = play.meta?.art || {};
380380
existingArt.album = this.imageApi.getItemImageUrlById(AlbumId, undefined, {maxHeight: 500});
381+
if(
382+
this.config.data.frontendUrlOverride !== undefined &&
383+
this.config.data.frontendUrlOverride.length > 0 &&
384+
existingArt.album !== undefined &&
385+
existingArt.album.length > 0
386+
) {
387+
existingArt.album = existingArt.album.replace(this.config.data.url, this.config.data.frontendUrlOverride);
388+
}
381389
play.meta.art = existingArt;
382390
}
383391
if(ParentId !== undefined) {
@@ -388,6 +396,14 @@ export default class JellyfinApiSource extends MemoryPositionalSource {
388396
...(play.meta?.url || {}),
389397
web: u.toString().replace('%23', '#')
390398
}
399+
if(
400+
this.config.data.frontendUrlOverride !== undefined &&
401+
this.config.data.frontendUrlOverride.length > 0 &&
402+
play.meta.url.web !== undefined &&
403+
play.meta.url.web.length > 0
404+
) {
405+
play.meta.url.web = play.meta.url.web.replace(this.config.data.url, this.config.data.frontendUrlOverride);
406+
}
391407
}
392408

393409
return play;

src/backend/tests/jellyfin/jellyfin.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
// @ts-expect-error weird typings?
1616
SessionInfo,
1717
} from "@jellyfin/sdk/lib/generated-client/index.js";
18+
// @ts-expect-error weird typings?
19+
import { getImageApi } from "@jellyfin/sdk/lib/utils/api/index.js";
1820
import { PlayerStateDataMaybePlay } from "../../common/infrastructure/Atomic.js";
1921

2022
const dataAsFixture = (data: any): TestFixture => {
@@ -132,6 +134,44 @@ describe("Jellyfin API Source", function() {
132134
});
133135
});
134136

137+
describe('Parses and replaces frontendUrlOverride correctly if set', function () {
138+
139+
const item = {
140+
AlbumId: 123,
141+
AlbumPrimaryImageTag: 'Primary',
142+
ParentId: 456,
143+
ServerId: 789,
144+
};
145+
const sourceUrl = 'http://192.168.10.11:8096';
146+
const frontendUrlOverride = 'https://myjellyfin.com';
147+
148+
it('Should use default url if frontendUrlOverride unset', async function () {
149+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl});
150+
await jf.buildInitData();
151+
jf.address = sourceUrl;
152+
jf.api = jf.client.createApi(jf.address);
153+
jf.imageApi = getImageApi(jf.api);
154+
155+
expect(jf.formatPlayObjAware(item).meta.art.album).to.be.eql(`${sourceUrl}/Items/123/Images/Primary?maxHeight=500`);
156+
expect(jf.formatPlayObjAware(item).meta.url.web).to.be.eql(`${sourceUrl}/web/#/details?id=456&serviceId=789`);
157+
158+
await jf.destroy();
159+
});
160+
161+
it('Should parse and replace frontendUrlOverride correctly', async function () {
162+
const jf = createJfApi({...defaultJfApiCreds, frontendUrlOverride: frontendUrlOverride, url: sourceUrl});
163+
await jf.buildInitData();
164+
jf.address = sourceUrl;
165+
jf.api = jf.client.createApi(jf.address);
166+
jf.imageApi = getImageApi(jf.api);
167+
168+
expect(jf.formatPlayObjAware(item).meta.art.album).to.be.eql(`${frontendUrlOverride}/Items/123/Images/Primary?maxHeight=500`);
169+
expect(jf.formatPlayObjAware(item).meta.url.web).to.be.eql(`${frontendUrlOverride}/web/#/details?id=456&serviceId=789`);
170+
171+
await jf.destroy();
172+
});
173+
});
174+
135175
describe('Correctly detects activity as valid/invalid', function() {
136176

137177
describe('Filters from Configuration', function() {

0 commit comments

Comments
 (0)