From c7ff62da6fd8abf7a0073935e3d8c52e2c7bf453 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Fri, 10 Oct 2025 23:17:00 -0600 Subject: [PATCH] EMBED --- src/backend/metadata/getmeta.ts | 83 +++++++++- src/backend/metadata/tmdb.ts | 32 ++++ src/components/player/atoms/Episodes.tsx | 16 +- .../player/atoms/NextEpisodeButton.tsx | 28 ++++ src/components/player/atoms/Settings.tsx | 36 ++++- src/components/player/atoms/Time.tsx | 63 ++++++++ .../player/atoms/settings/FedApiSetupView.tsx | 148 ++++++++++++++++++ .../player/atoms/settings/LanguageView.tsx | 79 ++++++++++ .../atoms/settings/RealDebridSetupView.tsx | 128 +++++++++++++++ .../player/atoms/settings/SettingsMenu.tsx | 74 ++++++--- .../atoms/settings/SourceSelectingView.tsx | 39 +++-- .../player/atoms/settings/ThemeView.tsx | 141 +++++++++++++++++ src/components/player/base/BackLink.tsx | 36 ++++- src/components/player/hooks/useCaptions.ts | 3 +- src/index.tsx | 16 +- src/pages/PlayerView.tsx | 2 +- src/pages/onboarding/utils.tsx | 33 ++++ src/pages/parts/errors/NotFoundPart.tsx | 24 +-- src/pages/parts/player/MetaPart.tsx | 32 ---- src/pages/parts/player/PlaybackErrorPart.tsx | 34 +++- src/pages/parts/player/PlayerPart.tsx | 75 ++++++--- src/pages/parts/player/ScrapeErrorPart.tsx | 34 +++- src/pages/parts/player/ScrapingPart.tsx | 16 +- src/setup/App.tsx | 139 +++++++++------- src/utils/language.ts | 22 ++- 25 files changed, 1137 insertions(+), 196 deletions(-) create mode 100644 src/components/player/atoms/settings/FedApiSetupView.tsx create mode 100644 src/components/player/atoms/settings/LanguageView.tsx create mode 100644 src/components/player/atoms/settings/RealDebridSetupView.tsx create mode 100644 src/components/player/atoms/settings/ThemeView.tsx diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index a208bd1f..fbd03570 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -12,6 +12,7 @@ import { getMediaDetails, getMediaPoster, getMovieFromExternalId, + getNumberedIDs, mediaTypeToTMDB, } from "./tmdb"; import { @@ -194,11 +195,16 @@ export async function convertLegacyUrl( if (isLegacyMediaType(url)) { const details = await getMediaDetails(id, TMDBContentTypes.TV); - return `/media/${TMDBIdToUrlId( + const newPath = `/media/${TMDBIdToUrlId( MWMediaType.SERIES, details.id.toString(), details.name, )}${suffix}`; + // Preserve query parameters + const searchParams = new URLSearchParams(window.location.search); + return searchParams.toString() + ? `${newPath}?${searchParams.toString()}` + : newPath; } const mediaType = TMDBMediaToMediaType(type as TMDBContentTypes); @@ -212,11 +218,82 @@ export async function convertLegacyUrl( if (imdbId && mediaType === MWMediaType.MOVIE) { const movieId = await getMovieFromExternalId(imdbId); if (movieId) { - return `/media/${TMDBIdToUrlId(mediaType, movieId, meta.meta.title)}`; + const newPath = `/media/${TMDBIdToUrlId(mediaType, movieId, meta.meta.title)}`; + // Preserve query parameters + const searchParams = new URLSearchParams(window.location.search); + return searchParams.toString() + ? `${newPath}?${searchParams.toString()}` + : newPath; } if (tmdbId) { - return `/media/${TMDBIdToUrlId(mediaType, tmdbId, meta.meta.title)}`; + const newPath = `/media/${TMDBIdToUrlId(mediaType, tmdbId, meta.meta.title)}`; + // Preserve query parameters + const searchParams = new URLSearchParams(window.location.search); + return searchParams.toString() + ? `${newPath}?${searchParams.toString()}` + : newPath; } } } + +export function isEmbedUrl(url: string): boolean { + if (url.startsWith("/embed/")) return true; + return false; +} + +export function isTVEmbedMediaType(url: string): boolean { + if (url.startsWith("/embed/tmdb-tv")) return true; + return false; +} + +export async function convertEmbedUrl( + url: string, +): Promise { + const urlParts = url.split("/").slice(2); + + const [, type, id] = urlParts[0].split("-", 3); + const suffix = urlParts + .slice(1) + .map((v) => `/${v}`) + .join(""); + + if (!id) return undefined; + + if (type === "tv") { + const suffixParts = suffix.split("/").slice(1); + const suffixIDs = await getNumberedIDs( + id, + suffixParts[0], + suffixParts[1], + ).then((v) => (v ? `${v.season}/${v.episode}` : "")); + + const details = await getMediaDetails(id, TMDBContentTypes.TV); + const newPath = `/media/${TMDBIdToUrlId( + MWMediaType.SERIES, + details.id.toString(), + details.name, + )}/${suffixIDs}`; + // Preserve query parameters + const searchParams = new URLSearchParams(window.location.search); + return searchParams.toString() + ? `${newPath}?${searchParams.toString()}` + : newPath; + } + + if (type === "movie") { + const details = await getMediaDetails(id, TMDBContentTypes.MOVIE); + const newPath = `/media/${TMDBIdToUrlId( + MWMediaType.MOVIE, + details.id.toString(), + details.title, + )}`; + // Preserve query parameters + const searchParams = new URLSearchParams(window.location.search); + return searchParams.toString() + ? `${newPath}?${searchParams.toString()}` + : newPath; + } + + return undefined; +} diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 92628fe2..68e2b756 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -427,6 +427,38 @@ export function formatTMDBSearchResult( }; } +export async function getNumberedIDs( + id: string, + seasonNumber: string, + episodeNumber: string, +): Promise<{ season: string; episode: string } | undefined> { + const season = parseInt(seasonNumber, 10); + const episode = parseInt(episodeNumber, 10); + if (Number.isNaN(season) || Number.isNaN(episode)) { + return undefined; + } + + const seasonData = await get(`/tv/${id}/season/${season}`); + if (!seasonData || !seasonData.episodes) { + return undefined; + } + + const targetEpisode = seasonData.episodes.find( + (e) => e.episode_number === episode, + ); + + if (!targetEpisode) { + // eslint-disable-next-line no-console + console.log("Episode not found"); + return undefined; + } + + return { + season: seasonData.id.toString(), + episode: targetEpisode.id.toString(), + }; +} + /** * Fetches the clear logo for a movie or show from TMDB images endpoint. */ diff --git a/src/components/player/atoms/Episodes.tsx b/src/components/player/atoms/Episodes.tsx index fa8ff140..5335da1d 100644 --- a/src/components/player/atoms/Episodes.tsx +++ b/src/components/player/atoms/Episodes.tsx @@ -681,15 +681,29 @@ export function EpisodesView({ const playEpisode = useCallback( (episodeId: string) => { + const oldMetaCopy = { ...meta }; if (loadingState.value) { const newData = setPlayerMeta(loadingState.value.fullData, episodeId); + window.parent.postMessage( + { + type: "episodeChanged", + episodeNumber: newData?.episode?.number, + seasonNumber: newData?.season?.number, + tmdbId: oldMetaCopy?.tmdbId, + imdbId: oldMetaCopy?.imdbId, + oldEpisodeNumber: oldMetaCopy?.episode?.number, + oldSeasonNumber: oldMetaCopy?.season?.number, + }, + "*", + ); if (newData) onChange?.(newData); } + // prevent router clear here, otherwise its done double // player already switches route after meta change router.close(true); }, - [setPlayerMeta, loadingState, router, onChange], + [setPlayerMeta, loadingState, router, onChange, meta], ); const toggleWatchStatus = useCallback( diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx index 86e082c5..0bc3504c 100644 --- a/src/components/player/atoms/NextEpisodeButton.tsx +++ b/src/components/player/atoms/NextEpisodeButton.tsx @@ -159,6 +159,21 @@ export function NextEpisodeButton(props: { setShouldStartFromBeginning(true); setDirectMeta(metaCopy); props.onChange?.(metaCopy); + + // Send message to parent window about episode change + window.parent.postMessage( + { + type: "episodeChanged", + episodeNumber: nextEp.number, + seasonNumber: metaCopy.season?.number, + oldEpisodeNumber: meta?.episode?.number, + oldSeasonNumber: meta?.season?.number, + title: nextEp.title, + tmdbId: nextEp.tmdbId, + }, + "*", + ); + const defaultProgress = { duration: 0, watched: 0 }; updateItem({ meta: metaCopy, @@ -181,6 +196,19 @@ export function NextEpisodeButton(props: { setShouldStartFromBeginning(true); setDirectMeta(metaCopy); props.onChange?.(metaCopy); + + // Send message to parent window about episode restart + window.parent.postMessage( + { + type: "episodeRestarted", + episodeNumber: meta.episode.number, + seasonNumber: meta.season?.number, + title: meta.episode.title, + tmdbId: meta.episode.tmdbId, + }, + "*", + ); + const defaultProgress = { duration: 0, watched: 0 }; updateItem({ meta: metaCopy, diff --git a/src/components/player/atoms/Settings.tsx b/src/components/player/atoms/Settings.tsx index 93cbccb9..ddc4b43c 100644 --- a/src/components/player/atoms/Settings.tsx +++ b/src/components/player/atoms/Settings.tsx @@ -18,11 +18,15 @@ import { AudioView } from "./settings/AudioView"; import { CaptionSettingsView } from "./settings/CaptionSettingsView"; import { CaptionsView } from "./settings/CaptionsView"; import { DownloadRoutes } from "./settings/Downloads"; +import { FedApiSetupView } from "./settings/FedApiSetupView"; +import { LanguageView } from "./settings/LanguageView"; import { OpenSubtitlesCaptionView } from "./settings/OpensubtitlesCaptionsView"; import { PlaybackSettingsView } from "./settings/PlaybackSettingsView"; import { QualityView } from "./settings/QualityView"; +import { RealDebridSetupView } from "./settings/RealDebridSetupView"; import { SettingsMenu } from "./settings/SettingsMenu"; import SourceCaptionsView from "./settings/SourceCaptionsView"; +import { ThemeView } from "./settings/ThemeView"; import { WatchPartyView } from "./settings/WatchPartyView"; function SettingsOverlay({ id }: { id: string }) { @@ -129,11 +133,41 @@ function SettingsOverlay({ id }: { id: string }) { - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/player/atoms/Time.tsx b/src/components/player/atoms/Time.tsx index cc48031c..ce94195e 100644 --- a/src/components/player/atoms/Time.tsx +++ b/src/components/player/atoms/Time.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { VideoPlayerButton } from "@/components/player/internals/Button"; @@ -15,10 +16,72 @@ export function Time(props: { short?: boolean }) { time, draggingTime, } = usePlayerStore((s) => s.progress); + const meta = usePlayerStore((s) => s.meta); const { isSeeking } = usePlayerStore((s) => s.interface); const { t } = useTranslation(); const hasHours = durationExceedsHour(timeDuration); + // Use refs to store the last update time and timeout ID + const lastUpdateRef = useRef(0); + const timeoutRef = useRef(null); + + // Send current time via postMessage with throttling + useEffect(() => { + const currentTime = Math.min( + Math.max(isSeeking ? draggingTime : time, 0), + timeDuration, + ); + + const now = Date.now(); + const timeSinceLastUpdate = now - lastUpdateRef.current; + const THROTTLE_MS = 1000; // 1 second + + // Clear any existing timeout + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + + // If enough time has passed, send update immediately + if (timeSinceLastUpdate >= THROTTLE_MS) { + window.parent.postMessage( + { + type: "playerTimeUpdate", + time: currentTime, + duration: timeDuration, + tmdbId: meta?.tmdbId, + imdbId: meta?.imdbId, + }, + "*", + ); + lastUpdateRef.current = now; + } else { + // Otherwise, schedule an update + timeoutRef.current = window.setTimeout(() => { + window.parent.postMessage( + { + type: "playerTimeUpdate", + time: currentTime, + duration: timeDuration, + tmdbId: meta?.tmdbId, + imdbId: meta?.imdbId, + }, + "*", + ); + lastUpdateRef.current = Date.now(); + timeoutRef.current = null; + }, THROTTLE_MS - timeSinceLastUpdate); + } + + // Cleanup function to clear any pending timeout + return () => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, [time, draggingTime, isSeeking, timeDuration, meta]); + function toggleMode() { setTimeFormat( timeFormat === VideoPlayerTimeFormat.REGULAR diff --git a/src/components/player/atoms/settings/FedApiSetupView.tsx b/src/components/player/atoms/settings/FedApiSetupView.tsx new file mode 100644 index 00000000..f9efbed8 --- /dev/null +++ b/src/components/player/atoms/settings/FedApiSetupView.tsx @@ -0,0 +1,148 @@ +import { useEffect, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; + +import { Button } from "@/components/buttons/Button"; +import { Icon, Icons } from "@/components/Icon"; +import { Menu } from "@/components/player/internals/ContextMenu"; +import { + StatusCircle, + StatusCircleProps, +} from "@/components/player/internals/StatusCircle"; +import { MwLink } from "@/components/text/Link"; +import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; +import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { Status, testFebboxKey } from "@/pages/parts/settings/SetupPart"; +import { usePreferencesStore } from "@/stores/preferences"; + +async function getFebboxKeyStatus(febboxKey: string | null) { + if (febboxKey) { + const status: Status = await testFebboxKey(febboxKey); + return status; + } + return "unset"; +} + +export function FedApiSetupView({ id }: { id: string }) { + const { t } = useTranslation(); + const router = useOverlayRouter(id); + const febboxKey = usePreferencesStore((s) => s.febboxKey); + const setFebboxKey = usePreferencesStore((s) => s.setFebboxKey); + const [showVideo, setShowVideo] = useState(false); + const [status, setStatus] = useState("unset"); + const statusMap: Record = { + error: "error", + success: "success", + unset: "noresult", + api_down: "error", + invalid_token: "error", + }; + + useEffect(() => { + const checkKeyStatus = async () => { + const result = await getFebboxKeyStatus(febboxKey); + setStatus(result); + }; + checkKeyStatus(); + }, [febboxKey]); + + const handleReload = () => { + window.location.reload(); + }; + + const alertRef = useRef(null); + + useEffect(() => { + if ((status === "success" || status === "error") && alertRef.current) { + alertRef.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + }, [status]); + + return ( + <> + router.navigate("/source")}> + Febbox API Setup + + +
+

+ + To get your UI Key: +
+

setShowVideo(!showVideo)} + className="flex items-center justify-between p-1 px-2 my-2 w-fit border border-type-secondary rounded-lg cursor-pointer text-type-secondary hover:text-white transition-colors duration-200" + > + + {showVideo ? "Hide Video Tutorial" : "Show Video Tutorial"} + + {showVideo ? ( + + ) : ( + + )} +
+ {showVideo && ( + <> +
+