feat(playlist): open recognized M3U movies in the VOD detail view (#1420)

M3U entries recognized as movie files now open in the portals' two-state VOD
detail view, fed by TMDB metadata instead of the empty EPG zone. Watch-first:
activation still plays immediately, with plot, cast, rating and artwork below
the player; Escape reveals the Browse hero.

Recognition is a synchronous URL-shape heuristic (movie container extension or
an Xtream-style /movie/ path; radio, DASH, /series/ paths and episode-marker
names keep today's live layout), gated on TMDB enrichment plus the new
default-on Settings.m3uVodDetails toggle. Works in Electron and the PWA.

Review follow-ups included: the playback payload no longer carries TMDB fields
(its identity is the player's source-application key), the persisted volume
reaches the player and survives Browse → Play, the enrichment guard keys on
the full lookup identity, and the saved engine mounts first time instead of
briefly falling back to Video.js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-08-13 17:18:06 +02:00
1 parent 82a2948ffd
commit f7bb3a13db
49 files changed
+2344 -146

No files matched your search

@@ -35,6 +35,7 @@ export function createSettingsForm(
playerAmbientMode: false,
playerUpNextRail: true,
vodAutoFailover: false,
m3uVodDetails: true,
...(supportsEpg
? { epgUrl: new FormArray<FormControl<string | null>>([]) }
: {}),
@@ -125,6 +126,7 @@ export function createSettingsFromFormValue(
playerAmbientMode: value.playerAmbientMode ?? false,
playerUpNextRail: value.playerUpNextRail ?? true,
vodAutoFailover: value.vodAutoFailover ?? false,
m3uVodDetails: value.m3uVodDetails ?? true,
streamFormat: value.streamFormat ?? StreamFormat.AutoStreamFormat,
openStreamOnDoubleClick: value.openStreamOnDoubleClick ?? false,
language: value.language ?? Language.ENGLISH,
@@ -1,8 +1,4 @@
<section
class="settings-group"
id="tmdb"
[formGroup]="form()"
>
<section class="settings-group" id="tmdb" [formGroup]="form()">
<header class="settings-group__header">
<div class="settings-group__header-icon">
<mat-icon>movie</mat-icon>
@@ -98,6 +94,30 @@
</div>
}
@if (form().value.tmdb?.enabled === true) {
@if (m3uVodDetailsControl; as m3uVodDetails) {
<div class="setting-item">
<div class="setting-item__meta">
<h4>
{{ 'SETTINGS.TMDB_M3U_VOD_DETAILS' | translate }}
</h4>
<p>
{{
'SETTINGS.TMDB_M3U_VOD_DETAILS_DESCRIPTION'
| translate
}}
</p>
</div>
<div class="setting-item__control setting-item__toggle">
<mat-checkbox
[formControl]="m3uVodDetails"
data-test-id="tmdb-m3u-vod-details"
></mat-checkbox>
</div>
</div>
}
}
<div class="setting-item">
<div class="setting-item__meta">
<h4>{{ 'SETTINGS.TMDB_CACHE' | translate }}</h4>
@@ -15,6 +15,7 @@ describe('SettingsTmdbSectionComponent', () => {
const createForm = () =>
new FormGroup({
m3uVodDetails: new FormControl(true),
tmdb: new FormGroup({
enabled: new FormControl(true),
apiKey: new FormControl(''),
@@ -86,6 +87,47 @@ describe('SettingsTmdbSectionComponent', () => {
fixture.detectChanges();
});
it('offers the M3U movie-recognition toggle while TMDB is enabled', async () => {
await settle();
const checkbox = queryByTestId('tmdb-m3u-vod-details');
expect(checkbox).not.toBeNull();
(checkbox?.querySelector('input') as HTMLInputElement).click();
fixture.detectChanges();
expect(fixture.componentInstance.m3uVodDetailsControl?.value).toBe(
false
);
});
it('hides the M3U toggle when TMDB is disabled', async () => {
await settle();
fixture.componentInstance.form().get('tmdb.enabled')?.setValue(false);
fixture.detectChanges();
expect(queryByTestId('tmdb-m3u-vod-details')).toBeNull();
});
it('hides the M3U toggle row when the host form lacks the control', async () => {
fixture.destroy();
fixture = TestBed.createComponent(SettingsTmdbSectionComponent);
fixture.componentRef.setInput(
'form',
new FormGroup({
tmdb: new FormGroup({
enabled: new FormControl(true),
apiKey: new FormControl(''),
}),
})
);
fixture.detectChanges();
await settle();
expect(queryByTestId('tmdb-m3u-vod-details')).toBeNull();
});
it('sizes the cache as soon as the section page opens', async () => {
await settle();
@@ -5,7 +5,7 @@ import {
signal,
ViewEncapsulation,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -102,6 +102,18 @@ export class SettingsTmdbSectionComponent {
return (this.form().value.tmdb?.apiKey ?? '').trim();
}
/**
* The M3U movie-recognition toggle lives at the settings ROOT (it is a
* player-layout preference, not part of the persisted `tmdb` group this
* section's `formGroupName` wraps), so the template binds it via
* `[formControl]` instead of a group-relative `formControlName`.
* Nullable: the template hides the row for a form without the control.
*/
get m3uVodDetailsControl(): FormControl | null {
const control = this.form().get('m3uVodDetails');
return control instanceof FormControl ? control : null;
}
async testApiKey(): Promise<void> {
const apiKey = this.enteredApiKey;
if (!apiKey || this.keyTestState() === 'testing') {
@@ -152,5 +164,4 @@ export class SettingsTmdbSectionComponent {
this.cacheError.set(stats === null);
this.cacheStats.set(stats);
}
}
@@ -88,6 +88,7 @@ export const DEFAULT_SETTINGS = {
playerAmbientMode: false,
playerUpNextRail: true,
vodAutoFailover: false,
m3uVodDetails: true,
streamFormat: StreamFormat.AutoStreamFormat,
openStreamOnDoubleClick: false,
language: Language.ENGLISH,
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "مفتاح TMDB API (اختياري)",
"TMDB_API_KEY_DESCRIPTION": "استخدم مفتاح TMDB API الخاص بك بدلاً من المفتاح المدمج",
"TMDB_GET_KEY_HINT": "احصل على مفتاح API مجاني على themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "التعرّف على الأفلام في قوائم تشغيل M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "عناصر قائمة التشغيل التي تبدو كملفات أفلام تُفتح في صفحة تفاصيل الفيلم مع بيانات TMDB الوصفية بدلاً من لوحة دليل البرامج.",
"TMDB_TEST_BUTTON": "التحقق من المفتاح",
"TMDB_TEST_SUCCESS": "المفتاح يعمل — استجاب TMDB بنجاح",
"TMDB_TEST_FAILED": "فشل التحقق — رفض TMDB المفتاح أو تعذر الوصول إليه",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "مفتاح TMDB API (اختياري)",
"TMDB_API_KEY_DESCRIPTION": "استعمل مفتاح TMDB API ديالك بلاصة المفتاح المدمج",
"TMDB_GET_KEY_HINT": "جيب مفتاح API مجاني من themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "التعرف على الأفلام فقوائم M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "العناصر ديال القائمة اللي كيبانو بحال ملفات أفلام كيتفتحو فصفحة تفاصيل الفيلم مع ميتاداتا TMDB بلاصة لوحة دليل البرامج.",
"TMDB_TEST_BUTTON": "تأكد من المفتاح",
"TMDB_TEST_SUCCESS": "المفتاح خدام — TMDB جاوب مزيان",
"TMDB_TEST_FAILED": "التحقق ما نجحش — TMDB رفض المفتاح ولا ما يمكنش الوصول ليه",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Ключ TMDB API (неабавязкова)",
"TMDB_API_KEY_DESCRIPTION": "Выкарыстоўвайце ўласны ключ TMDB API замест убудаванага",
"TMDB_GET_KEY_HINT": "Атрымайце бясплатны API-ключ на themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Распазнаваць фільмы ў плэйлістах M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Элементы плэйліста, падобныя на файлы фільмаў, адкрываюцца на старонцы дэталяў фільма з метаданымі TMDB замест панэлі тэлепраграмы.",
"TMDB_TEST_BUTTON": "Праверыць ключ",
"TMDB_TEST_SUCCESS": "Ключ працуе — TMDB паспяхова адказаў",
"TMDB_TEST_FAILED": "Праверка не ўдалася — TMDB адхіліў ключ або недаступны",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB-API-Schlüssel (optional)",
"TMDB_API_KEY_DESCRIPTION": "Eigenen TMDB-API-Schlüssel anstelle des integrierten verwenden",
"TMDB_GET_KEY_HINT": "Kostenlosen API-Schlüssel auf themoviedb.org erhalten",
"TMDB_M3U_VOD_DETAILS": "Filme in M3U-Playlists erkennen",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Playlist-Einträge, die wie Filmdateien aussehen, öffnen sich in der Film-Detailansicht mit TMDB-Metadaten anstelle des EPG-Bereichs.",
"TMDB_TEST_BUTTON": "Schlüssel prüfen",
"TMDB_TEST_SUCCESS": "Der Schlüssel funktioniert — TMDB hat erfolgreich geantwortet",
"TMDB_TEST_FAILED": "Prüfung fehlgeschlagen — TMDB hat den Schlüssel abgelehnt oder ist nicht erreichbar",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Κλειδί API TMDB (προαιρετικό)",
"TMDB_API_KEY_DESCRIPTION": "Χρησιμοποιήστε το δικό σας κλειδί API TMDB αντί για το ενσωματωμένο",
"TMDB_GET_KEY_HINT": "Αποκτήστε δωρεάν κλειδί API στο themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Αναγνώριση ταινιών σε λίστες αναπαραγωγής M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Οι καταχωρήσεις λίστας που μοιάζουν με αρχεία ταινιών ανοίγουν στην προβολή λεπτομερειών ταινίας με μεταδεδομένα TMDB αντί για τον πίνακα EPG.",
"TMDB_TEST_BUTTON": "Έλεγχος κλειδιού",
"TMDB_TEST_SUCCESS": "Το κλειδί λειτουργεί — το TMDB απάντησε με επιτυχία",
"TMDB_TEST_FAILED": "Ο έλεγχος απέτυχε — το TMDB απέρριψε το κλειδί ή δεν είναι προσβάσιμο",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API key (optional)",
"TMDB_API_KEY_DESCRIPTION": "Use your own TMDB API key instead of the built-in one",
"TMDB_GET_KEY_HINT": "Get a free API key on themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Recognize movies in M3U playlists",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Playlist entries that look like movie files open in the movie detail view with TMDB metadata instead of the program guide panel.",
"TMDB_TEST_BUTTON": "Check key",
"TMDB_TEST_SUCCESS": "The key works — TMDB responded successfully",
"TMDB_TEST_FAILED": "Check failed — TMDB rejected the key or is unreachable",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Clave de API de TMDB (opcional)",
"TMDB_API_KEY_DESCRIPTION": "Usa tu propia clave de API de TMDB en lugar de la integrada",
"TMDB_GET_KEY_HINT": "Obtén una clave de API gratuita en themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Reconocer películas en listas M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Las entradas de la lista que parecen archivos de películas se abren en la vista de detalles de la película con metadatos de TMDB en lugar del panel de EPG.",
"TMDB_TEST_BUTTON": "Comprobar clave",
"TMDB_TEST_SUCCESS": "La clave funciona — TMDB respondió correctamente",
"TMDB_TEST_FAILED": "La comprobación falló — TMDB rechazó la clave o no está disponible",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Clé API TMDB (optionnelle)",
"TMDB_API_KEY_DESCRIPTION": "Utiliser votre propre clé API TMDB au lieu de celle intégrée",
"TMDB_GET_KEY_HINT": "Obtenez une clé API gratuite sur themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Reconnaître les films dans les listes M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Les entrées de liste qui ressemblent à des fichiers de films s'ouvrent dans la vue détaillée du film avec les métadonnées TMDB au lieu du panneau EPG.",
"TMDB_TEST_BUTTON": "Vérifier la clé",
"TMDB_TEST_SUCCESS": "La clé fonctionne — TMDB a répondu avec succès",
"TMDB_TEST_FAILED": "Échec de la vérification — TMDB a rejeté la clé ou est injoignable",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API-kulcs (nem kötelező)",
"TMDB_API_KEY_DESCRIPTION": "A beépített helyett saját TMDB API-kulcs használata",
"TMDB_GET_KEY_HINT": "Ingyenes API-kulcs igénylése a themoviedb.org webhelyen",
"TMDB_M3U_VOD_DETAILS": "Filmek felismerése az M3U lejátszási listákban",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "A filmfájlnak tűnő listaelemek a film adatlapján nyílnak meg TMDB-metaadatokkal az EPG panel helyett.",
"TMDB_TEST_BUTTON": "Kulcs ellenőrzése",
"TMDB_TEST_SUCCESS": "A kulcs működik – a TMDB sikeresen válaszolt",
"TMDB_TEST_FAILED": "Az ellenőrzés sikertelen – a TMDB elutasította a kulcsot, vagy nem érhető el",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Chiave API TMDB (opzionale)",
"TMDB_API_KEY_DESCRIPTION": "Usa la tua chiave API TMDB al posto di quella integrata",
"TMDB_GET_KEY_HINT": "Ottieni una chiave API gratuita su themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Riconosci i film nelle playlist M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Le voci della playlist che sembrano file di film si aprono nella vista di dettaglio del film con i metadati TMDB al posto del pannello EPG.",
"TMDB_TEST_BUTTON": "Verifica chiave",
"TMDB_TEST_SUCCESS": "La chiave funziona — TMDB ha risposto correttamente",
"TMDB_TEST_FAILED": "Verifica non riuscita — TMDB ha rifiutato la chiave o non è raggiungibile",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API キー(任意)",
"TMDB_API_KEY_DESCRIPTION": "内蔵のキーの代わりに独自の TMDB API キーを使用します",
"TMDB_GET_KEY_HINT": "themoviedb.org で無料の API キーを取得",
"TMDB_M3U_VOD_DETAILS": "M3U プレイリスト内の映画を認識",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "映画ファイルと思われるプレイリスト項目は、EPG パネルの代わりに TMDB メタデータ付きの映画詳細画面で開きます。",
"TMDB_TEST_BUTTON": "キーを確認",
"TMDB_TEST_SUCCESS": "キーは有効です — TMDB が正常に応答しました",
"TMDB_TEST_FAILED": "確認に失敗しました — TMDB がキーを拒否したか、接続できません",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API 키 (선택 사항)",
"TMDB_API_KEY_DESCRIPTION": "기본 제공 키 대신 자신의 TMDB API 키를 사용합니다",
"TMDB_GET_KEY_HINT": "themoviedb.org에서 무료 API 키 받기",
"TMDB_M3U_VOD_DETAILS": "M3U 재생목록에서 영화 인식",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "영화 파일로 보이는 재생목록 항목은 EPG 패널 대신 TMDB 메타데이터가 포함된 영화 상세 보기로 열립니다.",
"TMDB_TEST_BUTTON": "키 확인",
"TMDB_TEST_SUCCESS": "키가 작동합니다 — TMDB가 정상적으로 응답했습니다",
"TMDB_TEST_FAILED": "확인에 실패했습니다 — TMDB가 키를 거부했거나 연결할 수 없습니다",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API-sleutel (optioneel)",
"TMDB_API_KEY_DESCRIPTION": "Gebruik je eigen TMDB API-sleutel in plaats van de ingebouwde",
"TMDB_GET_KEY_HINT": "Vraag een gratis API-sleutel aan op themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Films in M3U-afspeellijsten herkennen",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Afspeellijstitems die op filmbestanden lijken, openen in de filmdetailweergave met TMDB-metadata in plaats van het EPG-paneel.",
"TMDB_TEST_BUTTON": "Sleutel controleren",
"TMDB_TEST_SUCCESS": "De sleutel werkt — TMDB heeft succesvol gereageerd",
"TMDB_TEST_FAILED": "Controle mislukt — TMDB heeft de sleutel geweigerd of is niet bereikbaar",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Klucz API TMDB (opcjonalnie)",
"TMDB_API_KEY_DESCRIPTION": "Użyj własnego klucza API TMDB zamiast wbudowanego",
"TMDB_GET_KEY_HINT": "Uzyskaj bezpłatny klucz API na themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Rozpoznawaj filmy na playlistach M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Pozycje playlisty wyglądające jak pliki filmowe otwierają się w widoku szczegółów filmu z metadanymi TMDB zamiast panelu EPG.",
"TMDB_TEST_BUTTON": "Sprawdź klucz",
"TMDB_TEST_SUCCESS": "Klucz działa — TMDB odpowiedziało poprawnie",
"TMDB_TEST_FAILED": "Sprawdzenie nie powiodło się — TMDB odrzuciło klucz lub jest niedostępne",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "Chave de API do TMDB (opcional)",
"TMDB_API_KEY_DESCRIPTION": "Use sua própria chave de API do TMDB em vez da chave integrada",
"TMDB_GET_KEY_HINT": "Obtenha uma chave de API gratuita em themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Reconhecer filmes em playlists M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "As entradas da playlist que parecem ficheiros de filmes abrem na vista de detalhes do filme com metadados do TMDB em vez do painel de EPG.",
"TMDB_TEST_BUTTON": "Verificar chave",
"TMDB_TEST_SUCCESS": "A chave funciona — o TMDB respondeu com sucesso",
"TMDB_TEST_FAILED": "A verificação falhou — o TMDB rejeitou a chave ou está inacessível",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "API-ключ TMDB (необязательно)",
"TMDB_API_KEY_DESCRIPTION": "Использовать собственный API-ключ TMDB вместо встроенного",
"TMDB_GET_KEY_HINT": "Получите бесплатный API-ключ на themoviedb.org",
"TMDB_M3U_VOD_DETAILS": "Распознавать фильмы в плейлистах M3U",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Элементы плейлиста, похожие на файлы фильмов, открываются на странице деталей фильма с метаданными TMDB вместо панели телепрограммы.",
"TMDB_TEST_BUTTON": "Проверить ключ",
"TMDB_TEST_SUCCESS": "Ключ работает — TMDB успешно ответил",
"TMDB_TEST_FAILED": "Проверка не удалась — TMDB отклонил ключ или недоступен",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API anahtarı (isteğe bağlı)",
"TMDB_API_KEY_DESCRIPTION": "Yerleşik anahtar yerine kendi TMDB API anahtarınızı kullanın",
"TMDB_GET_KEY_HINT": "themoviedb.org üzerinden ücretsiz API anahtarı alın",
"TMDB_M3U_VOD_DETAILS": "M3U oynatma listelerindeki filmleri tanı",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "Film dosyasına benzeyen oynatma listesi öğeleri, EPG paneli yerine TMDB meta verileriyle film ayrıntı görünümünde açılır.",
"TMDB_TEST_BUTTON": "Anahtarı kontrol et",
"TMDB_TEST_SUCCESS": "Anahtar çalışıyor — TMDB başarıyla yanıt verdi",
"TMDB_TEST_FAILED": "Kontrol başarısız — TMDB anahtarı reddetti veya erişilemiyor",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API 密钥(可选)",
"TMDB_API_KEY_DESCRIPTION": "使用您自己的 TMDB API 密钥替代内置密钥",
"TMDB_GET_KEY_HINT": "在 themoviedb.org 获取免费的 API 密钥",
"TMDB_M3U_VOD_DETAILS": "识别 M3U 播放列表中的电影",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "疑似电影文件的播放列表条目会在包含 TMDB 元数据的电影详情视图中打开,而不是 EPG 面板。",
"TMDB_TEST_BUTTON": "检查密钥",
"TMDB_TEST_SUCCESS": "密钥有效 — TMDB 响应成功",
"TMDB_TEST_FAILED": "检查失败 — TMDB 拒绝了该密钥或无法访问",
+2
View File
@@ -264,6 +264,8 @@
"TMDB_API_KEY": "TMDB API 金鑰(選填)",
"TMDB_API_KEY_DESCRIPTION": "使用您自己的 TMDB API 金鑰,而非內建金鑰",
"TMDB_GET_KEY_HINT": "在 themoviedb.org 取得免費的 API 金鑰",
"TMDB_M3U_VOD_DETAILS": "辨識 M3U 播放清單中的電影",
"TMDB_M3U_VOD_DETAILS_DESCRIPTION": "看似電影檔案的播放清單項目會在包含 TMDB 中繼資料的電影詳細資訊頁面中開啟,而非 EPG 面板。",
"TMDB_TEST_BUTTON": "檢查金鑰",
"TMDB_TEST_SUCCESS": "金鑰有效 — TMDB 回應成功",
"TMDB_TEST_FAILED": "檢查失敗 — TMDB 拒絕該金鑰或無法連線",