Skip to content

Commit cac8163

Browse files
authored
Merge pull request #374 from wolffshots/feature/optionally-redirect-image-paths
feat(JellyfinApiSource): add option to override the Jellyfin base url
2 parents 0790da2 + 11dbdaf commit cac8163

3 files changed

Lines changed: 137 additions & 2 deletions

File tree

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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,18 @@ export default class JellyfinApiSource extends MemoryPositionalSource {
364364
return true;
365365
}
366366

367+
replaceUrlIfNeeded = (url: string): string => {
368+
if(
369+
this.config.data.frontendUrlOverride !== undefined &&
370+
this.config.data.frontendUrlOverride.length > 0 &&
371+
url !== undefined &&
372+
url.length > 0
373+
) {
374+
return url.replace(this.config.data.url, this.config.data.frontendUrlOverride);
375+
}
376+
return url;
377+
}
378+
367379
formatPlayObjAware(obj: BaseItemDto, options: FormatPlayObjectOptions = {}): PlayObject {
368380
const play = JellyfinApiSource.formatPlayObj(obj, options);
369381

@@ -377,7 +389,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource {
377389

378390
if(AlbumId !== undefined && AlbumPrimaryImageTag !== undefined) {
379391
const existingArt = play.meta?.art || {};
380-
existingArt.album = this.imageApi.getItemImageUrlById(AlbumId, undefined, {maxHeight: 500});
392+
existingArt.album = this.replaceUrlIfNeeded(this.imageApi.getItemImageUrlById(AlbumId, undefined, {maxHeight: 500}));
381393
play.meta.art = existingArt;
382394
}
383395
if(ParentId !== undefined) {
@@ -386,7 +398,7 @@ export default class JellyfinApiSource extends MemoryPositionalSource {
386398
u.searchParams.append('serviceId', ServerId);
387399
play.meta.url = {
388400
...(play.meta?.url || {}),
389-
web: u.toString().replace('%23', '#')
401+
web: this.replaceUrlIfNeeded(u.toString().replace('%23', '#'))
390402
}
391403
}
392404

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

Lines changed: 116 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,120 @@ 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+
175+
describe('Correctly replaces URLs with frontendUrlOverride', function () {
176+
177+
const sourceUrl = 'http://192.168.10.11:8096';
178+
const frontendUrlOverride = 'https://myjellyfin.com';
179+
180+
it('Should return original URL when frontendUrlOverride is not set', async function () {
181+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl});
182+
await jf.buildInitData();
183+
184+
const testUrl = `${sourceUrl}/Items/123/Images/Primary`;
185+
expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl);
186+
187+
await jf.destroy();
188+
});
189+
190+
it('Should return original URL when frontendUrlOverride is empty string', async function () {
191+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: ''});
192+
await jf.buildInitData();
193+
194+
const testUrl = `${sourceUrl}/Items/123/Images/Primary`;
195+
expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl);
196+
197+
await jf.destroy();
198+
});
199+
200+
it('Should replace source URL with frontendUrlOverride when set', async function () {
201+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride});
202+
await jf.buildInitData();
203+
204+
const testUrl = `${sourceUrl}/Items/123/Images/Primary`;
205+
const expectedUrl = `${frontendUrlOverride}/Items/123/Images/Primary`;
206+
expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(expectedUrl);
207+
208+
await jf.destroy();
209+
});
210+
211+
it('Should return original URL when input URL is undefined', async function () {
212+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride});
213+
await jf.buildInitData();
214+
215+
expect(jf.replaceUrlIfNeeded(undefined)).to.be.undefined;
216+
217+
await jf.destroy();
218+
});
219+
220+
it('Should return original URL when input URL is empty string', async function () {
221+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride});
222+
await jf.buildInitData();
223+
224+
expect(jf.replaceUrlIfNeeded('')).to.be.eql('');
225+
226+
await jf.destroy();
227+
});
228+
229+
it('Should not replace URL when source URL is not present', async function () {
230+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride});
231+
await jf.buildInitData();
232+
233+
const testUrl = 'https://some-other-domain.com/Items/123/Images/Primary';
234+
expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(testUrl);
235+
236+
await jf.destroy();
237+
});
238+
239+
it('Should not replace multiple occurrences of source URL', async function () {
240+
const jf = createJfApi({...defaultJfApiCreds, url: sourceUrl, frontendUrlOverride: frontendUrlOverride});
241+
await jf.buildInitData();
242+
243+
const testUrl = `${sourceUrl}/redirect?url=${sourceUrl}/Items/123`;
244+
const expectedUrl = `${frontendUrlOverride}/redirect?url=${sourceUrl}/Items/123`;
245+
expect(jf.replaceUrlIfNeeded(testUrl)).to.be.eql(expectedUrl);
246+
247+
await jf.destroy();
248+
});
249+
});
250+
135251
describe('Correctly detects activity as valid/invalid', function() {
136252

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

0 commit comments

Comments
 (0)