import { useState, useEffect, useRef, useCallback } from "react"; import Hls from "hls.js"; import { invoke } from "@tauri-apps/api/core"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { openUrl } from "@tauri-apps/plugin-opener"; import type { CinemaState, CinemaConfig, CinemaMediaItem, CinemaSeason, CinemaEpisode, CinemaPlayback, UserDto, } from "../../types"; import "./Cinema.css"; interface CinemaProps { currentUser: UserDto; onNavigateTab?: (tab: string) => void; onLogout?: () => void; isTvEnvironment?: boolean; } const buildAnimeSeasons = (seasonConfigs: { season_number: number; count: number }[], thumb: string): CinemaSeason[] => { return seasonConfigs.map((cfg) => ({ season_number: cfg.season_number, episodes: Array.from({ length: cfg.count }, (_, i) => ({ id: `demonic-s${cfg.season_number}-e${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: thumb, duration_minutes: 24, })), })); }; const formatCurrency = (amount?: number): string | null => { if (!amount || amount <= 0) return null; if (amount >= 1_000_000_000) { return `$${(amount / 1_000_000_000).toFixed(2)} млрд ($${amount.toLocaleString("ru-RU")})`; } if (amount >= 1_000_000) { return `$${(amount / 1_000_000).toFixed(1)} млн ($${amount.toLocaleString("ru-RU")})`; } return `$${amount.toLocaleString("ru-RU")}`; }; const formatRuntime = (minutes?: number): string | null => { if (!minutes || minutes <= 0) return null; const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours > 0 && mins > 0) return `${hours} ч ${mins} мин`; if (hours > 0) return `${hours} ч`; return `${mins} мин`; }; // Accurate media category label (never confuses live-action series with anime) const getMediaCategoryLabel = (media: CinemaMediaItem | null | undefined): string => { if (!media) return "Сериал"; const g = (media.genre || "").toLowerCase(); const isAnime = g.includes("аниме") || g.includes("anime"); const isCartoon = g.includes("мультфильм") || g.includes("анимация") || g.includes("animation"); if (media.media_type === "movie") { if (isAnime) return "Аниме-фильм"; if (isCartoon) return "Мультфильм"; return "Фильм"; } // TV / Series if (isAnime) return "Аниме-сериал"; if (isCartoon) return "Мультсериал"; return "Сериал"; }; const getMediaBadgeLabel = (media: CinemaMediaItem | null | undefined): string => { return getMediaCategoryLabel(media).toUpperCase(); }; // Initial Curated Fallback Library const INITIAL_LIBRARY: CinemaMediaItem[] = [ { id: "demonic-slash", tmdb_id: 85937, media_type: "tv", title: "Клинок, рассекающий демонов", original_title: "Kimetsu no Yaiba", backdrop_url: "https://images.unsplash.com/photo-1578632767115-351597cf2477?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?q=80&w=600&auto=format&fit=crop", release_year: 2024, seasons_count: 4, genre: "Аниме / Сёнэн", rating: 4.9, tagline: "Разруби свою судьбу клинком", countries: "Япония", director: "Харуо Сотодзаки", cast: [ { id: 21, name: "Нацуки Ханаэ", character: "Танджиро Камадо" }, { id: 22, name: "Акари Кито", character: "Незуко Камадо" }, { id: 23, name: "Хиро Симоно", character: "Зеницу Агацума" }, { id: 24, name: "Ёсицугу Мацуока", character: "Иноске Хашибира" }, ], synopsis: "Танджиро Камадо становится истребителем демонов, чтобы спасти свою сестру Незуко, превращенную в демона, и отомстить за гибель своей семьи.", is_original: true, seasons: buildAnimeSeasons( [ { season_number: 1, count: 26 }, { season_number: 2, count: 11 }, { season_number: 3, count: 11 }, { season_number: 4, count: 8 }, ], "https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?q=80&w=600&auto=format&fit=crop" ), }, { id: "dune-part-two", tmdb_id: 693134, media_type: "movie", title: "Дюна: Часть вторая", original_title: "Dune: Part Two", backdrop_url: "https://images.unsplash.com/photo-1509198397868-475647b2a1e5?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", release_year: 2024, duration_minutes: 166, genre: "Фантастика / Приключения", rating: 4.9, tagline: "Да пребудет с вами сила пустыни", budget: 190000000, revenue: 711844358, countries: "США, Канада", director: "Дени Вильнёв", cast: [ { id: 1, name: "Тимоти Шаламе", character: "Пол Атрейдес" }, { id: 2, name: "Зендея", character: "Чани" }, { id: 3, name: "Ребекка Фергюсон", character: "Леди Джессика" }, { id: 4, name: "Хавьер Бардем", character: "Стилгар" }, { id: 5, name: "Остин Батлер", character: "Фейд-Раута" }, { id: 6, name: "Флоренс Пью", character: "Принцесса Ирулан" }, ], synopsis: "Герцог Пол Атрейдес объединяется с фременами, чтобы отомстить заговорщикам, уничтожившим его семью. Эпическое продолжение борьбы за Арракис.", is_original: false, seasons: [], }, { id: "interstellar", tmdb_id: 157336, media_type: "movie", title: "Интерстеллар", original_title: "Interstellar", backdrop_url: "https://images.unsplash.com/photo-1506703719100-a0f3a48c0f86?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?q=80&w=600&auto=format&fit=crop", release_year: 2014, duration_minutes: 169, genre: "Фантастика / Драма", rating: 4.9, tagline: "Следующий шаг человечества станет величайшим", budget: 165000000, revenue: 701729206, countries: "США, Великобритания", director: "Кристофер Нолан", cast: [ { id: 11, name: "Мэттью Макконахи", character: "Купер" }, { id: 12, name: "Энн Хэтэуэй", character: "Бренд" }, { id: 13, name: "Джессика Честейн", character: "Мёрф" }, { id: 14, name: "Майкл Кейн", character: "Профессор Бренд" }, { id: 15, name: "Мэтт Дэймон", character: "Манн" }, ], synopsis: "Когда засуха приводит человечество к глобальному продовольственному кризису, группа ученых отправляется сквозь червоточину в поисках нового дома для людей.", is_original: false, seasons: [], }, { id: "arcane", tmdb_id: 94605, media_type: "tv", title: "Аркейн", original_title: "Arcane: League of Legends", backdrop_url: "https://images.unsplash.com/photo-1563089145-599997674d42?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1579783902614-a3fb3927b675?q=80&w=600&auto=format&fit=crop", release_year: 2024, seasons_count: 2, genre: "Анимация / Экшен", rating: 4.9, tagline: "Каждая легенда с чего-то начинается", countries: "США, Франция", director: "Паскаль Шаррю, Арно Делорд", cast: [ { id: 31, name: "Хейли Стайнфелд", character: "Вай" }, { id: 32, name: "Элла Пернелл", character: "Джинкс" }, { id: 33, name: "Кэти Льюнг", character: "Кейтлин" }, ], synopsis: "История двух сестер, Джинкс и Вай, оказавшихся по разные стороны баррикад в войне между технологическим Пилтовером и преступным Зауном.", is_original: false, seasons: [ { season_number: 1, episodes: Array.from({ length: 9 }, (_, i) => ({ id: `arcane-1-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1563089145-599997674d42?q=80&w=600&auto=format&fit=crop", duration_minutes: 41, })), }, { season_number: 2, episodes: Array.from({ length: 9 }, (_, i) => ({ id: `arcane-2-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1563089145-599997674d42?q=80&w=600&auto=format&fit=crop", duration_minutes: 42, })), }, ], }, { id: "breaking-bad", tmdb_id: 1396, media_type: "tv", title: "Во все тяжкие", original_title: "Breaking Bad", backdrop_url: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1509198397868-475647b2a1e5?q=80&w=600&auto=format&fit=crop", release_year: 2013, seasons_count: 5, genre: "Криминал / Драма", rating: 5.0, synopsis: "Школьный учитель химии Уолтер Уайт узнает о смертельном диагнозе и вместе с бывшим учеником начинает варить метамфетамин, чтобы обеспечить семью.", is_original: false, seasons: [ { season_number: 1, episodes: Array.from({ length: 7 }, (_, i) => ({ id: `bb-1-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", duration_minutes: 48, })), }, { season_number: 2, episodes: Array.from({ length: 13 }, (_, i) => ({ id: `bb-2-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", duration_minutes: 47, })), }, { season_number: 3, episodes: Array.from({ length: 13 }, (_, i) => ({ id: `bb-3-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", duration_minutes: 47, })), }, { season_number: 4, episodes: Array.from({ length: 13 }, (_, i) => ({ id: `bb-4-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", duration_minutes: 47, })), }, { season_number: 5, episodes: Array.from({ length: 16 }, (_, i) => ({ id: `bb-5-${i + 1}`, number: i + 1, title: `Серия ${i + 1}`, thumbnail: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", duration_minutes: 47, })), }, ], }, { id: "oppenheimer", tmdb_id: 872585, media_type: "movie", title: "Оппенгеймер", original_title: "Oppenheimer", backdrop_url: "https://images.unsplash.com/photo-1509198397868-475647b2a1e5?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=600&auto=format&fit=crop", release_year: 2023, duration_minutes: 180, genre: "Драма / История", rating: 4.9, synopsis: "История американского физика-теоретика Роберта Оппенгеймера, руководителя Манхэттенского проекта во время Второй мировой войны.", is_original: false, seasons: [], }, { id: "jujutsu-kaisen", tmdb_id: 95479, media_type: "tv", title: "Магическая битва", original_title: "Jujutsu Kaisen", backdrop_url: "https://images.unsplash.com/photo-1578632767115-351597cf2477?q=80&w=1600&auto=format&fit=crop", poster_url: "https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?q=80&w=600&auto=format&fit=crop", release_year: 2020, seasons_count: 2, genre: "Аниме / Экшен", rating: 4.9, synopsis: "Юдзи Итадори проглатывает палец могущественного демона Рёмэна Сукуны и вступает в Токийский магический колледж.", is_original: true, seasons: buildAnimeSeasons( [ { season_number: 1, count: 24 }, { season_number: 2, count: 23 }, ], "https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?q=80&w=600&auto=format&fit=crop" ), }, ]; interface RawTmdbItem { id: number; title?: string; name?: string; original_name?: string; original_title?: string; media_type?: string; poster_path?: string; backdrop_path?: string; overview?: string; release_date?: string; first_air_date?: string; vote_average?: number; vote_count?: number; genre_ids?: number[]; number_of_seasons?: number; status?: string; } // Filter out titles that have not yet been released or are unstreamable announcements const isMediaReleased = (item: RawTmdbItem): boolean => { const today = new Date().toISOString().slice(0, 10); const dateStr = item.release_date || item.first_air_date; // 1. If release date is in the future, it is not yet released if (dateStr && dateStr > today) { return false; } // 2. If it has no release date at all or invalid year if (!dateStr || dateStr.trim().length < 4) { return false; } // 3. Status checks: upcoming or unproduced titles if ( item.status && (item.status === "Planned" || item.status === "In Production" || item.status === "Post Production" || item.status === "Rumored") ) { return false; } // 4. Low-vote / announcement filter: titles with < 20 votes on TMDB // are almost exclusively unreleased festival announcements or dead unstreamable metadata if (item.vote_count !== undefined && item.vote_count < 20) { return false; } // 5. Very low rating with low votes filter if (item.vote_average !== undefined && item.vote_average < 3.5 && (item.vote_count || 0) < 50) { return false; } return true; }; interface KodikItem { id: string; title: string; title_orig?: string; link: string; translation: { id: number; title: string; type: string; }; } // Known Kinopoisk IDs for top popular titles (instant 0ms resolution for Russian dubbing) const KNOWN_KP_MAP: Record = { 157336: 258687, // Интерстеллар 693134: 4465403, // Дюна: Часть вторая 438631: 409424, // Дюна 94605: 1318855, // Аркейн 1396: 404900, // Во все тяжкие 85937: 1112136, // Клинок, рассекающий демонов 872585: 4664634, // Оппенгеймер 155: 111543, // Тёмный рыцарь 27205: 447301, // Начало 550: 361, // Бойцовский клуб 680: 342, // Криминальное чтиво 1399: 464963, // Игра престолов 60625: 685246, // Рик и Морти 1429: 749374, // Атака титанов 95479: 1227958, // Магическая битва 106379: 1399708, // Фоллаут 76479: 949804, // Пацаны 100088: 1354148, // Одни из нас 66732: 915196, // Очень странные дела 119051: 1373510, // Уэнсдей 87108: 1227803, // Чернобыль 126308: 1219417, // Сёгун 93405: 1309570, // Игра в кальмара 299534: 843650, // Мстители: Финал 299536: 843649, // Мстители: Война бесконечности 24428: 263531, // Мстители 634649: 1309599, // Человек-паук: Нет пути домой 19995: 251733, // Аватар 76600: 505898, // Аватар: Путь воды 597: 2213, // Титаник 120: 328, // Властелин колец: Братство Кольца 121: 312, // Властелин колец: Две крепости 122: 3498, // Властелин колец: Возвращение Короля 671: 689, // Гарри Поттер и философский камень 672: 688, // Гарри Поттер и Тайная комната 673: 324, // Гарри Поттер и узник Азкабана 674: 8408, // Гарри Поттер и Кубок огня 675: 48356, // Гарри Поттер и Орден Феникса 767: 86621, // Гарри Поттер и Принц-полукровка 12444: 276762, // Гарри Поттер и Дары Смерти: Часть 1 12445: 407636, // Гарри Поттер и Дары Смерти: Часть 2 389: 349, // 12 разгневанных мужчин 278: 326, // Побег из Шоушенка 238: 325, // Крёстный отец 424: 329, // Список Шиндлера 13: 448, // Форрест Гамп 129: 370, // Унесённые призраками 496243: 1043758, // Паразиты 493529: 1009436, // Джон Уик 3 603692: 1267348, // Джон Уик 4 245891: 762738, // Джон Уик 324552: 885658, // Джон Уик 2 533535: 1318973, // Дэдпул и Росомаха 293660: 462360, // Дэдпул 383498: 977288, // Дэдпул 2 }; interface RussianMediaInfo { kp_id?: number | null; imdb_id?: string | null; iframe_url?: string | null; russian_title?: string | null; } type BalancerMirror = | "smotrim_ru" | "embess_ru" | "alloha_ru" | "kodik_ru" | "anilibria" | "vidlink_pro" | "vidsrc_to" | "autoembed"; // Reliable image URL helper with proxy caching (bypasses Russian ISP / Cloudflare blocks on image.tmdb.org) const formatMediaImg = (path: string | undefined, size: "w500" | "original" = "w500"): string => { if (!path) return size === "original" ? INITIAL_LIBRARY[0].backdrop_url : INITIAL_LIBRARY[0].poster_url; if (path.startsWith("http")) return path; return `https://wsrv.nl/?url=https://image.tmdb.org/t/p/${size}${path}`; }; // Detect Android TV / Smart TV user agent or previous user preference const detectTvEnvironment = (): boolean => { if (typeof window === "undefined") return false; const saved = localStorage.getItem("has_tv_mode"); if (saved !== null) return saved === "true"; const ua = (navigator.userAgent || "").toLowerCase(); const isTvUa = ua.includes("android tv") || ua.includes("smarttv") || ua.includes("googletv") || ua.includes("tizen") || ua.includes("webos") || ua.includes("appletv") || ua.includes("crkey") || ua.includes("bravia") || ua.includes("aftt") || ua.includes("aftm") || ua.includes("mibox") || ua.includes("mi box") || ua.includes("shield") || ua.includes("dtv") || ua.includes("smart-tv") || ua.includes("hbbtv") || ua.includes("netcast") || ua.includes("viera") || ua.includes("roku") || ua.includes("firetv"); if (isTvUa) return true; const isAndroid = ua.includes("android"); const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0; if (isAndroid && noTouch) return true; return false; }; export default function Cinema({ currentUser, onNavigateTab, onLogout, isTvEnvironment = false, }: CinemaProps) { const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; // Android TV / Remote Control (10-Foot UI) State const [isTvMode, setIsTvMode] = useState(() => { return isTvEnvironment || detectTvEnvironment(); }); const [isTvNavOpen, setIsTvNavOpen] = useState(false); const lastFocusedCardRef = useRef(null); useEffect(() => { if (isTvEnvironment && !isTvMode) { setIsTvMode(true); } }, [isTvEnvironment, isTvMode]); const [tvToast, setTvToast] = useState<{ text: string; icon?: string } | null>(null); const tvToastTimerRef = useRef(null); const audioCtxRef = useRef(null); const showTvToast = useCallback((text: string, icon = "📺", duration = 2000) => { if (tvToastTimerRef.current) clearTimeout(tvToastTimerRef.current); setTvToast({ text, icon }); tvToastTimerRef.current = setTimeout(() => { setTvToast(null); }, duration); }, []); const playTvBeep = useCallback((freq = 580, duration = 0.035) => { try { const AudioCtx = window.AudioContext || (window as any).webkitAudioContext; if (!AudioCtx) return; if (!audioCtxRef.current) { audioCtxRef.current = new AudioCtx(); } const ctx = audioCtxRef.current; if (ctx.state === "suspended") { ctx.resume().catch(() => {}); } const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; osc.frequency.setValueAtTime(freq, ctx.currentTime); gain.gain.setValueAtTime(0.04, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + duration); } catch { // AudioContext might be blocked until user gesture, ignore safely } }, []); const toggleTvMode = useCallback(() => { setIsTvMode((prev) => { const next = !prev; localStorage.setItem("has_tv_mode", String(next)); showTvToast(next ? "ТВ-Режим включен (10-Foot UI)" : "ПК-Режим включен", "📺", 2500); playTvBeep(next ? 750 : 400, 0.05); return next; }); }, [showTvToast, playTvBeep]); // Local Cinema State (Bookmarks & Progress saved on user's machine) const [cinemaState, setCinemaState] = useState({ bookmarks: [], history: [], }); const [, setConfig] = useState({ tmdb_api_key: null }); // Navigation Filter const [activeTab, setActiveTab] = useState<"home" | "movies" | "series" | "anime" | "bookmarks">("home"); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); // Active Selected Hero Item const [selectedMedia, setSelectedMedia] = useState(INITIAL_LIBRARY[0]); const [selectedSeason, setSelectedSeason] = useState(1); // Dynamic Content Collections from TMDB const [trendingAll, setTrendingAll] = useState([]); const [popularMovies, setPopularMovies] = useState([]); const [popularSeries, setPopularSeries] = useState([]); const [popularAnime, setPopularAnime] = useState([]); // Video Player Modal State const [playingMedia, setPlayingMedia] = useState(null); const [playingEpisode, setPlayingEpisode] = useState(null); const [playingSeasonNumber, setPlayingSeasonNumber] = useState(1); const playerMode: "balancer" | "direct" = "balancer"; const [selectedMirror, setSelectedMirror] = useState("embess_ru"); const [currentKpId, setCurrentKpId] = useState(null); const [currentImdbId, setCurrentImdbId] = useState(null); const [currentAllohaUrl, setCurrentAllohaUrl] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); const [kodikResults, setKodikResults] = useState([]); const [selectedKodikLink, setSelectedKodikLink] = useState(null); const [isSearchingKodik, setIsSearchingKodik] = useState(false); const [russianDubNotice, setRussianDubNotice] = useState(null); const [anilibriaStreamUrl, setAnilibriaStreamUrl] = useState(null); const [isLoadingAniLibria, setIsLoadingAniLibria] = useState(false); const videoRef = useRef(null); const topRef = useRef(null); const hlsInstanceRef = useRef(null); const aniLibriaReleasesCache = useRef>(new Map()); // Detailed Movie / Series Information Modal State const [detailsMedia, setDetailsMedia] = useState(null); const [isLoadingDetails, setIsLoadingDetails] = useState(false); const [showTrailerModal, setShowTrailerModal] = useState(false); // Simplified Video Player HUD & Controls State const [isHudVisible, setIsHudVisible] = useState(true); const [showPlayerSettings, setShowPlayerSettings] = useState(false); const [showPlayerEpisodes, setShowPlayerEpisodes] = useState(false); const hudTimeoutRef = useRef(null); const resetHudTimer = useCallback(() => { setIsHudVisible(true); if (hudTimeoutRef.current) { clearTimeout(hudTimeoutRef.current); } hudTimeoutRef.current = setTimeout(() => { setShowPlayerSettings((settingsOpen) => { setShowPlayerEpisodes((episodesOpen) => { if (!settingsOpen && !episodesOpen) { setIsHudVisible(false); } return episodesOpen; }); return settingsOpen; }); }, 3500); }, []); // Universal helper to convert a TMDB item into CinemaMediaItem const mapTmdbItem = (item: RawTmdbItem, forcedType?: "movie" | "tv"): CinemaMediaItem => { let isTv = false; if (forcedType) { isTv = forcedType === "tv"; } else if (item.media_type) { isTv = item.media_type === "tv"; } else { isTv = !item.title && (!!item.name || !!item.first_air_date); } const title = item.title || item.name || item.original_title || item.original_name || "Без названия"; const backdrop = formatMediaImg(item.backdrop_path || item.poster_path, "original"); const poster = formatMediaImg(item.poster_path || item.backdrop_path, "w500"); const year = (item.release_date || item.first_air_date || "").slice(0, 4) || "2024"; if (!isTv) { return { id: `tmdb-movie-${item.id}`, tmdb_id: item.id, media_type: "movie", title, original_title: item.original_name || item.original_title, backdrop_url: backdrop, poster_url: poster, release_year: year, seasons_count: undefined, duration_minutes: 120, genre: "Фильм", rating: item.vote_average ? Math.round(item.vote_average * 10) / 10 : 8.0, vote_count: item.vote_count, synopsis: item.overview?.trim() || "Описание фильма из международной базы TMDB.", is_original: false, seasons: [], }; } const initialSeasonsCount = item.number_of_seasons && item.number_of_seasons > 0 ? item.number_of_seasons : 1; const seasonsList: CinemaSeason[] = []; for (let s = 1; s <= initialSeasonsCount; s++) { seasonsList.push({ season_number: s, episodes: [ { id: `tmdb-${item.id}-s${s}-e1`, number: 1, title: "Серия 1", thumbnail: poster, duration_minutes: 45, }, ], }); } return { id: `tmdb-tv-${item.id}`, tmdb_id: item.id, media_type: "tv", title, original_title: item.original_name || item.original_title, backdrop_url: backdrop, poster_url: poster, release_year: year, seasons_count: initialSeasonsCount, duration_minutes: undefined, genre: "Сериал", rating: item.vote_average ? Math.round(item.vote_average * 10) / 10 : 8.0, vote_count: item.vote_count, synopsis: item.overview?.trim() || "Описание сериала из международной базы TMDB.", is_original: false, seasons: seasonsList, }; }; // Safe TMDB query via Rust backend (bypasses RU 403 blocks and CORS) const queryTmdb = useCallback(async (endpoint: string): Promise => { if (!isTauri) return null; try { return await invoke("tmdb_query", { endpoint }); } catch (err) { console.warn(`Error querying TMDB endpoint ${endpoint}:`, err); return null; } }, [isTauri]); // Verifies that titles returned from TMDB actually have active players/streams on CDN balancers const verifyPlayableBatch = useCallback( async (items: CinemaMediaItem[]): Promise => { if (!isTauri || items.length === 0) return items; try { const checkPayload = items.map((i) => ({ tmdb_id: i.tmdb_id || 0, title: i.title, original_title: i.original_title, media_type: i.media_type || "movie", vote_count: i.vote_count, release_year: i.release_year ? String(i.release_year) : undefined, })); const verifiedIds = await invoke("filter_playable_media", { items: checkPayload }); const verifiedSet = new Set(verifiedIds); return items.filter( (i) => i.tmdb_id && (verifiedSet.has(i.tmdb_id) || (KNOWN_KP_MAP as any)[i.tmdb_id]) ); } catch (err) { console.warn("Error verifying playable media:", err); return items; } }, [isTauri] ); // Load complete rich details (budget, revenue, director, cast, trailer, etc.) const fetchMediaDetails = useCallback(async (item: CinemaMediaItem) => { if (!item.tmdb_id) return; try { const isTv = item.media_type === "tv"; const endpoint = isTv ? `/tv/${item.tmdb_id}?language=ru-RU&append_to_response=credits,videos` : `/movie/${item.tmdb_id}?language=ru-RU&append_to_response=credits,videos`; const data = await queryTmdb(endpoint); if (data) { const cast = (data.credits?.cast || []).slice(0, 16).map((c: any) => ({ id: c.id, name: c.name || c.original_name, original_name: c.original_name, character: c.character, profile_path: c.profile_path ? formatMediaImg(c.profile_path, "w500") : null, })); const director = isTv ? (data.created_by && data.created_by.length > 0 ? data.created_by.map((c: any) => c.name).join(", ") : data.credits?.crew?.find((c: any) => c.job === "Director" || c.job === "Executive Producer")?.name) : data.credits?.crew?.find((c: any) => c.job === "Director")?.name; const countries = (data.production_countries || []).map((c: any) => c.name).join(", "); const genres = (data.genres || []).map((g: any) => g.name).join(", "); const runtime = data.runtime || (data.episode_run_time && data.episode_run_time[0]) || item.duration_minutes; const videos = data.videos?.results || []; const trailer = videos.find((v: any) => v.site === "YouTube" && v.type === "Trailer" && (v.iso_639_1 === "ru" || (v.name || "").toLowerCase().includes("рус"))) || videos.find((v: any) => v.site === "YouTube" && v.type === "Trailer") || videos.find((v: any) => v.site === "YouTube"); let realSeasons: CinemaSeason[] = []; let realSeasonsCount: number | undefined = undefined; if (isTv) { if (Array.isArray(data.seasons) && data.seasons.length > 0) { const regularSeasons = data.seasons.filter((s: any) => s.season_number > 0); const validSeasons = regularSeasons.length > 0 ? regularSeasons : data.seasons; realSeasonsCount = data.number_of_seasons || validSeasons.length; realSeasons = validSeasons.map((s: any) => { const epCount = s.episode_count && s.episode_count > 0 ? s.episode_count : 1; const seasonThumb = s.poster_path ? formatMediaImg(s.poster_path, "w500") : item.poster_url; return { season_number: s.season_number, episodes: Array.from({ length: epCount }, (_, idx) => ({ id: `tmdb-${item.tmdb_id}-s${s.season_number}-e${idx + 1}`, number: idx + 1, title: `Серия ${idx + 1}`, thumbnail: seasonThumb, duration_minutes: (data.episode_run_time && data.episode_run_time[0]) || 45, })), }; }); } else { realSeasonsCount = data.number_of_seasons || 1; realSeasons = [ { season_number: 1, episodes: [ { id: `tmdb-${item.tmdb_id}-s1-e1`, number: 1, title: "Серия 1", thumbnail: item.poster_url, duration_minutes: 45, }, ], }, ]; } } const today = new Date().toISOString().slice(0, 10); const releaseDate = data.release_date || data.first_air_date; const isFuture = releaseDate && releaseDate > today; const isUnreleasedStatus = data.status === "Planned" || data.status === "In Production" || data.status === "Post Production"; const isUnreleased = !!(isFuture || isUnreleasedStatus); const updateItem = (prev: CinemaMediaItem): CinemaMediaItem => ({ ...prev, tagline: data.tagline || prev.tagline, budget: data.budget && data.budget > 0 ? data.budget : prev.budget, revenue: data.revenue && data.revenue > 0 ? data.revenue : prev.revenue, duration_minutes: runtime || prev.duration_minutes, countries: countries || prev.countries, genre: genres || prev.genre, director: director || prev.director, cast: cast.length > 0 ? cast : prev.cast, trailer_youtube_key: trailer?.key || prev.trailer_youtube_key, synopsis: data.overview?.trim() || prev.synopsis, rating: data.vote_average ? Math.round(data.vote_average * 10) / 10 : prev.rating, seasons: isTv ? realSeasons : [], seasons_count: isTv ? realSeasonsCount : undefined, is_unreleased: isUnreleased, release_date_full: releaseDate || prev.release_date_full, }); setDetailsMedia((prev) => (prev && prev.id === item.id ? updateItem(prev) : prev)); setSelectedMedia((prev) => (prev && prev.id === item.id ? updateItem(prev) : prev)); if (isTv) { loadTvSeasonsDetails(item.tmdb_id, 1); } } // Also query Russian media info for Kinopoisk ID / rating if (isTauri && item.tmdb_id) { invoke("get_russian_media_info", { tmdbId: item.tmdb_id, isTv: item.media_type === "tv", title: item.title || item.original_title, }).then((info) => { if (info?.kp_id) { setDetailsMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); setSelectedMedia((prev) => (prev && prev.id === item.id ? { ...prev, kinopoisk_rating: info.kp_id ? "КП" : undefined } : prev)); } }).catch(() => {}); } } catch (e) { console.warn("Could not load full media details:", e); } finally { setIsLoadingDetails(false); } }, [queryTmdb, isTauri]); // Load All Catalogs (Released Movies, Series, Anime, Trending) on Mount const loadFullCatalog = useCallback(async () => { try { const today = new Date().toISOString().slice(0, 10); // 1. Trending All (Top 40 - page 1 & 2, filtered by released status & active streams) const [tData1, tData2] = await Promise.all([ queryTmdb("/trending/all/week?language=ru-RU&page=1"), queryTmdb("/trending/all/week?language=ru-RU&page=2"), ]); const allTrendingRaw = [...(tData1?.results || []), ...(tData2?.results || [])]; if (allTrendingRaw.length > 0) { const mapped = allTrendingRaw .filter((i: RawTmdbItem) => (i.title || i.name) && isMediaReleased(i) && (i.vote_count || 0) >= 30) .map((i: RawTmdbItem) => mapTmdbItem(i)); const verified = await verifyPlayableBatch(mapped); setTrendingAll(verified); if (verified.length > 0) { setSelectedMedia(verified[0]); } } // 2. Popular Released Movies (only those with digital/physical distribution and vote_count >= 100) const [mData1, mData2] = await Promise.all([ queryTmdb(`/discover/movie?language=ru-RU&sort_by=popularity.desc&primary_release_date.lte=${today}&vote_count.gte=100&with_release_type=4|5|6&page=1`), queryTmdb(`/discover/movie?language=ru-RU&sort_by=popularity.desc&primary_release_date.lte=${today}&vote_count.gte=100&with_release_type=4|5|6&page=2`), ]); const allMoviesRaw = [...(mData1?.results || []), ...(mData2?.results || [])]; if (allMoviesRaw.length > 0) { const mapped = allMoviesRaw .filter((i: RawTmdbItem) => (i.title || i.name) && isMediaReleased(i)) .map((i: RawTmdbItem) => mapTmdbItem(i, "movie")); const verified = await verifyPlayableBatch(mapped); setPopularMovies(verified); } // 3. Popular Released TV Shows (first_air_date <= today and vote_count >= 50) const [tvData1, tvData2] = await Promise.all([ queryTmdb(`/discover/tv?language=ru-RU&sort_by=popularity.desc&first_air_date.lte=${today}&vote_count.gte=50&page=1`), queryTmdb(`/discover/tv?language=ru-RU&sort_by=popularity.desc&first_air_date.lte=${today}&vote_count.gte=50&page=2`), ]); const allTvRaw = [...(tvData1?.results || []), ...(tvData2?.results || [])]; if (allTvRaw.length > 0) { const mapped = allTvRaw .filter((i: RawTmdbItem) => (i.title || i.name) && isMediaReleased(i)) .map((i: RawTmdbItem) => mapTmdbItem(i, "tv")); const verified = await verifyPlayableBatch(mapped); setPopularSeries(verified); } // 4. Genuine Anime (Japanese Animation, vote_count >= 30) const [aData1, aData2] = await Promise.all([ queryTmdb(`/discover/tv?with_genres=16&with_original_language=ja&sort_by=popularity.desc&first_air_date.lte=${today}&vote_count.gte=30&language=ru-RU&page=1`), queryTmdb(`/discover/tv?with_genres=16&with_original_language=ja&sort_by=popularity.desc&first_air_date.lte=${today}&vote_count.gte=30&language=ru-RU&page=2`), ]); const allAnimeRaw = [...(aData1?.results || []), ...(aData2?.results || [])]; if (allAnimeRaw.length > 0) { const mapped = allAnimeRaw .filter((i: RawTmdbItem) => (i.title || i.name) && isMediaReleased(i)) .map((i: RawTmdbItem) => mapTmdbItem(i, "tv")); const verified = await verifyPlayableBatch(mapped); setPopularAnime(verified); } } catch (err) { console.warn("Failed to load catalog from TMDB:", err); } }, [queryTmdb, verifyPlayableBatch]); // Load config, state, and full catalog on mount useEffect(() => { if (!isTauri) return; invoke("get_cinema_config") .then((cfg) => { setConfig(cfg); loadFullCatalog(); }) .catch((err) => console.warn("Failed to get cinema config:", err)); invoke("get_cinema_state") .then((state) => { if (state) setCinemaState(state); }) .catch((err) => console.warn("Failed to get local cinema state:", err)); }, [isTauri, loadFullCatalog]); // Search TMDB live across all movies, series, and anime (only released and verified playable titles) const executeSearch = useCallback( async (query: string) => { if (!query.trim()) { setSearchResults([]); return; } setIsSearching(true); try { const data = await queryTmdb( `/search/multi?query=${encodeURIComponent(query)}&language=ru-RU&page=1` ); let rawResults = (data?.results || []).filter( (r: RawTmdbItem) => (r.media_type === "movie" || r.media_type === "tv") && (r.title || r.name) && isMediaReleased(r) ); if (rawResults.length === 0) { // Fallback search with en-US query if Russian gave 0 results const dataEn = await queryTmdb( `/search/multi?query=${encodeURIComponent(query)}&language=en-US&page=1` ); if (dataEn?.results) { rawResults = dataEn.results.filter( (r: RawTmdbItem) => (r.media_type === "movie" || r.media_type === "tv") && (r.title || r.name) && isMediaReleased(r) ); } } if (rawResults.length > 0) { const mapped = rawResults .sort((a: any, b: any) => (b.vote_count || b.popularity || 0) - (a.vote_count || a.popularity || 0)) .map((r: RawTmdbItem) => mapTmdbItem(r)); const verified = await verifyPlayableBatch(mapped); setSearchResults(verified); } else { setSearchResults([]); } } catch (err) { console.warn("Search error:", err); } finally { setIsSearching(false); } }, [queryTmdb, verifyPlayableBatch] ); useEffect(() => { const timer = setTimeout(() => { if (searchQuery.trim().length >= 2) { executeSearch(searchQuery); } else { setSearchResults([]); } }, 300); return () => clearTimeout(timer); }, [searchQuery, executeSearch]); // When a TV series is selected, load its real seasons and episodes from TMDB const loadTvSeasonsDetails = async (tmdbId: number, seasonNumber: number) => { try { const data = await queryTmdb(`/tv/${tmdbId}/season/${seasonNumber}?language=ru-RU`); if (data?.episodes && data.episodes.length > 0) { const loadedEpisodes: CinemaEpisode[] = data.episodes.map((ep: any) => ({ id: `tmdb-${tmdbId}-s${seasonNumber}-e${ep.episode_number}`, number: ep.episode_number, title: ep.name ? `${ep.name}` : `Серия ${ep.episode_number}`, thumbnail: ep.still_path ? formatMediaImg(ep.still_path, "w500") : selectedMedia.poster_url, duration_minutes: ep.runtime || 45, description: ep.overview || undefined, })); const applyEpisodes = (prev: CinemaMediaItem): CinemaMediaItem => { if (prev.tmdb_id !== tmdbId) return prev; const seasonExists = prev.seasons.some((s) => s.season_number === seasonNumber); const nextSeasons = seasonExists ? prev.seasons.map((s) => (s.season_number === seasonNumber ? { ...s, episodes: loadedEpisodes } : s)) : [...prev.seasons, { season_number: seasonNumber, episodes: loadedEpisodes }]; return { ...prev, seasons: nextSeasons, }; }; setSelectedMedia((prev) => applyEpisodes(prev)); setDetailsMedia((prev) => (prev ? applyEpisodes(prev) : prev)); setPlayingMedia((prev) => (prev ? applyEpisodes(prev) : prev)); } } catch (err) { console.warn("Failed to load season details:", err); } }; const isBookmarked = cinemaState.bookmarks.includes(selectedMedia.id); // Toggle Bookmark (Saved locally on user's computer in cinema_state.json) const handleToggleBookmark = async () => { if (isTauri) { try { const nextState = await invoke("toggle_cinema_bookmark", { titleId: selectedMedia.id, }); setCinemaState(nextState); } catch (err) { console.error("Error toggling bookmark:", err); } } else { setCinemaState((prev) => ({ ...prev, bookmarks: prev.bookmarks.includes(selectedMedia.id) ? prev.bookmarks.filter((id) => id !== selectedMedia.id) : [...prev.bookmarks, selectedMedia.id], })); } }; // Get progress for a specific episode const getEpisodeProgress = (seasonNum: number, episodeNum: number): number => { const found = cinemaState.history.find( (h) => h.title_id === selectedMedia.id && h.season === seasonNum && h.episode === episodeNum ); if (!found || found.duration_seconds <= 0) return 0; return Math.min(100, Math.round((found.progress_seconds / found.duration_seconds) * 100)); }; const DEMON_SLAYER_SEASON_RELEASES: Record = { 1: 8325, // Season 1 (2019, 26 eps) 2: 9093, // Season 2 (Entertainment District, 11 eps) 3: 9406, // Season 3 (Swordsmith Village, 11 eps) 4: 9660, // Season 4 (Hashira Training, 8 eps) 5: 10144, // Season 5 (Infinity Castle, 2025) }; // Fetch AniLibria direct Full HD HLS stream (.m3u8) without regional blocks const fetchAniLibriaEpisodeStream = useCallback( async (media: CinemaMediaItem, seasonNum: number, episodeNum: number): Promise => { setIsLoadingAniLibria(true); try { let releaseId: number | null = null; const tLower = media.title.toLowerCase(); const origLower = (media.original_title || "").toLowerCase(); if (tLower.includes("клинок") || origLower.includes("kimetsu")) { releaseId = DEMON_SLAYER_SEASON_RELEASES[seasonNum] || 8325; } else { // Search AniLibria by title const q = encodeURIComponent(media.title); const searchRes = await fetch(`https://anilibria.top/api/v1/app/search/releases?query=${q}`); if (searchRes.ok) { const searchData = await searchRes.json(); if (Array.isArray(searchData) && searchData.length > 0) { releaseId = searchData[0].id; } } if (!releaseId && media.original_title) { const qOrig = encodeURIComponent(media.original_title); const searchRes2 = await fetch(`https://anilibria.top/api/v1/app/search/releases?query=${qOrig}`); if (searchRes2.ok) { const searchData2 = await searchRes2.json(); if (Array.isArray(searchData2) && searchData2.length > 0) { releaseId = searchData2[0].id; } } } } if (!releaseId) { return null; } // Cache release details let relData = aniLibriaReleasesCache.current.get(releaseId); if (!relData) { const relRes = await fetch(`https://anilibria.top/api/v1/anime/releases/${releaseId}`); if (relRes.ok) { relData = await relRes.json(); aniLibriaReleasesCache.current.set(releaseId, relData); } } if (relData?.episodes && relData.episodes.length > 0) { const targetEp = relData.episodes.find((e: any) => e.ordinal === episodeNum) || relData.episodes[episodeNum - 1] || relData.episodes[0]; return targetEp.hls_1080 || targetEp.hls_720 || targetEp.hls_480 || null; } return null; } catch (err) { console.warn("AniLibria fetch error:", err); return null; } finally { setIsLoadingAniLibria(false); } }, [] ); // Initialize HLS.js on the video element for native in-app streaming useEffect(() => { if (selectedMirror !== "anilibria" || !anilibriaStreamUrl || !videoRef.current) { if (hlsInstanceRef.current) { hlsInstanceRef.current.destroy(); hlsInstanceRef.current = null; } return; } const video = videoRef.current; if (Hls.isSupported()) { if (hlsInstanceRef.current) { hlsInstanceRef.current.destroy(); } const hls = new Hls({ enableWorker: true, lowLatencyMode: false, backBufferLength: 60, }); hlsInstanceRef.current = hls; hls.loadSource(anilibriaStreamUrl); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal) { console.warn("HLS fatal error, falling back to VidSrc Pro:", data); hls.destroy(); hlsInstanceRef.current = null; setAnilibriaStreamUrl(null); setSelectedMirror("vidsrc_to"); setRussianDubNotice("⚠️ Поток AniLibria временно недоступен. Переключено на проверенный Full HD плеер VidSrc Pro."); } }); } else if (video.canPlayType("application/vnd.apple.mpegurl")) { video.src = anilibriaStreamUrl; video.play().catch(() => {}); video.onerror = () => { setAnilibriaStreamUrl(null); setSelectedMirror("vidsrc_to"); setRussianDubNotice("⚠️ Ошибка воспроизведения AniLibria. Переключено на VidSrc Pro."); }; } return () => { if (hlsInstanceRef.current) { hlsInstanceRef.current.destroy(); hlsInstanceRef.current = null; } }; }, [selectedMirror, anilibriaStreamUrl]); // Start playback for a media item (movie or specific series episode) const startPlayback = (media: CinemaMediaItem, seasonNum: number = 1, episode?: CinemaEpisode) => { setSelectedMedia(media); setPlayingMedia(media); setPlayingSeasonNumber(seasonNum); setRussianDubNotice(null); setAnilibriaStreamUrl(null); setIsHudVisible(true); setShowPlayerSettings(false); setShowPlayerEpisodes(false); resetHudTimer(); const ep = episode || media.seasons.find((s) => s.season_number === seasonNum)?.episodes[0] || media.seasons[0]?.episodes[0] || { id: `play-${media.id}-s${seasonNum}`, number: 1, title: media.media_type === "movie" ? "Полный фильм" : "Серия 1", thumbnail: media.poster_url, duration_minutes: media.duration_minutes || (media.media_type === "movie" ? 120 : 45), }; setPlayingEpisode(ep); const isAnime = media.genre?.toLowerCase().includes("аниме") || media.genre?.toLowerCase().includes("cartoon") || media.title.toLowerCase().includes("клинок") || (media.original_title || "").toLowerCase().includes("kimetsu"); // Instantly check known Kinopoisk ID for zero-latency Russian dubbing const knownKp = media.tmdb_id ? KNOWN_KP_MAP[media.tmdb_id] : null; if (knownKp) { setCurrentKpId(knownKp); } else { setCurrentKpId(null); } setCurrentImdbId(null); setCurrentAllohaUrl(null); // Default to genuine Russian spoken voiceover (direct Embess Full HD stream): if (isAnime) { setSelectedMirror("kodik_ru"); showTvToast("🌸 Русская озвучка аниме", "🎙️", 1800); } else { setSelectedMirror("embess_ru"); showTvToast("🇷🇺 Включен полный русский дубляж (1080p)", "🎙️", 1800); } // Resolve Kinopoisk ID & Russian media data asynchronously via Rust backend or web fetch if (isTauri && media.tmdb_id) { invoke("get_russian_media_info", { tmdbId: media.tmdb_id, isTv: media.media_type === "tv", title: media.title || media.original_title, }) .then((info) => { if (info) { if (info.kp_id) setCurrentKpId(Number(info.kp_id)); if (info.imdb_id) setCurrentImdbId(info.imdb_id); if (info.iframe_url) { setCurrentAllohaUrl(info.iframe_url); if (!isAnime) { setSelectedMirror("alloha_ru"); } } if (!isAnime) { setRussianDubNotice( `🇷🇺 Русская озвучка подключена: «${info.russian_title || media.title}» воспроизводится с полным русским дубляжем.` ); } } }) .catch(() => {}); } else if (media.tmdb_id) { fetch(`https://api.apbugall.org/?token=45e20a5f584becf7a64dffb7174ddf&tmdb=${media.tmdb_id}`) .then((r) => r.json()) .then((data) => { if (data?.data) { const d = Array.isArray(data.data) ? data.data[0] : data.data; if (d?.id_kp) setCurrentKpId(Number(d.id_kp)); if (d?.id_imdb) setCurrentImdbId(d.id_imdb); if (d?.iframe) { setCurrentAllohaUrl(d.iframe); if (!isAnime) { setSelectedMirror("alloha_ru"); } } } }) .catch(() => {}); } // Search Kodik in background for anime or studio translations searchKodikDubbings(media.original_title || media.title, isAnime); }; // Card Click Action: Opens rich media details modal (DOES NOT start player immediately) const handleMediaCardClick = (item: CinemaMediaItem) => { lastFocusedCardRef.current = document.activeElement as HTMLElement | null; setSelectedMedia(item); setDetailsMedia(item); setSelectedSeason(1); setIsLoadingDetails(true); fetchMediaDetails(item); if (item.media_type === "tv" && item.tmdb_id) { loadTvSeasonsDetails(item.tmdb_id, 1); } playTvBeep(520, 0.035); }; const focusFirstContentCard = useCallback(() => { setTimeout(() => { const target = document.querySelector(".has-cinema-btn-play") || document.querySelector(".has-cinema-media-card") || document.querySelector(".has-cinema-grid-card"); if (target) { target.focus(); target.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); playTvBeep(640, 0.03); } }, 50); }, [playTvBeep]); // Play button from Details Modal const handlePlayFromDetails = (seasonNum: number = 1, episode?: CinemaEpisode) => { if (!detailsMedia) return; const media = detailsMedia; startPlayback(media, seasonNum, episode); }; // Toggle bookmark directly from details modal const handleToggleBookmarkDetails = async () => { if (!detailsMedia) return; const mediaId = detailsMedia.id; if (isTauri) { try { const nextState = await invoke("toggle_cinema_bookmark", { titleId: mediaId, }); setCinemaState(nextState); } catch (err) { console.error("Error toggling bookmark:", err); } } else { setCinemaState((prev) => ({ ...prev, bookmarks: prev.bookmarks.includes(mediaId) ? prev.bookmarks.filter((id) => id !== mediaId) : [...prev.bookmarks, mediaId], })); } playTvBeep(560, 0.03); }; // Auto-focus the Play button when Details modal opens (for Android TV Remote D-Pad) useEffect(() => { if (detailsMedia && !playingEpisode) { const timer = setTimeout(() => { const playBtn = document.querySelector(".has-cinema-details-btn-play"); if (playBtn) { playBtn.focus(); } }, 50); return () => clearTimeout(timer); } }, [detailsMedia, playingEpisode]); // Play a specific episode const handlePlayEpisode = (seasonNum: number, episode: CinemaEpisode) => { startPlayback(selectedMedia, seasonNum, episode); }; // Play button on Hero Banner const handleHeroPlay = () => { if (selectedMedia.is_unreleased) { showTvToast("Фильм ещё не вышел в цифровой прокат", "⏳", 3000); return; } if (selectedMedia.media_type === "movie") { startPlayback(selectedMedia, 1); return; } const lastHistory = cinemaState.history.find((h) => h.title_id === selectedMedia.id); if (lastHistory) { const seasonObj = selectedMedia.seasons.find((s) => s.season_number === lastHistory.season); const epObj = seasonObj?.episodes.find((e) => e.number === lastHistory.episode); if (epObj) { startPlayback(selectedMedia, lastHistory.season, epObj); return; } } const s1 = selectedMedia.seasons[0]; if (s1 && s1.episodes.length > 0) { startPlayback(selectedMedia, 1, s1.episodes[0]); } else { startPlayback(selectedMedia, 1); } }; // Next / Previous episode navigation right inside the player const handleNextEpisode = () => { if (!playingEpisode) return; const currentMedia = playingMedia || selectedMedia; const currentSeason = currentMedia.seasons.find((s) => s.season_number === playingSeasonNumber) || currentMedia.seasons[0]; const nextEp = currentSeason.episodes.find((e) => e.number === playingEpisode.number + 1); if (nextEp) { startPlayback(currentMedia, playingSeasonNumber, nextEp); } }; const handlePrevEpisode = () => { if (!playingEpisode || playingEpisode.number <= 1) return; const currentMedia = playingMedia || selectedMedia; const currentSeason = currentMedia.seasons.find((s) => s.season_number === playingSeasonNumber) || currentMedia.seasons[0]; const prevEp = currentSeason.episodes.find((e) => e.number === playingEpisode.number - 1); if (prevEp) { startPlayback(currentMedia, playingSeasonNumber, prevEp); } }; // Quick 1-click mirror switcher if a CDN balancer fails or is blocked const handleCycleMirror = () => { const mirrors: BalancerMirror[] = ["embess_ru", "vidlink_pro", "vidsrc_to", "autoembed"]; if (kodikResults.length > 0) mirrors.push("kodik_ru"); if (currentAllohaUrl) mirrors.push("alloha_ru"); const currentIndex = mirrors.indexOf(selectedMirror); const nextIndex = (currentIndex + 1) % mirrors.length; const nextMirror = mirrors[nextIndex]; setSelectedMirror(nextMirror); const labels: Record = { embess_ru: "Embess (Дубляж 1080p)", smotrim_ru: "Smotrim", vidlink_pro: "VidLink Pro (Full HD)", vidsrc_to: "VidSrc Pro", autoembed: "AutoEmbed", alloha_ru: "Alloha", kodik_ru: "Kodik", anilibria: "AniLibria", }; showTvToast(`Сервер: ${labels[nextMirror] || nextMirror}`, "🔄", 2000); resetHudTimer(); }; // Save playback progress on time update const handleVideoTimeUpdate = () => { if (!videoRef.current || !playingEpisode) return; const currentTime = videoRef.current.currentTime; const duration = videoRef.current.duration; const activeMedia = playingMedia || selectedMedia; if (duration > 0 && isTauri) { const payload: CinemaPlayback = { title_id: activeMedia.id, title_name: activeMedia.title, poster_path: activeMedia.poster_url, backdrop_path: activeMedia.backdrop_url, season: playingSeasonNumber, episode: playingEpisode.number, progress_seconds: currentTime, duration_seconds: duration, last_watched: new Date().toISOString(), }; if (Math.floor(currentTime) % 5 === 0) { invoke("update_cinema_progress", { item: payload }) .then((st) => setCinemaState(st)) .catch(() => {}); } } }; // Close player and save exact position const handleClosePlayer = useCallback(() => { const activeMedia = playingMedia || selectedMedia; try { if (isTauri && isFullscreen) { try { getCurrentWindow().setFullscreen(false).catch(() => {}); } catch {} setIsFullscreen(false); } } catch {} if (videoRef.current && playingEpisode && isTauri) { try { const currentTime = videoRef.current.currentTime; const duration = videoRef.current.duration; if (duration > 0 && activeMedia) { const payload: CinemaPlayback = { title_id: activeMedia.id, title_name: activeMedia.title, poster_path: activeMedia.poster_url, backdrop_path: activeMedia.backdrop_url, season: playingSeasonNumber, episode: playingEpisode.number, progress_seconds: currentTime, duration_seconds: duration, last_watched: new Date().toISOString(), }; invoke("update_cinema_progress", { item: payload }) .then((st) => setCinemaState(st)) .catch(() => {}); } } catch {} } if (hlsInstanceRef.current) { try { hlsInstanceRef.current.destroy(); } catch {} hlsInstanceRef.current = null; } if (hudTimeoutRef.current) { clearTimeout(hudTimeoutRef.current); } setIsHudVisible(true); setShowPlayerSettings(false); setShowPlayerEpisodes(false); // Return user back cleanly setDetailsMedia(null); if (activeMedia) { setSelectedMedia(activeMedia); } setPlayingEpisode(null); setPlayingMedia(null); setCurrentKpId(null); setCurrentImdbId(null); setCurrentAllohaUrl(null); setKodikResults([]); setSelectedKodikLink(null); setRussianDubNotice(null); setAnilibriaStreamUrl(null); }, [isTauri, isFullscreen, playingEpisode, playingMedia, selectedMedia, playingSeasonNumber]); // D-Pad Spatial Navigation & TV Remote Key Handler const handleRemoteKeyDown = useCallback( (e: KeyboardEvent) => { // Auto-activate TV mode immediately on ANY remote control button press const isRemoteKey = [19, 20, 21, 22, 23, 4, 82, 85, 179].includes(e.keyCode) || ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key); if (isRemoteKey && !isTvMode) { setIsTvMode(true); localStorage.setItem("has_tv_mode", "true"); } const isInput = document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement; // Android TV / Remote key mappings const isBack = e.key === "Escape" || e.key === "GoBack" || e.key === "BrowserBack" || e.keyCode === 4 || e.keyCode === 27; const isPlayPause = e.key === "MediaPlayPause" || e.keyCode === 85 || e.keyCode === 179; const isFastForward = e.key === "MediaFastForward" || e.keyCode === 90; const isRewind = e.key === "MediaRewind" || e.keyCode === 89; // Remote Menu key (Android TV remote Menu button / ContextMenu) if (e.key === "Menu" || e.keyCode === 82 || e.key === "ContextMenu") { e.preventDefault(); setIsTvNavOpen((prev) => { const next = !prev; if (next) { setTimeout(() => { const navBtn = document.querySelector(".has-cinema-tv-nav-item.active") || document.querySelector(".has-cinema-tv-nav-item"); if (navBtn) { navBtn.focus(); playTvBeep(640, 0.03); } }, 30); } return next; }); return; } // F10 key toggles TV mode fallback if (e.key === "F10") { e.preventDefault(); toggleTvMode(); return; } // 1. In-player remote control if (playingEpisode !== null) { resetHudTimer(); if (isBack) { e.preventDefault(); if (showPlayerSettings) { setShowPlayerSettings(false); return; } if (showPlayerEpisodes) { setShowPlayerEpisodes(false); return; } handleClosePlayer(); showTvToast("Плеер закрыт", "✕", 1500); playTvBeep(400, 0.05); return; } // Pressing ArrowUp / Menu / Info on TV remote reveals HUD and focuses "← Назад" button if ( e.key === "ArrowUp" || e.keyCode === 19 || e.keyCode === 38 || e.key === "Info" ) { e.preventDefault(); setIsHudVisible(true); resetHudTimer(); setTimeout(() => { const backBtn = document.querySelector(".has-cinema-player-btn-back"); if (backBtn) { backBtn.focus(); playTvBeep(640, 0.03); } }, 40); return; } // Pressing OK / Enter when HUD is idle wakes up the full HUD if ((e.key === "Enter" || e.keyCode === 13) && !isHudVisible && !videoRef.current) { e.preventDefault(); setIsHudVisible(true); resetHudTimer(); setTimeout(() => { const backBtn = document.querySelector(".has-cinema-player-btn-back"); if (backBtn) backBtn.focus(); }, 40); return; } if (isPlayPause || (e.code === "Space" && !isInput)) { e.preventDefault(); if (videoRef.current) { if (videoRef.current.paused) { videoRef.current.play().catch(() => {}); showTvToast("Воспроизведение", "▶", 1200); } else { videoRef.current.pause(); showTvToast("Пауза", "⏸", 1200); } playTvBeep(560, 0.04); } return; } if ( isFastForward || (e.key === "ArrowRight" && !isInput && !document.activeElement?.closest(".has-cinema-player-modal")) ) { if (videoRef.current) { e.preventDefault(); videoRef.current.currentTime = Math.min( videoRef.current.duration || 999999, videoRef.current.currentTime + 10 ); showTvToast("+10 сек", "⏩", 1000); playTvBeep(520, 0.03); return; } } if ( isRewind || (e.key === "ArrowLeft" && !isInput && !document.activeElement?.closest(".has-cinema-player-modal")) ) { if (videoRef.current) { e.preventDefault(); videoRef.current.currentTime = Math.max( 0, videoRef.current.currentTime - 10 ); showTvToast("-10 сек", "⏪", 1000); playTvBeep(480, 0.03); return; } } } // 2. Global Back button outside player if (isBack) { if (isTvNavOpen) { e.preventDefault(); setIsTvNavOpen(false); focusFirstContentCard(); return; } if (showTrailerModal) { e.preventDefault(); setShowTrailerModal(false); showTvToast("Трейлер закрыт", "✕", 1000); playTvBeep(450, 0.04); return; } if (detailsMedia) { e.preventDefault(); setDetailsMedia(null); showTvToast("Возврат к списку", "↩", 1000); playTvBeep(450, 0.04); setTimeout(() => { if (lastFocusedCardRef.current && document.contains(lastFocusedCardRef.current)) { lastFocusedCardRef.current.focus(); lastFocusedCardRef.current.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); } }, 40); return; } if (searchQuery.trim().length > 0) { e.preventDefault(); setSearchQuery(""); showTvToast("Поиск очищен", "🔍", 1200); playTvBeep(450, 0.04); focusFirstContentCard(); return; } if (activeTab !== "home") { e.preventDefault(); setActiveTab("home"); showTvToast("Каталог", "🏠", 1200); playTvBeep(450, 0.04); focusFirstContentCard(); return; } } // 3. D-Pad Directional Arrows Navigation const isArrow = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key) || [19, 20, 21, 22, 37, 38, 39, 40].includes(e.keyCode); if (isArrow) { if (isInput && e.key !== "ArrowDown") { return; // Let user type and navigate within text input } let direction: "up" | "down" | "left" | "right" = "down"; if (e.key === "ArrowUp" || e.keyCode === 19 || e.keyCode === 38) direction = "up"; else if ( e.key === "ArrowDown" || e.keyCode === 20 || e.keyCode === 40 ) direction = "down"; else if ( e.key === "ArrowLeft" || e.keyCode === 21 || e.keyCode === 37 ) direction = "left"; else if ( e.key === "ArrowRight" || e.keyCode === 22 || e.keyCode === 39 ) direction = "right"; const detailsEl = document.querySelector(".has-cinema-details-modal"); const playerEl = document.querySelector(".has-cinema-player-modal"); const trailerEl = document.querySelector(".has-cinema-trailer-modal"); const settingsSheet = document.querySelector(".has-cinema-player-settings-sheet"); const episodesSheet = document.querySelector(".has-cinema-player-episodes-sheet"); // If focus is currently inside the TV Navigation drawer const currentActive = document.activeElement as HTMLElement | null; if (currentActive && currentActive.closest(".has-cinema-tv-nav-drawer")) { if (direction === "right") { e.preventDefault(); setIsTvNavOpen(false); focusFirstContentCard(); return; } } let focusables = Array.from( document.querySelectorAll( '[data-tv-focusable="true"], .has-cinema-nav-item, .has-cinema-tv-nav-item, .has-cinema-tv-menu-pill, .has-cinema-btn-play, .has-cinema-btn-bookmark, .has-cinema-search-input, .has-cinema-media-card, .has-cinema-grid-card, .has-cinema-episode-card, .has-cinema-player-hud-btn, .has-cinema-player-btn-back, .has-cinema-drawer-option-btn, .has-cinema-drawer-chip, .has-cinema-drawer-ep-btn, .has-cinema-drawer-close-btn, .has-cinema-drawer-btn-primary, .has-cinema-drawer-btn-secondary, .has-cinema-player-pill-nav, .has-cinema-season-tab, .has-cinema-details-btn-play, .has-cinema-details-close-btn, .has-cinema-details-btn-bookmark, .has-cinema-details-btn-trailer, .has-cinema-actor-card, .has-cinema-btn-info-wide' ) ).filter((el) => { const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0) return false; const style = window.getComputedStyle(el); return ( style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" ); }); // Trap focus inside modal or drawer if open if (settingsSheet) { focusables = focusables.filter((el) => settingsSheet.contains(el)); } else if (episodesSheet) { focusables = focusables.filter((el) => episodesSheet.contains(el)); } else if (trailerEl) { focusables = focusables.filter((el) => trailerEl.contains(el)); } else if (playerEl) { focusables = focusables.filter((el) => playerEl.contains(el)); } else if (detailsEl) { focusables = focusables.filter((el) => detailsEl.contains(el)); } if (focusables.length === 0) return; const current = document.activeElement as HTMLElement | null; if (!current || !focusables.includes(current)) { const initial = focusables.find((el) => el.classList.contains("has-cinema-btn-play")) || focusables.find((el) => el.classList.contains("has-cinema-nav-item")) || focusables[0]; if (initial) { e.preventDefault(); initial.focus(); initial.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); playTvBeep(580, 0.03); } return; } const curRect = current.getBoundingClientRect(); const curCx = curRect.left + curRect.width / 2; const curCy = curRect.top + curRect.height / 2; let bestEl: HTMLElement | null = null; let minScore = Infinity; for (const candidate of focusables) { if (candidate === current) continue; const cRect = candidate.getBoundingClientRect(); const cCx = cRect.left + cRect.width / 2; const cCy = cRect.top + cRect.height / 2; const dx = cCx - curCx; const dy = cCy - curCy; if (direction === "up" && dy >= -8) continue; if (direction === "down" && dy <= 8) continue; if (direction === "left" && dx >= -8) continue; if (direction === "right" && dx <= 8) continue; let score = Infinity; if (direction === "up" || direction === "down") { score = Math.abs(dy) + Math.abs(dx) * 1.7; } else { score = Math.abs(dx) + Math.abs(dy) * 2.4; } if (score < minScore) { minScore = score; bestEl = candidate; } } if (bestEl) { e.preventDefault(); bestEl.focus(); bestEl.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); playTvBeep(640, 0.025); } else { // If no element found in this direction outside modals: // Trigger the TV Navigation Drawer on Left (from leftmost card) or Right (from rightmost edge) if (!detailsEl && !playerEl && !trailerEl && !settingsSheet && !episodesSheet) { if (direction === "left" || direction === "right") { e.preventDefault(); setIsTvNavOpen(true); setTimeout(() => { const targetNav = document.querySelector(".has-cinema-tv-nav-item.active") || document.querySelector(".has-cinema-tv-nav-item"); if (targetNav) { targetNav.focus(); playTvBeep(640, 0.03); } }, 40); } } } } }, [ isTvMode, isTvNavOpen, playingEpisode, detailsMedia, showTrailerModal, showPlayerSettings, showPlayerEpisodes, searchQuery, activeTab, handleClosePlayer, showTvToast, playTvBeep, toggleTvMode, resetHudTimer, focusFirstContentCard, ] ); useEffect(() => { window.addEventListener("keydown", handleRemoteKeyDown); return () => { window.removeEventListener("keydown", handleRemoteKeyDown); }; }, [handleRemoteKeyDown]); // Handle Android TV hardware back & browser popstate to guarantee return to movie details card useEffect(() => { if (playingEpisode !== null) { window.history.pushState({ cinemaPlayer: true }, ""); const handlePopState = (e: PopStateEvent) => { e.preventDefault(); if (showPlayerSettings) { setShowPlayerSettings(false); window.history.pushState({ cinemaPlayer: true }, ""); return; } if (showPlayerEpisodes) { setShowPlayerEpisodes(false); window.history.pushState({ cinemaPlayer: true }, ""); return; } handleClosePlayer(); }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; } }, [playingEpisode, showPlayerSettings, showPlayerEpisodes, handleClosePlayer]); // Handle Android TV hardware back & browser popstate for details modal useEffect(() => { if (detailsMedia !== null && playingEpisode === null) { window.history.pushState({ cinemaDetails: true }, ""); const handlePopState = (e: PopStateEvent) => { e.preventDefault(); if (showTrailerModal) { setShowTrailerModal(false); window.history.pushState({ cinemaDetails: true }, ""); return; } setDetailsMedia(null); }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; } }, [detailsMedia, playingEpisode, showTrailerModal]); // Toggle Desktop Fullscreen mode const handleToggleFullscreen = async () => { if (isTauri) { try { const win = getCurrentWindow(); const current = await win.isFullscreen(); await win.setFullscreen(!current); setIsFullscreen(!current); } catch { setIsFullscreen((prev) => !prev); } } else { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}); setIsFullscreen(true); } else { document.exitFullscreen().catch(() => {}); setIsFullscreen(false); } } }; // Switch mirror manually const handleSelectMirror = async (mirrorId: BalancerMirror) => { setSelectedMirror(mirrorId); if (mirrorId === "anilibria") { const activeMedia = playingMedia || selectedMedia; const epNum = playingEpisode?.number || 1; const stream = await fetchAniLibriaEpisodeStream(activeMedia, playingSeasonNumber, epNum); if (stream) { setAnilibriaStreamUrl(stream); setRussianDubNotice("🌸 Подключен поток AniLibria Full HD (Studio Band)."); } } else { setAnilibriaStreamUrl(null); } }; // Title matching helper to eliminate unrelated fuzzy search results const isMatchingTitle = (target: string, candidate: string): boolean => { if (!target || !candidate) return false; const cleanTarget = target.toLowerCase().replace(/[^a-zа-я0-9\s]/gi, " ").trim(); const cleanCandidate = candidate.toLowerCase().replace(/[^a-zа-я0-9\s]/gi, " ").trim(); if (cleanTarget === cleanCandidate) return true; const targetWords = cleanTarget.split(/\s+/).filter((w) => w.length > 2); if (targetWords.length === 0) return false; const minWordsToCheck = Math.min(2, targetWords.length); return targetWords.slice(0, minWordsToCheck).every((w) => cleanCandidate.includes(w)); }; // Search Kodik API for Russian studio voiceovers (LostFilm, HDRezka, Studio Band, AniLibria, AniDUB, Дубляж) const searchKodikDubbings = async (title: string, autoSwitch: boolean = false) => { setIsSearchingKodik(true); setRussianDubNotice(null); const activeMedia = playingMedia || selectedMedia; const targetTitle = activeMedia.title; const targetOrig = activeMedia.original_title || title; try { let rawResults: KodikItem[] = []; // 1. Try search by original title first if (targetOrig) { const q = encodeURIComponent(targetOrig); const res = await fetch(`https://kodik-api.com/search?token=41dd95f84c21719b09d6c71182237a25&title=${q}`); if (res.ok) { const data = await res.json(); if (Array.isArray(data.results)) { rawResults = data.results; } } } // 2. Try search by Russian title if needed if (rawResults.length === 0 && targetTitle) { const qRu = encodeURIComponent(targetTitle); const resRu = await fetch(`https://kodik-api.com/search?token=41dd95f84c21719b09d6c71182237a25&title=${qRu}`); if (resRu.ok) { const dataRu = await resRu.json(); if (Array.isArray(dataRu.results)) { rawResults = dataRu.results; } } } // 3. Strict filtering: ONLY include results that actually match this title! const matchingResults = rawResults.filter((r) => { const matchRu = isMatchingTitle(targetTitle, r.title); const matchOrig = isMatchingTitle(targetOrig, r.title_orig || r.title); return matchRu || matchOrig; }); // 4. Deduplicate by translation studio name const uniqueStudios = new Map(); for (const item of matchingResults) { const studioName = item.translation?.title?.trim() || "Озвучка"; if (!uniqueStudios.has(studioName)) { uniqueStudios.set(studioName, item); } } const finalDubbings = Array.from(uniqueStudios.values()); if (finalDubbings.length > 0) { setKodikResults(finalDubbings); setSelectedKodikLink(finalDubbings[0].link); if (autoSwitch) { setSelectedMirror("kodik_ru"); setRussianDubNotice(`🇷🇺 Найдено ${finalDubbings.length} студий русской озвучки для «${targetTitle}».`); } } else { setKodikResults([]); setSelectedKodikLink(null); if (autoSwitch) { setSelectedMirror("vidlink_pro"); setRussianDubNotice("🇷🇺 Для данного фильма русская аудиодорожка встроена в плеер VidLink Pro (нажмите на шестеренку ⚙️ или значок аудио в правом нижнем углу плеера)."); } } } catch { setKodikResults([]); setSelectedKodikLink(null); if (autoSwitch) { setSelectedMirror("vidlink_pro"); setRussianDubNotice("🌍 Переключено на основной плеер VidLink Pro."); } } finally { setIsSearchingKodik(false); } }; const currentSeasonData: CinemaSeason = selectedMedia.seasons.find((s) => s.season_number === selectedSeason) || selectedMedia.seasons[0]; // Balancer embed URL with multiple verified mirror selection const getBalancerEmbedUrl = (): string => { const media = playingMedia || selectedMedia; const tmdb = media.tmdb_id; const isTv = media.media_type === "tv"; const epNum = playingEpisode?.number || 1; const sNum = playingSeasonNumber || 1; const kpId = currentKpId || (tmdb ? KNOWN_KP_MAP[tmdb] : null); const imdbId = currentImdbId; switch (selectedMirror) { case "smotrim_ru": case "embess_ru": if (kpId) { return `https://api.embess.ws/embed/kp/${kpId}${isTv ? `?season=${sNum}&episode=${epNum}` : ""}`; } if (imdbId) { return `https://api.embess.ws/embed/imdb/${imdbId}${isTv ? `?season=${sNum}&episode=${epNum}` : ""}`; } // If neither KP nor IMDb is known for Embess, route to VidLink Pro which natively resolves all TMDB IDs return isTv ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?primaryColor=6366f1` : `https://vidlink.pro/movie/${tmdb || 693134}?primaryColor=6366f1`; case "alloha_ru": if (currentAllohaUrl) { return currentAllohaUrl; } if (kpId) { return `https://api.delivembd.ws/embed/kp/${kpId}${isTv ? `?season=${sNum}&episode=${epNum}` : ""}`; } return isTv ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?primaryColor=6366f1` : `https://vidlink.pro/movie/${tmdb || 693134}?primaryColor=6366f1`; case "kodik_ru": if (selectedKodikLink) { return selectedKodikLink.startsWith("//") ? `https:${selectedKodikLink}` : selectedKodikLink; } if (kpId) { return `https://api.embess.ws/embed/kp/${kpId}${isTv ? `?season=${sNum}&episode=${epNum}` : ""}`; } return isTv ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?primaryColor=6366f1` : `https://vidlink.pro/movie/${tmdb || 693134}?primaryColor=6366f1`; case "vidlink_pro": return isTv ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?primaryColor=6366f1` : `https://vidlink.pro/movie/${tmdb || 693134}?primaryColor=6366f1`; case "vidsrc_to": return isTv ? `https://vidsrc.to/embed/tv/${tmdb || 693134}/${sNum}/${epNum}` : `https://vidsrc.to/embed/movie/${tmdb || 693134}`; case "autoembed": return isTv ? `https://player.autoembed.co/embed/tv/${tmdb || 693134}/${sNum}/${epNum}` : `https://player.autoembed.co/embed/movie/${tmdb || 693134}`; default: if (kpId) { return `https://api.embess.ws/embed/kp/${kpId}${isTv ? `?season=${sNum}&episode=${epNum}` : ""}`; } return isTv ? `https://vidlink.pro/tv/${tmdb || 693134}/${sNum}/${epNum}?primaryColor=6366f1` : `https://vidlink.pro/movie/${tmdb || 693134}?primaryColor=6366f1`; } }; const balancerEmbedUrl = getBalancerEmbedUrl(); // Combine live TMDB catalog with initial fallbacks const allMoviesList = popularMovies.length > 0 ? popularMovies : INITIAL_LIBRARY.filter((i) => i.media_type === "movie"); const allSeriesList = popularSeries.length > 0 ? popularSeries : INITIAL_LIBRARY.filter((i) => i.media_type === "tv"); const allAnimeList = popularAnime.length > 0 ? popularAnime : INITIAL_LIBRARY.filter((i) => i.genre.includes("Аниме") || i.genre.includes("Cartoon")); const allTrending = trendingAll.length > 0 ? trendingAll : INITIAL_LIBRARY; const bookmarkedItems = [ ...trendingAll, ...popularMovies, ...popularSeries, ...popularAnime, ...INITIAL_LIBRARY, ].filter((item, index, self) => cinemaState.bookmarks.includes(item.id) && self.findIndex((s) => s.id === item.id) === index); return (
{/* ======================================================== ANDROID TV LEANBACK NAVIGATION DRAWER (Opens when pressing Left from cards, remote Menu, or Menu pill) ======================================================== */} {isTvMode && ( )} {/* ======================================================== 1. TOP NAVIGATION BAR (All categories & search) ======================================================== */}
{isTvMode && ( )} {/* Universal Search Box */}
setSearchQuery(e.target.value)} className="has-cinema-search-input" data-tv-focusable="true" /> {isSearching && } {searchQuery && ( )}
{/* User profile avatar */}
{currentUser.avatar ? ( User ) : ( {(currentUser.username || currentUser.email || "U").slice(0, 2).toUpperCase()} )}
{/* ======================================================== SEARCH RESULTS FULL GRID VIEW (When Searching) ======================================================== */} {searchQuery.trim().length >= 2 ? (

Результаты поиска по запросу «{searchQuery}»

Найдено: {searchResults.length}
{searchResults.length === 0 ? (
{isSearching ? "Поиск в международной базе фильмов..." : "Ничего не найдено по данному запросу."} Попробуйте ввести оригинальное название на английском или уточнить запрос.
) : (
{searchResults.map((item) => (
handleMediaCardClick(item)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(item); } }} >
{item.title}
★ {item.rating}
▶
{item.title} {item.release_year} • {getMediaCategoryLabel(item)}
))}
)}
) : ( <> {/* ======================================================== 2. HERO FEATURED BANNER ======================================================== */}
{selectedMedia.title}
{/* Badge */}
{getMediaCategoryLabel(selectedMedia)}
{/* Title */}

{selectedMedia.title}

{/* Metadata Row */}
{selectedMedia.release_year} {selectedMedia.media_type !== "movie" && selectedMedia.seasons_count && ( {selectedMedia.seasons_count}{" "} {selectedMedia.seasons_count === 1 ? "сезон" : selectedMedia.seasons_count < 5 ? "сезона" : "сезонов"} )} {selectedMedia.duration_minutes && ( {selectedMedia.duration_minutes} мин )} {selectedMedia.genre}
★★★★★ {selectedMedia.rating}
{/* Synopsis */}

{selectedMedia.synopsis}

{/* Action Buttons: Play + Bookmark + Info */}
{/* ======================================================== 3. SEASONS TABS & EPISODES (For TV Series & Anime) ======================================================== */} {selectedMedia.media_type !== "movie" && selectedMedia.seasons.length > 0 && (
{selectedMedia.seasons.map((season) => ( ))}
{/* Horizontal Carousel of Episodes */}
{currentSeasonData.episodes.map((ep) => { const progress = getEpisodeProgress(selectedSeason, ep.number); return (
handlePlayEpisode(selectedSeason, ep)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handlePlayEpisode(selectedSeason, ep); } }} title={`Смотреть: Сезон ${selectedSeason}, Серия ${ep.number} — ${ep.title}`} >
{ep.title}
S
{ep.title.startsWith("Серия") ? ep.title : `${ep.number}. ${ep.title}`}
{ep.duration_minutes && (
{ep.duration_minutes} мин
)}
); })}
)} {/* ======================================================== 4. FULL CATALOG ROWS & COLLECTIONS (All Movies & Series) ======================================================== */}
{/* Row 1: Continue Watching */} {cinemaState.history.length > 0 && activeTab === "home" && (

Продолжить просмотр

{cinemaState.history.map((h, i) => { const percent = h.duration_seconds > 0 ? Math.min(100, Math.round((h.progress_seconds / h.duration_seconds) * 100)) : 0; return (
{ const found = [ ...allTrending, ...allMoviesList, ...allSeriesList, ...allAnimeList, ].find((item) => item.id === h.title_id); if (found) { handleMediaCardClick(found); } }} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); const found = [ ...allTrending, ...allMoviesList, ...allSeriesList, ...allAnimeList, ].find((item) => item.id === h.title_id); if (found) { handleMediaCardClick(found); } } }} >
{h.title_name}
▶
{h.title_name} {h.season > 0 ? `Сезон ${h.season}, Серия ${h.episode}` : "Фильм"}
); })}
)} {/* Row 2: In Trend (All Movies & Series) */} {activeTab === "home" && allTrending.length > 0 && (

В тренде на этой неделе

{allTrending.map((item) => (
handleMediaCardClick(item)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(item); } }} >
{item.title}
★ {item.rating}
▶
{item.title} {item.release_year} • {getMediaCategoryLabel(item)}
))}
)} {/* Row 3: All Movies */} {(activeTab === "home" || activeTab === "movies") && (

{activeTab === "movies" ? "Все популярные фильмы" : "Фильмы"}

{allMoviesList.map((movie) => (
handleMediaCardClick(movie)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(movie); } }} >
{movie.title}
★ {movie.rating}
▶
{movie.title} {movie.release_year} • {movie.genre}
))}
)} {/* Row 4: All TV Series */} {(activeTab === "home" || activeTab === "series") && (

{activeTab === "series" ? "Все популярные сериалы" : "Сериалы"}

{allSeriesList.map((series) => (
handleMediaCardClick(series)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(series); } }} >
{series.title}
★ {series.rating}
▶
{series.title} {series.release_year} • {series.seasons_count || 1} сезонов
))}
)} {/* Row 5: All Anime */} {(activeTab === "home" || activeTab === "anime") && (

{activeTab === "anime" ? "Все популярные аниме" : "Аниме & Мультфильмы"}

{allAnimeList.map((anime) => (
handleMediaCardClick(anime)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(anime); } }} >
{anime.title}
★ {anime.rating}
▶
{anime.title} {anime.release_year} • {anime.genre}
))}
)} {/* Row 6: My Bookmarks List */} {activeTab === "bookmarks" && (

Мой список закладок

{bookmarkedItems.length === 0 ? (
В вашем списке пока нет сохраненных фильмов или сериалов. Нажмите на кнопку + на баннере любого тайтла, чтобы добавить его сюда.
) : (
{bookmarkedItems.map((item) => (
handleMediaCardClick(item)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handleMediaCardClick(item); } }} >
{item.title}
★ {item.rating}
▶
{item.title} {item.release_year} • {getMediaCategoryLabel(item)}
))}
)}
)}
)} {/* ======================================================== 4.5. DETAILED MOVIE & SERIES INFORMATION MODAL ======================================================== */} {detailsMedia && !playingEpisode && (
setDetailsMedia(null)} >
e.stopPropagation()} role="dialog" aria-modal="true" > {/* Background Backdrop with Gradient Fades */}
{detailsMedia.title}
{/* Top Hero Grid: Poster on Left, Main Info on Right */}
{detailsMedia.title}
★ {detailsMedia.rating} {detailsMedia.kinopoisk_rating && ( {detailsMedia.kinopoisk_rating} )} {getMediaBadgeLabel(detailsMedia)}
{getMediaCategoryLabel(detailsMedia)}

{detailsMedia.title}

{detailsMedia.original_title && detailsMedia.original_title !== detailsMedia.title && (

{detailsMedia.original_title}

)} {detailsMedia.tagline && (

«{detailsMedia.tagline}»

)} {/* Quick Stats Badges */}
📅 {detailsMedia.release_year} {detailsMedia.duration_minutes && detailsMedia.duration_minutes > 0 && ( ⏱ {formatRuntime(detailsMedia.duration_minutes)} )} {detailsMedia.seasons_count && detailsMedia.media_type !== "movie" && ( 📺 Сезонов: {detailsMedia.seasons_count} )} {detailsMedia.genre && ( 🎭 {detailsMedia.genre} )} {detailsMedia.countries && ( 🌍 {detailsMedia.countries} )}
{/* Financial & Production Specs Grid */}
Рейтинг: ★ {detailsMedia.rating} / 10
{detailsMedia.director && (
{detailsMedia.media_type === "movie" ? "Режиссер:" : "Создатели / Режиссер:"} {detailsMedia.director}
)} {detailsMedia.media_type === "movie" && detailsMedia.budget && detailsMedia.budget > 0 ? (
Бюджет фильма: 💰 {formatCurrency(detailsMedia.budget)}
) : null} {detailsMedia.media_type === "movie" && detailsMedia.revenue && detailsMedia.revenue > 0 ? (
Сборы в мире: 💵 {formatCurrency(detailsMedia.revenue)}
) : null} {detailsMedia.media_type === "movie" && !detailsMedia.budget && !detailsMedia.revenue && (
Формат: 🎬 Полнометражный фильм
)}
{detailsMedia.is_unreleased && (
⏳ Премьера ожидается: Фильм запланирован к выходу ( {detailsMedia.release_date_full || detailsMedia.release_year}). Цифровой плеер станет доступен сразу после официального цифрового релиза.
)} {/* Action Buttons: Play + Bookmark + Trailer */}
{detailsMedia.trailer_youtube_key && ( )} {isLoadingDetails && ( Загрузка данных... )}
{/* Description / Synopsis */}

{detailsMedia.media_type === "movie" ? "О фильме" : "О сериале"}

{detailsMedia.synopsis}

{/* Actors / Cast Carousel */} {detailsMedia.cast && detailsMedia.cast.length > 0 && (

В главных ролях (Актеры)

{detailsMedia.cast.map((actor) => (
{actor.profile_path ? ( {actor.name} ) : (
{actor.name.slice(0, 1).toUpperCase()}
)}
{actor.name}
{actor.character && (
{actor.character}
)}
))}
)} {/* ======================================================== IF TV SERIES OR ANIME: SHOW SEASONS & EPISODES IF MOVIE: ABSOLUTELY NO SEASONS OR EPISODES! ======================================================== */} {detailsMedia.media_type !== "movie" && detailsMedia.seasons.length > 0 && (

Сезоны и серии

{detailsMedia.seasons.map((season) => ( ))}
{((detailsMedia.seasons.find((s) => s.season_number === selectedSeason) || detailsMedia.seasons[0])?.episodes || []).map((ep) => (
handlePlayFromDetails(selectedSeason, ep)} data-tv-focusable="true" tabIndex={0} role="button" onKeyDown={(e) => { if (e.key === "Enter" || e.key === " " || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); handlePlayFromDetails(selectedSeason, ep); } }} >
{ep.title}
▶
{ep.number}. {ep.title}
{ep.duration_minutes && (
{ep.duration_minutes} мин
)}
))}
)}
)} {/* ======================================================== TRAILER MODAL ======================================================== */} {showTrailerModal && detailsMedia?.trailer_youtube_key && (
setShowTrailerModal(false)} >
e.stopPropagation()} role="dialog" aria-modal="true" >
Трейлер: {detailsMedia.title}