fix(xtream): drop URL-only season overviews and fall back to TMDB (#1382)

Xtream panels routinely fill get_series_info seasons[].overview with a
bare cover-image URL, which rendered verbatim under the season tabs.
URL-only overviews are now treated as absent (sanitizeProviderOverview),
and the lazy season enrichment stores the TMDB season overview on the
selection (tmdb_season_overviews) as the fallback description - same
cached /tv/{id}/season/{n} payload, so no extra requests. Provider text
keeps priority when it is real prose.

The enrichment write is also convergent now: the serial detail re-fires
season enrichment after every selection write, and the previous
unconditional rewrite scheduled the next cache-served run indefinitely.
A repeat run that changes nothing no longer writes.

buildSeasonDescriptions is extracted from SerialDetailsComponent, which
would otherwise cross the 400-line max-lines limit.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-08-08 11:12:59 +02:00
1 parent 7103f7e734
commit 92be39ef66
13 files changed
+411 -43

No files matched your search

+1
View File
@@ -35,6 +35,7 @@ export * from './lib/portal-activity-item.interface';
export * from './lib/portal-debug.interface';
export * from './lib/playlist-display-label.util';
export * from './lib/portal-playback.interface';
export * from './lib/provider-overview.util';
export * from './lib/random-id.util';
export * from './lib/security-policy-error.utils';
export * from './lib/settings.interface';
@@ -0,0 +1,51 @@
import { sanitizeProviderOverview } from './provider-overview.util';
describe('sanitizeProviderOverview', () => {
it('keeps real description text', () => {
expect(sanitizeProviderOverview('A tense hijack drama.')).toBe(
'A tense hijack drama.'
);
});
it('trims surrounding whitespace', () => {
expect(sanitizeProviderOverview(' Season text ')).toBe('Season text');
});
it('returns null for empty and missing values', () => {
expect(sanitizeProviderOverview('')).toBeNull();
expect(sanitizeProviderOverview(' ')).toBeNull();
expect(sanitizeProviderOverview(null)).toBeNull();
expect(sanitizeProviderOverview(undefined)).toBeNull();
});
it('drops a bare http image URL', () => {
expect(
sanitizeProviderOverview(
'http://line.example.net:80/images/series/cover_small.jpg'
)
).toBeNull();
});
it('drops a bare https URL with a query string', () => {
expect(
sanitizeProviderOverview('https://cdn.example.com/p.jpg?w=300')
).toBeNull();
});
it('drops a bare URL padded with whitespace', () => {
expect(
sanitizeProviderOverview(' http://cdn.example.com/cover.png ')
).toBeNull();
});
it('drops a protocol-relative URL', () => {
expect(
sanitizeProviderOverview('//cdn.example.com/cover.jpg')
).toBeNull();
});
it('keeps prose that merely contains a URL', () => {
const text = 'More info at http://example.com/season1';
expect(sanitizeProviderOverview(text)).toBe(text);
});
});
@@ -0,0 +1,26 @@
/**
* Xtream panels routinely fill editorial text fields with junk — most
* commonly a season `overview` that holds a bare cover-image URL instead
* of a description. Rendering that verbatim puts a raw URL on screen, so
* a value that is nothing but a URL is treated as absent.
*/
/**
* Trimmed overview text, or `null` when the value is empty or a bare URL.
* Prose that merely contains a URL is kept — only a single URL token with
* no surrounding text is junk.
*/
export function sanitizeProviderOverview(
text: string | null | undefined
): string | null {
const trimmed = text?.trim();
if (!trimmed) {
return null;
}
return isBareUrl(trimmed) ? null : trimmed;
}
/** A single absolute or protocol-relative URL token (`http://…/x.jpg`). */
function isBareUrl(text: string): boolean {
return /^(?:https?:)?\/\/\S+$/i.test(text);
}
@@ -8,6 +8,13 @@ export interface XtreamSerieDetails {
seasons: XtreamSerieSeason[];
info: XtreamSerieInfo;
episodes: Record<string, XtreamSerieEpisode[]>;
/**
* Populated by lazy TMDB season enrichment; absent in raw provider
* responses. Keyed by the provider season key (the `episodes` record
* key). Detail views use it as the season-description fallback when
* the provider's `seasons[].overview` is empty or URL-only junk.
*/
tmdb_season_overviews?: Record<string, string>;
}
export interface XtreamSerieInfo {