diff --git a/web/components/details/sourcePlaybackPresentation.ts b/web/components/details/sourcePlaybackPresentation.ts index e71b1da88..381f83df1 100644 --- a/web/components/details/sourcePlaybackPresentation.ts +++ b/web/components/details/sourcePlaybackPresentation.ts @@ -135,15 +135,21 @@ export function sourceBadges(stream: StreamSource, info: { debrid: boolean; unca * what tells two "Torrentio 1080p" rows apart), else the addon's own label. */ export function sourceTitle(stream: Pick) { - const file = stream.behaviorHints?.filename?.trim(); + const rawFilename = stream.behaviorHints?.filename; + const file = typeof rawFilename === "string" ? rawFilename.trim() : ""; if (file) return file; - const firstLine = stream.description?.split("\n").map((line) => line.trim()).find(Boolean); + + const description = typeof stream.description === "string" ? stream.description : ""; + const firstLine = description.split("\n").map((line) => line.trim()).find(Boolean); // Stremio addons put the release name on the first description line and // decorations (šŸ‘¤ seeders, šŸ’¾ size, āš™ļø provider) on the following ones. if (firstLine && /[._ -]/.test(firstLine) && firstLine.length > 8 && !/^[šŸ‘¤šŸ’¾āš™ļøšŸ”—šŸ“¦]/u.test(firstLine)) { return firstLine.replace(/\s*šŸ’¾.*$/u, "").trim() || firstLine; } - return stream.source || stream.addonName; + + if (typeof stream.source === "string" && stream.source) return stream.source; + if (typeof stream.addonName === "string" && stream.addonName) return stream.addonName; + return "Source"; } export type SourceFilter = "all" | "here" | "4k" | "1080p" | "cached"; diff --git a/web/tests/source-title-runtime-types.test.cjs b/web/tests/source-title-runtime-types.test.cjs new file mode 100644 index 000000000..387812c9e --- /dev/null +++ b/web/tests/source-title-runtime-types.test.cjs @@ -0,0 +1,29 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { load } = require('./load.cjs'); + +const { sourceTitle } = load('components/details/sourcePlaybackPresentation.ts'); +const stream = (overrides = {}) => ({ + source: 'Fixture', + addonName: 'Library', + ...overrides +}); + +test('sourceTitle tolerates malformed runtime filename values', () => { + for (const filename of [123, { name: 'movie.mkv' }, ['movie.mkv']]) { + assert.equal(sourceTitle(stream({ behaviorHints: { filename } })), 'Fixture'); + } +}); + +test('sourceTitle tolerates malformed runtime description values', () => { + for (const description of [123, { text: 'movie' }, ['movie']]) { + assert.equal(sourceTitle(stream({ description })), 'Fixture'); + } +}); + +test('sourceTitle keeps valid provider labels and safe fallbacks', () => { + assert.equal(sourceTitle(stream({ behaviorHints: { filename: ' Movie.2026.1080p.mkv ' } })), 'Movie.2026.1080p.mkv'); + assert.equal(sourceTitle(stream({ description: 'Movie.2026.1080p.WEB-DL.mkv\nšŸ’¾ 3.2 GB' })), 'Movie.2026.1080p.WEB-DL.mkv'); + assert.equal(sourceTitle({ source: 42, addonName: 'Fallback addon', behaviorHints: { filename: null }, description: null }), 'Fallback addon'); + assert.equal(sourceTitle({ source: null, addonName: null, behaviorHints: { filename: {} }, description: {} }), 'Source'); +});