Files
Nodecast/public/index.html
blomios dbd5b7fea3 fix: group beIN Sports 1 variants and clarify Laya language warning (#15)
beIN Sports 1 variants stayed separated after projection because
names with region prefixes (C Car, C Reu, Thai), time-shift suffixes
(2h/4h/8h/12h) and language markers (En, Fr, Thai) produced distinct
fingerprints. Extend normalization in the server projection service
and the frontend grouping fallback to fold these into one logical
channel, with tests on the exact reported names. Clarify the Laya
language detection warning so a language-matching failure is not
mistaken for a grouping failure, and bump script versions for cache
refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 16:22:57 +02:00

1239 lines
64 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<title>nodecast-tv | IPTV Player</title>
<meta name="description" content="Stream live TV, movies, and series with EPG support">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="css/main.css?v=8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@800&display=swap"
rel="stylesheet">
<!-- HLS.js for video playback -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js"></script>
</head>
<body>
<div id="app">
<!-- Navigation -->
<nav class="navbar">
<div class="navbar-brand">
<span class="logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 187.63 210.6" fill="#6366f1"
class="icon">
<path
d="M169.71,87.87c-4.77,0-9.1,1.88-12.31,4.92L35.3,22.3c.35-1.4.56-2.86.56-4.37C35.86,8.03,27.83,0,17.93,0S0,8.03,0,17.93c0,8.44,5.84,15.5,13.69,17.4v140c-7.71,2.01-13.41,8.99-13.41,17.33,0,9.9,8.03,17.93,17.93,17.93s17.93-8.03,17.93-17.93c0-1.56-.22-3.07-.6-4.51l121.21-69.98c3.26,3.42,7.86,5.55,12.95,5.55,9.9,0,17.93-8.03,17.93-17.93s-8.03-17.93-17.93-17.93ZM152.78,99.92c-.14.41-.27.83-.38,1.25h-51.47l-44.18-24.72L30.4,30.8c.33-.32.64-.65.94-.99l121.45,70.12ZM22.17,175.2V35.33c.23-.05.45-.11.67-.17l26.35,45.65-.34,50.04-25.74,44.59c-.31-.09-.63-.17-.95-.24ZM31.52,180.69c-.27-.3-.55-.6-.84-.89l25.48-44.13,44.64-25.78h51.47c.07.32.15.63.24.94L31.52,180.69Z" />
</svg></span>
<span class="brand-text">nodecast-tv</span>
<span id="version-badge" class="version-badge"></span>
</div>
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
<div class="navbar-menu" id="navbar-menu">
<!-- Now Playing indicator for movies/series -->
<a href="#" class="now-playing-indicator hidden" id="now-playing-indicator" data-page="watch">
<span class="now-playing-icon">▶</span>
<span class="now-playing-text" id="now-playing-text">Now Playing</span>
</a>
<a href="#" class="nav-link active" data-page="home">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" />
</svg></span>
<span>Home</span>
</a>
<a href="#" class="nav-link" data-page="live">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path
d="M21 6h-7.59l3.29-3.29L16 2l-4 4-4-4-.71.71L10.59 6H3a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8a2 2 0 0 0-2-2zm0 14H3V8h18v12zM9 10v8l7-4z" />
</svg></span>
<span>Live TV</span>
</a>
<a href="#" class="nav-link" data-page="guide">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z" />
</svg></span>
<span>TV Guide</span>
</a>
<a href="#" class="nav-link" data-page="movies">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path
d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2V4h-4z" />
</svg></span>
<span>Movies</span>
</a>
<a href="#" class="nav-link" data-page="series">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path
d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z" />
</svg></span>
<span>Series</span>
</a>
<a href="#" class="nav-link" data-page="settings">
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon">
<path
d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L3.16 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.04.64.09.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.58 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg></span>
<span>Settings</span>
</a>
</div>
</nav>
<!-- Main Content Area -->
<main class="main-content">
<!-- Home (Dashboard) Page -->
<div id="page-home" class="page active">
<!-- Content will be rendered by HomePage.js -->
<div class="loading-state">Loading Dashboard...</div>
</div>
<!-- Live TV Page (formerly Home) -->
<div id="page-live" class="page">
<div class="home-layout">
<!-- Channel Toggle Button (Mobile) -->
<button class="channel-toggle-btn" id="channel-toggle-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z" />
</svg>
<span>Channels</span>
</button>
<!-- Overlay for drawer -->
<div class="channel-sidebar-overlay" id="channel-sidebar-overlay"></div>
<!-- Expand button (visible when sidebar collapsed on desktop) -->
<button class="sidebar-expand-btn" id="sidebar-expand-btn" title="Expand sidebar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
<!-- Channel Sidebar -->
<aside class="channel-sidebar" id="channel-sidebar">
<div class="sidebar-header">
<div class="sidebar-header-row">
<div class="search-wrapper">
<input type="text" id="channel-search" placeholder="Search channels..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<button id="sidebar-collapse-btn" class="btn btn-sm btn-ghost sidebar-collapse-btn"
title="Collapse sidebar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg>
</button>
</div>
<div class="sidebar-controls">
<select id="source-select" class="source-select">
<option value="">All Sources</option>
</select>
<button id="toggle-groups" class="btn btn-sm btn-ghost" title="Expand/Collapse All">
<span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z" />
</svg></span>
</button>
</div>
</div>
<div id="channel-list" class="channel-list">
<div class="empty-state">
<p>No channels loaded</p>
<p class="hint">Add a source in Settings to get started</p>
</div>
</div>
</aside>
<!-- Video Player Area -->
<section class="player-section">
<div class="video-container" id="video-container">
<video id="video-player" playsinline></video>
<!-- Custom Controls Overlay -->
<div class="watch-overlay hidden" id="player-controls-overlay">
<!-- Top Bar -->
<div class="watch-top-bar live-header">
<h2 class="watch-title-overlay" id="player-channel-name"></h2>
<div class="player-badges">
<div class="transcode-status hidden" id="player-transcode-status">Direct</div>
<div class="quality-badge hidden" id="player-quality-badge"></div>
</div>
</div>
<!-- Center: Large Play Button -->
<button class="watch-center-play" id="player-center-play">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8 5v14l11-7z" />
</svg>
</button>
<!-- Loading Spinner -->
<div class="watch-loading" id="player-loading">
<div class="loading-spinner"></div>
</div>
<!-- Bottom Bar: Controls -->
<div class="watch-bottom-bar">
<div class="watch-controls">
<button class="watch-btn watch-btn-lg" id="btn-play" title="Play/Pause">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-play">
<path d="M8 5v14l11-7z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-pause hidden">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
</button>
<div class="watch-spacer"></div>
<button class="watch-btn" id="btn-mute" title="Mute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-vol">
<path
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-muted hidden">
<path
d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" />
</svg>
</button>
<input type="range" class="watch-volume" id="player-volume" min="0" max="100" value="80">
<!-- Captions Button -->
<div class="watch-captions-wrapper">
<button class="watch-btn" id="player-captions-btn" title="Captions">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z" />
</svg>
</button>
<div class="watch-captions-menu hidden" id="player-captions-menu">
<div class="captions-menu-title">Subtitles</div>
<div class="captions-menu-list" id="player-captions-list">
<button class="captions-option active" data-index="-1">Off</button>
</div>
</div>
</div>
<button class="watch-btn" id="btn-pip" title="Picture-in-Picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" />
</svg>
</button>
<button class="watch-btn" id="btn-fullscreen" title="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
</svg>
</button>
<!-- Overflow Menu -->
<div class="player-overflow-wrapper">
<button class="watch-btn" id="btn-overflow" title="More options">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
</button>
<div class="player-overflow-menu hidden" id="player-overflow-menu">
<button class="overflow-menu-item" id="btn-copy-url">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
Copy Stream URL
</button>
</div>
</div>
</div>
</div>
</div>
<div class="player-overlay" id="player-overlay">
<div class="overlay-content">
<p>Select a channel to start watching</p>
</div>
</div>
<!-- Now Playing Overlay -->
<div class="now-playing-overlay hidden" id="now-playing">
<div class="now-playing-info">
<h3 class="channel-name">No channel selected</h3>
<p class="program-title"></p>
<p class="program-time"></p>
</div>
<div class="up-next">
<h4>Up Next</h4>
<ul id="up-next-list"></ul>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Guide Page -->
<div id="page-guide" class="page">
<div class="guide-header">
<h2>TV Guide</h2>
<div class="guide-controls">
<select id="epg-group-select" class="source-select">
<option value="">All Groups</option>
</select>
<div class="search-wrapper">
<input type="text" id="epg-search" placeholder="Search channels..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<button id="guide-prev" class="btn btn-ghost">&larr; Earlier</button>
<span id="guide-date" class="guide-date">Today</span>
<button id="guide-next" class="btn btn-ghost">Later &rarr;</button>
</div>
</div>
<div id="epg-grid" class="epg-grid">
<div class="empty-state">
<p>No EPG data available</p>
<p class="hint">Add an EPG source in Settings</p>
</div>
</div>
</div>
<!-- Movies Page -->
<div id="page-movies" class="page">
<div class="movies-header">
<h2>Movies</h2>
<div class="movies-controls">
<select id="movies-source-select" class="source-select">
<option value="">All Sources</option>
</select>
<select id="movies-category-select" class="source-select">
<option value="">All Categories</option>
</select>
<div class="search-wrapper">
<input type="text" id="movies-search" placeholder="Search movies..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<button id="movies-favorites-btn" class="btn btn-icon btn-ghost" title="Show Favorites Only">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg> Favorites
</button>
</div>
</div>
<div id="movies-grid" class="movies-grid">
<div class="empty-state">
<p>Loading movies...</p>
</div>
</div>
</div>
<!-- Series Page -->
<div id="page-series" class="page">
<div class="series-header">
<h2>Series</h2>
<div class="series-controls">
<select id="series-source-select" class="source-select">
<option value="">All Sources</option>
</select>
<select id="series-category-select" class="source-select">
<option value="">All Categories</option>
</select>
<div class="search-wrapper">
<input type="text" id="series-search" placeholder="Search series..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<button id="series-favorites-btn" class="btn btn-icon btn-ghost" title="Show Favorites Only">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg> Favorites
</button>
</div>
</div>
<div class="series-content">
<div id="series-grid" class="series-grid">
<div class="empty-state">
<p>Loading series...</p>
</div>
</div>
<!-- Series details panel (seasons/episodes) -->
<div id="series-details" class="series-details hidden">
<button class="series-back-btn">← Back to Series</button>
<div class="series-details-header">
<img id="series-poster" src="" alt="" class="series-poster">
<div class="series-info">
<h3 id="series-title"></h3>
<p id="series-plot"></p>
</div>
</div>
<div id="series-seasons" class="series-seasons"></div>
</div>
</div>
</div>
<!-- Settings Page -->
<div id="page-settings" class="page">
<div class="settings-container">
<h2>Settings</h2>
<!-- Tabs -->
<div class="tabs">
<button class="tab active" data-tab="sources">Sources</button>
<button class="tab" data-tab="player">Player</button>
<button class="tab" data-tab="transcode">Transcoding</button>
<button class="tab" data-tab="content">Manage Content</button>
<button class="tab" data-tab="users" id="users-tab" style="display:none;">Users</button>
</div>
<!-- Sources Tab -->
<div id="tab-sources" class="tab-content active">
<div class="source-section">
<div class="section-header">
<h3>Xtream Connections</h3>
<button class="btn btn-primary" id="add-xtream">+ Add Xtream</button>
</div>
<div id="xtream-list" class="source-list"></div>
</div>
<div class="source-section">
<div class="section-header">
<h3>M3U Playlists</h3>
<button class="btn btn-primary" id="add-m3u">+ Add M3U</button>
</div>
<div id="m3u-list" class="source-list"></div>
</div>
<div class="source-section">
<div class="section-header">
<h3>EPG Sources</h3>
<button class="btn btn-primary" id="add-epg">+ Add EPG</button>
</div>
<div id="epg-list" class="source-list"></div>
</div>
<div class="source-section">
<h3>EPG Data Settings</h3>
<div class="setting-item">
<label for="epg-refresh-interval">Auto-refresh EPG data every:</label>
<select id="epg-refresh-interval" class="form-input">
<option value="1">1 hour</option>
<option value="2">2 hours</option>
<option value="4">4 hours</option>
<option value="12">12 hours</option>
<option value="24" selected>24 hours</option>
<option value="0">Manual only</option>
</select>
</div>
<div class="setting-item">
<span class="setting-label">Last Refreshed:</span>
<span id="epg-last-refreshed" class="hint">Never</span>
</div>
<p class="hint">Use the <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon" style="width: 1em; height: 1em; vertical-align: middle;">
<path
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
</svg> button next to EPG sources for manual refresh</p>
</div>
</div>
<!-- Player Tab -->
<div id="tab-player" class="tab-content">
<div class="settings-section">
<h3>Live TV</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-group-live-channels" checked>
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Group channels by category</span>
<span class="setting-hint">Turn off to show all channels in a single list. Stream variants remain grouped by channel.</span>
</div>
</div>
<div class="channel-matching-settings">
<h4>Channel matching</h4>
<p class="setting-hint">Choose how Nodecast TV identifies matching streams and languages.</p>
<div class="live-analysis-panel" id="live-analysis-panel">
<div class="live-analysis-heading">
<div>
<strong>Channel analysis</strong>
<div class="setting-hint">Group matching streams into channels and update detected languages.</div>
</div>
<button type="button" class="btn btn-primary" id="live-analysis-button">Analyze channels</button>
</div>
<p class="setting-hint">Use this to analyze channels that were added before automatic analysis was enabled.</p>
<div id="live-analysis-status" class="setting-hint" role="status">Checking status…</div>
<div id="live-analysis-progress" class="laya-progress" hidden>
<div id="live-analysis-progress-track" class="laya-progress-track" role="progressbar" aria-label="Channel analysis progress">
<span id="live-analysis-progress-bar" class="laya-progress-bar"></span>
</div>
<div id="live-analysis-progress-text" class="setting-hint"></div>
</div>
<div id="live-analysis-fallback" class="setting-hint"></div>
<button type="button" class="btn btn-ghost laya-details-toggle" id="live-analysis-details-toggle" aria-expanded="false" hidden>Show details</button>
<pre id="live-analysis-error-details" class="laya-error-details" hidden></pre>
</div>
<div class="matching-mode-options">
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="algo" checked>
<span><strong>Standard (Recommended)</strong><small>Use the built-in matching system. Fast and no download required.</small></span>
</label>
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="laya">
<span><strong>Local AI only</strong><small>Use the downloaded AI model instead of the built-in matching system.</small></span>
</label>
<label class="matching-mode-card">
<input type="radio" name="language-detection-mode" value="hybrid">
<span><strong>Enhanced</strong><small>Combine the local AI model with the built-in matching system for more evidence.</small></span>
</label>
</div>
<div class="laya-model-panel" id="laya-model-panel">
<div class="laya-model-heading">
<div><strong>Local AI · Laya</strong><div id="laya-model-status" class="setting-hint" role="status">Checking status…</div></div>
</div>
<div class="laya-component-status"><span id="laya-model-state">Model: Checking…</span><span id="laya-model-size" class="setting-hint"></span></div>
<div id="laya-service-state">Local AI service: Checking…</div>
<div id="laya-model-message" class="setting-hint"></div>
<div id="laya-download-progress" class="laya-progress" hidden>
<div id="laya-progress-track" class="laya-progress-track" role="progressbar" aria-label="Local AI model download progress">
<span id="laya-progress-bar" class="laya-progress-bar"></span>
</div>
<div id="laya-progress-text" class="setting-hint"></div>
</div>
<button type="button" class="btn btn-ghost laya-details-toggle" id="laya-show-details" aria-expanded="false" hidden>Show details</button>
<pre id="laya-error-details" class="laya-error-details" hidden></pre>
<div class="laya-model-actions">
<button type="button" class="btn btn-primary" id="laya-download-model">Download model</button>
<button type="button" class="btn btn-ghost" id="laya-remove-model">Remove downloaded model</button>
<button type="button" class="btn btn-ghost" id="laya-check-again" hidden>Check again</button>
<button type="button" class="btn btn-ghost" id="laya-copy-command" hidden>Copy start command</button>
<button type="button" class="btn btn-ghost" id="laya-use-standard" hidden>Use Standard</button>
</div>
<div id="laya-start-command" class="laya-start-command" hidden><code>docker compose --profile laya up -d</code></div>
<p class="setting-hint">The model runs on this server. Channel names and metadata are not sent to an AI service.</p>
</div>
</div>
</div>
<div class="settings-section">
<h3>Keyboard Controls</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-arrow-keys" checked>
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Arrow keys change channels</span>
<span class="setting-hint">When enabled, ↑/↓ change channels and ←/→ adjust volume</span>
</div>
</div>
</div>
<div class="settings-section">
<h3>Now Playing Overlay</h3>
<div class="setting-item">
<label for="setting-overlay-duration">Display Duration (seconds)</label>
<input type="number" id="setting-overlay-duration" class="form-input setting-number" min="1" max="30"
value="5">
</div>
</div>
<div class="settings-section">
<h3>Volume Settings</h3>
<div class="setting-item">
<label for="setting-default-volume">Default Volume</label>
<div class="volume-slider-container">
<input type="range" id="setting-default-volume" class="volume-slider" min="0" max="100" value="80">
<span id="volume-value">80%</span>
</div>
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-remember-volume" checked>
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Remember Last Volume</span>
<span class="setting-hint">When enabled, volume persists between sessions</span>
</div>
</div>
</div>
<div class="settings-section">
<h3>Playback</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-autoplay-next">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Auto-play Next Episode</span>
<span class="setting-hint">Automatically play the next episode when one ends</span>
</div>
</div>
</div>
<div class="settings-section">
<h3>Keyboard Shortcuts</h3>
<div class="shortcuts-grid">
<div class="shortcut"><kbd>↑</kbd> / <kbd>↓</kbd> Channel up/down (or volume)</div>
<div class="shortcut"><kbd>←</kbd> / <kbd>→</kbd> Volume down/up (when arrows = channels)</div>
<div class="shortcut"><kbd>Space</kbd> / <kbd>K</kbd> Play/Pause</div>
<div class="shortcut"><kbd>F</kbd> Fullscreen</div>
<div class="shortcut"><kbd>M</kbd> Mute/Unmute</div>
<div class="shortcut"><kbd>I</kbd> Toggle info overlay</div>
<div class="shortcut"><kbd>PgUp</kbd> / <kbd>PgDn</kbd> Channel up/down</div>
</div>
</div>
</div>
<!-- Transcoding Tab -->
<div id="tab-transcode" class="tab-content">
<div class="settings-section">
<h3>Detected Hardware</h3>
<div id="hw-info-container" class="hw-info-container">
<p class="hint">Loading hardware information...</p>
</div>
</div>
<div class="settings-section">
<h3>Encoder Settings</h3>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Transcoding Device</span>
<span class="setting-hint">Select which GPU encoder to use for transcoding. Auto will use the best
available.</span>
</div>
<select id="setting-hw-encoder" class="source-select" style="width: auto; min-width: 200px;">
<option value="auto">Auto (Recommended)</option>
<option value="nvenc">NVIDIA NVENC</option>
<option value="amf">AMD AMF</option>
<option value="qsv">Intel QuickSync</option>
<option value="vaapi">VAAPI (Linux)</option>
<option value="software">Software (CPU)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Maximum Transcoded Resolution</span>
<span class="setting-hint">Limit transcoded output resolution. Lower = faster, more
compatible.</span>
</div>
<select id="setting-max-resolution" class="source-select" style="width: auto; min-width: 200px;">
<option value="4k">4K (2160p)</option>
<option value="1080p" selected>1080p (Full HD)</option>
<option value="720p">720p (HD)</option>
<option value="480p">480p (SD)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Encoding Quality</span>
<span class="setting-hint">Higher quality uses more bandwidth and CPU/GPU.</span>
</div>
<select id="setting-quality" class="source-select" style="width: auto; min-width: 200px;">
<option value="high">High</option>
<option value="medium" selected>Medium</option>
<option value="low">Low (Fastest)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Audio Downmix</span>
<span class="setting-hint">How multi-channel audio (5.1/7.1) is mixed down to stereo for browser
playback.</span>
</div>
<select id="setting-audio-mix" class="source-select" style="width: auto; min-width: 200px;">
<option value="auto">Auto (Smart Copy/ITU)</option>
<option value="itu">ITU-R BS.775 (Balanced)</option>
<option value="night">Night Mode (Dialogue Boost)</option>
<option value="cinematic">Cinematic (Wide)</option>
<option value="passthrough">Passthrough (Copy)</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>Upscaling</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-upscale-enabled">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Upscale Low-Resolution Video</span>
<span class="setting-hint">Upscale lower-resolution content to a higher resolution during
transcoding.</span>
</div>
</div>
<div class="setting-item" id="upscale-method-container" style="display: none;">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Upscaling Method</span>
<span class="setting-hint">Hardware is faster; Software (Lanczos) is higher quality.</span>
</div>
<select id="setting-upscale-method" class="source-select" style="width: auto; min-width: 200px;">
<option value="hardware">Hardware (GPU)</option>
<option value="software">Software (Lanczos)</option>
</select>
</div>
<div class="setting-item" id="upscale-target-container" style="display: none;">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Target Resolution</span>
<span class="setting-hint">Target resolution for upscaled output.</span>
</div>
<select id="setting-upscale-target" class="source-select" style="width: auto; min-width: 200px;">
<option value="720p">720p (HD)</option>
<option value="1080p" selected>1080p (Full HD)</option>
<option value="4k">4K (2160p)</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>Automatic Playback</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-auto-transcode-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Choose Playback Method Automatically</span>
<span class="setting-hint">Automatically detect stream codecs and only transcode/remux when needed.
Adds ~1-3s probe delay on first play.</span>
</div>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Maximum Streaming Bandwidth</span>
<span class="setting-hint">The bandwidth limit is evaluated when playback starts. Quality does not change automatically during playback.</span>
</div>
<select id="setting-max-direct-bitrate" class="source-select" style="width: auto; min-width: 200px;">
<option value="0">No Limit</option>
<option value="20000">20 Mbps</option>
<option value="12000">12 Mbps</option>
<option value="8000">8 Mbps</option>
<option value="4000">4 Mbps</option>
<option value="2000">2 Mbps</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>Stream Processing</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-transcode-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Always Convert Audio</span>
<span class="setting-hint">Transcode audio to AAC for browser compatibility. Enable if you get video
but no audio (fixes Dolby/AC3/EAC3).</span>
</div>
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-video-transcode-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Always Convert Video</span>
<span class="setting-hint">Force full video transcoding (burn-in subtitles, incompatible video).
Uses
HW encoder.</span>
</div>
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-remux-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Always Remux Streams</span>
<span class="setting-hint">Remux streams to MP4 container. Enable for raw .ts streams from IPTV
middleware like m3u-editor or dispatcharr.</span>
</div>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Stream Container</span>
<span class="setting-hint">Container format for Xtream streams. Try TS if you experience buffering
issues.</span>
</div>
<select id="setting-stream-format-tc" class="source-select" style="width: auto; min-width: 200px;">
<option value="m3u8">HLS (m3u8) - Recommended</option>
<option value="ts">MPEG-TS (ts) - Better buffer</option>
</select>
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-allow-hardware-fallback">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Use CPU if hardware transcoding fails</span>
</div>
</div>
</div>
<div class="settings-section">
<h3>HTTP Settings</h3>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">User-Agent</span>
<span class="setting-hint">HTTP User-Agent for stream requests. Try VLC if your provider blocks
Chrome.</span>
</div>
<select id="setting-user-agent-tc" class="source-select" style="width: auto; min-width: 200px;">
<option value="chrome">Chrome (Default)</option>
<option value="vlc">VLC</option>
<option value="tivimate">TiviMate</option>
<option value="custom">Custom...</option>
</select>
</div>
<div class="setting-item" id="custom-user-agent-container-tc" style="display: none;">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Custom User-Agent</span>
<span class="setting-hint">Enter your own User-Agent string.</span>
</div>
<input type="text" id="setting-user-agent-custom-tc" class="search-input" style="width: 300px;"
placeholder="Custom User-Agent string...">
</div>
</div>
<div class="settings-section">
<h3>Network</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-proxy-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Force Backend Proxy</span>
<span class="setting-hint">Route all streams through the server to bypass CORS restrictions. Enable if
streams don't play directly.</span>
</div>
</div>
</div>
</div>
<!-- Manage Content Tab -->
<div id="tab-content" class="tab-content">
<div class="content-browser">
<div class="content-browser-header">
<div class="content-type-toggle">
<button class="btn btn-sm active" id="content-type-channels">Channels</button>
<button class="btn btn-sm" id="content-type-movies">Movies</button>
<button class="btn btn-sm" id="content-type-series">Series</button>
</div>
<select id="content-source-select" class="source-select">
<option value="">Select a source...</option>
</select>
<div class="search-wrapper">
<input type="text" id="content-search" placeholder="Search groups/channels..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<div class="content-actions">
<button class="btn btn-sm btn-secondary" id="content-show-all">Show All</button>
<button class="btn btn-sm btn-secondary" id="content-hide-all">Hide All</button>
<button class="btn btn-sm btn-primary" id="content-save"><svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" />
</svg> Save Changes</button>
</div>
</div>
<p class="hint" style="margin-bottom: var(--space-sm);">✓ Checked = Visible, ☐ Unchecked = Hidden</p>
<div id="content-tree" class="content-tree">
<p class="hint">Select a source to view groups and channels</p>
</div>
</div>
</div>
<!-- Users Tab (Admin Only) -->
<div id="tab-users" class="tab-content">
<div class="settings-section">
<h3>User Management</h3>
<p class="hint">Manage user accounts and permissions</p>
<div class="user-list-container">
<table class="user-table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="user-list">
<tr>
<td colspan="4" class="hint">Loading users...</td>
</tr>
</tbody>
</table>
</div>
<div class="add-user-section">
<h4>Add New User</h4>
<form id="add-user-form" class="user-form">
<div class="form-group">
<label for="new-username">Username</label>
<input type="text" id="new-username" name="username" class="form-input" required>
</div>
<div class="form-group">
<label for="new-password">Password</label>
<input type="password" id="new-password" name="password" class="form-input" required minlength="6">
</div>
<div class="form-group">
<label for="new-role">Role</label>
<select id="new-role" name="role" class="form-input" required>
<option value="viewer">Viewer</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add User</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Watch Page (VOD Player) - Not in navigation -->
<div id="page-watch" class="page watch-page">
<!-- Video Section (100vh) -->
<section class="watch-video-section">
<video id="watch-video" playsinline></video>
<!-- Player Controls Overlay -->
<div class="watch-overlay" id="watch-overlay">
<!-- Top Bar: Back + Title -->
<div class="watch-top-bar vod-header">
<button class="watch-back-btn" id="watch-back-btn" title="Back">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</button>
<div class="watch-title-info">
<h3 class="watch-title" id="watch-title"></h3>
<p class="watch-subtitle" id="watch-subtitle"></p>
</div>
<div class="player-badges">
<div class="transcode-status hidden" id="watch-transcode-status">Direct</div>
<div class="quality-badge hidden" id="watch-quality-badge"></div>
</div>
</div>
<!-- Center: Large Play Button -->
<button class="watch-center-play" id="watch-center-play">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8 5v14l11-7z" />
</svg>
</button>
<!-- Loading Spinner -->
<div class="watch-loading" id="watch-loading">
<div class="loading-spinner"></div>
</div>
<!-- Bottom Bar: Controls -->
<div class="watch-bottom-bar">
<div class="watch-progress-container">
<span id="watch-time-current" class="watch-time">0:00</span>
<input type="range" class="watch-progress" id="watch-progress" min="0" max="100" value="0" step="0.1">
</div>
<div class="watch-controls">
<button class="watch-btn" id="watch-skip-back" title="Back 10 seconds">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" />
</svg>
<span class="skip-label">10</span>
</button>
<button class="watch-btn watch-btn-lg" id="watch-play-pause" title="Play/Pause">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-play">
<path d="M8 5v14l11-7z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-pause hidden">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
</button>
<button class="watch-btn" id="watch-skip-fwd" title="Forward 10 seconds">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
</svg>
<span class="skip-label">10</span>
</button>
<div class="watch-spacer"></div>
<button class="watch-btn" id="watch-mute" title="Mute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon icon-vol">
<path
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-muted hidden">
<path
d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" />
</svg>
</button>
<input type="range" class="watch-volume" id="watch-volume" min="0" max="100" value="80">
<!-- Captions Button -->
<div class="watch-captions-wrapper">
<button class="watch-btn" id="watch-captions-btn" title="Captions">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z" />
</svg>
</button>
<div class="watch-captions-menu hidden" id="watch-captions-menu">
<div class="captions-menu-title">Subtitles</div>
<div class="captions-menu-list" id="watch-captions-list">
<button class="captions-option active" data-index="-1">Off</button>
</div>
</div>
</div>
<button class="watch-btn" id="watch-pip" title="Picture-in-Picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" />
</svg>
</button>
<button class="watch-btn" id="watch-fullscreen" title="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
</svg>
</button>
<!-- Overflow Menu -->
<div class="player-overflow-wrapper">
<button class="watch-btn" id="watch-overflow" title="More options">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
</button>
<div class="player-overflow-menu hidden" id="watch-overflow-menu">
<button class="overflow-menu-item" id="watch-copy-url">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" />
</svg>
Copy Stream URL
</button>
</div>
</div>
</div>
</div>
<!-- Scroll Indicator -->
<div class="watch-scroll-hint" id="watch-scroll-hint">
<span>Scroll for details</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
</svg>
</div>
</div>
<!-- Next Episode Panel -->
<div class="watch-next-episode hidden" id="watch-next-episode">
<div class="next-info">
<p class="next-label">Up Next</p>
<h4 id="next-episode-title"></h4>
</div>
<span class="next-countdown" id="next-countdown">10</span>
<button class="btn btn-primary btn-sm" id="next-play-now">Play Now</button>
<button class="btn btn-ghost btn-sm" id="next-cancel">Cancel</button>
</div>
</section>
<!-- Content Details Section (scrollable) -->
<section class="watch-details-section" id="watch-details">
<!-- Content Info -->
<div class="watch-content-info">
<img class="watch-poster" id="watch-poster" src="" alt="">
<div class="watch-meta">
<h2 id="watch-content-title"></h2>
<div class="watch-meta-row">
<span class="watch-year" id="watch-year"></span>
<span class="watch-rating" id="watch-rating"></span>
<span class="watch-duration" id="watch-duration"></span>
</div>
<p class="watch-description" id="watch-description"></p>
<div class="watch-actions">
<button class="btn btn-primary" id="watch-play-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8 5v14l11-7z" />
</svg>
<span id="watch-play-btn-text">Play</span>
</button>
<button class="btn btn-ghost" id="watch-favorite-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-fav-outline">
<path
d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="icon icon-fav-filled hidden">
<path
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
</svg>
<span>Favorite</span>
</button>
</div>
</div>
</div>
<!-- Movies: Recommended Section -->
<div class="watch-recommended hidden" id="watch-recommended">
<h3>More Like This</h3>
<div class="watch-recommended-grid" id="watch-recommended-grid">
<!-- Movie cards inserted dynamically -->
</div>
</div>
<!-- Series: Seasons & Episodes -->
<div class="watch-episodes hidden" id="watch-episodes">
<h3>Episodes</h3>
<div class="watch-seasons" id="watch-seasons">
<!-- Season accordions inserted dynamically -->
</div>
</div>
</section>
</div>
</main>
</div>
<!-- Modal Template -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Modal</h3>
<button class="modal-close">&times;</button>
</div>
<div id="modal-body" class="modal-body"></div>
<div id="modal-footer" class="modal-footer"></div>
</div>
</div>
<!-- Context Menu Template -->
<div id="context-menu" class="context-menu">
<button class="context-item" data-action="play"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor" class="icon">
<path d="M8 5v14l11-7z" />
</svg> Play</button>
<button class="context-item" data-action="hide"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor" class="icon">
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-4.01.7l2.16 2.16C10.85 7.18 11.4 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg> Hide</button>
</div>
<!-- Edit User Modal -->
<div class="modal-overlay" id="edit-user-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Edit User</h3>
<button class="modal-close" id="edit-user-close">&times;</button>
</div>
<div class="modal-body">
<form id="edit-user-form">
<input type="hidden" id="edit-user-id">
<div class="modal-form-group">
<label for="edit-username">Username</label>
<input type="text" id="edit-username" name="username" class="form-input" required>
</div>
<div class="modal-form-group">
<label for="edit-email">Email</label>
<input type="text" id="edit-email" class="form-input" readonly disabled placeholder="No email associated">
</div>
<div class="modal-form-group">
<label for="edit-role">Role</label>
<select id="edit-role" name="role" class="form-input" required>
<option value="viewer">Viewer</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="modal-form-group">
<label for="edit-password">New Password</label>
<input type="password" id="edit-password" name="password" class="form-input"
placeholder="Leave blank to keep current">
<p class="hint" id="edit-password-hint" style="margin-top: 4px;">Optional. Leave blank to keep unchanged.
</p>
</div>
<div class="modal-form-group hidden" id="oidc-info-group">
<label>OIDC Provider ID</label>
<code id="edit-oidc-id"
style="font-size: 0.8rem; color: var(--color-text-muted); padding: 4px; background: rgba(0,0,0,0.3); border-radius: 4px; display: block;"></code>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" id="edit-user-cancel">Cancel</button>
<button class="btn btn-primary" id="edit-user-save">Save Changes</button>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="/js/icons.js"></script>
<script src="/js/api.js?v=4"></script>
<script src="/js/components/VideoPlayer.js?v=5"></script>
<script src="/js/services/channelLanguageDetection.js?v=1"></script>
<script src="/js/services/channelLanguageFusion.js?v=1"></script>
<script src="/js/services/layaStatusPresentation.js?v=5"></script>
<script src="/js/services/liveAnalysisPresentation.js?v=1"></script>
<script src="/js/services/liveChannelProjection.js?v=1"></script>
<script src="/js/services/channelVariantGrouping.js?v=7"></script>
<script src="/js/components/ChannelList.js?v=11"></script>
<script src="/js/components/SourceManager.js?v=2"></script>
<script src="/js/components/EpgGuide.js?v=2"></script>
<script src="/js/pages/HomePage.js?v=2"></script>
<script src="/js/pages/LivePage.js?v=2"></script>
<script src="/js/pages/Guide.js?v=2"></script>
<script src="/js/pages/MoviesPage.js?v=4"></script>
<script src="/js/pages/SeriesPage.js?v=1"></script>
<script src="/js/pages/Settings.js?v=8"></script>
<script src="/js/pages/WatchPage.js?v=1"></script>
<script src="/js/app.js?v=4"></script>
<script>
// Check for SSO token in URL
(function () {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
console.log('SSO Login successful, saving token...');
localStorage.setItem('authToken', token);
// Clean URL without reloading
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
})();
</script>
</body>
</html>