Merge branch 'dev' of https://github.com/frost768/movie-web into feat/subtitle-rendering

This commit is contained in:
frost768 2023-03-14 23:54:59 +03:00
commit f0c9103e0d
20 changed files with 3603 additions and 3465 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "movie-web", "name": "movie-web",
"version": "3.0.5", "version": "3.0.6",
"private": true, "private": true,
"homepage": "https://movie.squeezebox.dev", "homepage": "https://movie.squeezebox.dev",
"dependencies": { "dependencies": {
@ -65,7 +65,6 @@
"@types/pako": "^2.0.0", "@types/pako": "^2.0.0",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-helmet": "^6.1.6",
"@types/react-router": "^5.1.18", "@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-stickynode": "^4.0.0", "@types/react-stickynode": "^4.0.0",
@ -95,6 +94,7 @@
"vite-plugin-package-version": "^1.0.2", "vite-plugin-package-version": "^1.0.2",
"vite-plugin-pwa": "^0.14.4", "vite-plugin-pwa": "^0.14.4",
"vitest": "^0.28.5", "vitest": "^0.28.5",
"workbox-window": "^6.5.4" "workbox-window": "^6.5.4",
"@types/react-helmet": "^6.1.6"
} }
} }

View File

@ -10,6 +10,7 @@ registerEmbedScraper({
async getStream() { async getStream() {
// throw new Error("Oh well 2") // throw new Error("Oh well 2")
return { return {
embedId: "",
streamUrl: "", streamUrl: "",
quality: MWStreamQuality.Q1080P, quality: MWStreamQuality.Q1080P,
captions: [], captions: [],

View File

@ -3,7 +3,7 @@ import { registerEmbedScraper } from "@/backend/helpers/register";
import { import {
MWStreamQuality, MWStreamQuality,
MWStreamType, MWStreamType,
MWStream, MWEmbedStream,
} from "@/backend/helpers/streams"; } from "@/backend/helpers/streams";
import { proxiedFetch } from "@/backend/helpers/fetch"; import { proxiedFetch } from "@/backend/helpers/fetch";
@ -13,7 +13,7 @@ const URL_API = `${URL_BASE}/api`;
const URL_API_SOURCE = `${URL_API}/source`; const URL_API_SOURCE = `${URL_API}/source`;
async function scrape(embed: string) { async function scrape(embed: string) {
const sources: MWStream[] = []; const sources: MWEmbedStream[] = [];
const embedID = embed.split("/").pop(); const embedID = embed.split("/").pop();
@ -28,6 +28,7 @@ async function scrape(embed: string) {
for (const stream of streams) { for (const stream of streams) {
sources.push({ sources.push({
embedId: "",
streamUrl: stream.file as string, streamUrl: stream.file as string,
quality: stream.label as MWStreamQuality, quality: stream.label as MWStreamQuality,
type: stream.type as MWStreamType, type: stream.type as MWStreamType,

View File

@ -1,4 +1,4 @@
import { MWStream } from "./streams"; import { MWEmbedStream } from "./streams";
export enum MWEmbedType { export enum MWEmbedType {
M4UFREE = "m4ufree", M4UFREE = "m4ufree",
@ -23,5 +23,5 @@ export type MWEmbedScraper = {
rank: number; rank: number;
disabled?: boolean; disabled?: boolean;
getStream(ctx: MWEmbedContext): Promise<MWStream>; getStream(ctx: MWEmbedContext): Promise<MWEmbedStream>;
}; };

View File

@ -43,7 +43,13 @@ async function findBestEmbedStream(
providerId: string, providerId: string,
ctx: MWProviderRunContext ctx: MWProviderRunContext
): Promise<MWStream | null> { ): Promise<MWStream | null> {
if (result.stream) return result.stream; if (result.stream) {
return {
...result.stream,
providerId,
embedId: providerId,
};
}
let embedNum = 0; let embedNum = 0;
for (const embed of result.embeds) { for (const embed of result.embeds) {
@ -89,6 +95,7 @@ async function findBestEmbedStream(
type: "embed", type: "embed",
}); });
stream.providerId = providerId;
return stream; return stream;
} }

View File

@ -28,5 +28,11 @@ export type MWStream = {
streamUrl: string; streamUrl: string;
type: MWStreamType; type: MWStreamType;
quality: MWStreamQuality; quality: MWStreamQuality;
providerId?: string;
embedId?: string;
captions: MWCaption[]; captions: MWCaption[];
}; };
export type MWEmbedStream = MWStream & {
embedId: string;
};

View File

@ -55,7 +55,7 @@ registerProvider({
rank: 100, rank: 100,
type: [MWMediaType.MOVIE, MWMediaType.SERIES], type: [MWMediaType.MOVIE, MWMediaType.SERIES],
async scrape({ media, progress }) { async scrape({ media, episode, progress }) {
if (!this.type.includes(media.meta.type)) { if (!this.type.includes(media.meta.type)) {
throw new Error("Unsupported type"); throw new Error("Unsupported type");
} }
@ -97,10 +97,23 @@ registerProvider({
if (!mediaInfo.episodes) throw new Error("No watchable item found"); if (!mediaInfo.episodes) throw new Error("No watchable item found");
// get stream info from media // get stream info from media
progress(75); progress(75);
// By default we assume it is a movie
let episodeId: string | undefined = mediaInfo.episodes[0].id;
if (media.meta.type === MWMediaType.SERIES) {
const seasonNo = media.meta.seasonData.number;
const episodeNo = media.meta.seasonData.episodes.find(
(e) => e.id === episode
)?.number;
episodeId = mediaInfo.episodes.find(
(e: any) => e.season === seasonNo && e.number === episodeNo
)?.id;
}
if (!episodeId) throw new Error("No watchable item found");
const watchInfo = await proxiedFetch<any>("/watch", { const watchInfo = await proxiedFetch<any>("/watch", {
baseURL: flixHqBase, baseURL: flixHqBase,
params: { params: {
episodeId: mediaInfo.episodes[0].id, episodeId,
mediaId: flixId, mediaId: flixId,
}, },
}); });

View File

@ -1,5 +1,11 @@
import { Transition } from "@/components/Transition"; import { Transition } from "@/components/Transition";
import React, { ReactNode, useCallback, useEffect, useRef } from "react"; import React, {
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
interface Props { interface Props {
@ -10,6 +16,8 @@ interface Props {
} }
export function FloatingContainer(props: Props) { export function FloatingContainer(props: Props) {
const [portalElement, setPortalElement] = useState<Element | null>(null);
const ref = useRef<HTMLDivElement>(null);
const target = useRef<Element | null>(null); const target = useRef<Element | null>(null);
useEffect(() => { useEffect(() => {
@ -34,7 +42,15 @@ export function FloatingContainer(props: Props) {
[props] [props]
); );
return createPortal( useEffect(() => {
const element = ref.current?.closest(".popout-location");
setPortalElement(element ?? document.body);
}, []);
return (
<div ref={ref}>
{portalElement
? createPortal(
<Transition show={props.show} animation="none"> <Transition show={props.show} animation="none">
<div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none"> <div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none">
<Transition animation="fade" isChild> <Transition animation="fade" isChild>
@ -51,6 +67,9 @@ export function FloatingContainer(props: Props) {
</Transition> </Transition>
</div> </div>
</Transition>, </Transition>,
document.body portalElement
)
: null}
</div>
); );
} }

View File

@ -1,4 +1,4 @@
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb"; export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
export const GITHUB_LINK = "https://github.com/movie-web/movie-web"; export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
export const APP_VERSION = "3.0.5"; export const APP_VERSION = "3.0.6";
export const GA_ID = "G-44YVXRL61C"; export const GA_ID = "G-44YVXRL61C";

View File

@ -8,6 +8,7 @@ import {
useVideoPlayerDescriptor, useVideoPlayerDescriptor,
VideoPlayerContextProvider, VideoPlayerContextProvider,
} from "../state/hooks"; } from "../state/hooks";
import { MetaAction } from "./actions/MetaAction";
import { VideoElementInternal } from "./internal/VideoElementInternal"; import { VideoElementInternal } from "./internal/VideoElementInternal";
export interface VideoPlayerBaseProps { export interface VideoPlayerBaseProps {
@ -38,12 +39,13 @@ function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) {
<div <div
ref={ref} ref={ref}
className={[ className={[
"is-video-player relative h-full w-full select-none overflow-hidden bg-black", "is-video-player popout-location relative h-full w-full select-none overflow-hidden bg-black",
props.includeSafeArea || videoInterface.isFullscreen props.includeSafeArea || videoInterface.isFullscreen
? "[border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]" ? "[border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]"
: "", : "",
].join(" ")} ].join(" ")}
> >
<MetaAction />
<VideoElementInternal autoPlay={props.autoPlay} /> <VideoElementInternal autoPlay={props.autoPlay} />
<CastingInternal /> <CastingInternal />
<WrapperRegisterInternal wrapper={ref.current} /> <WrapperRegisterInternal wrapper={ref.current} />

View File

@ -0,0 +1,59 @@
import { MWCaption } from "@/backend/helpers/streams";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useMeta } from "@/video/state/logic/meta";
import { useProgress } from "@/video/state/logic/progress";
import { useEffect } from "react";
export type WindowMeta = {
meta: DetailedMeta;
captions: MWCaption[];
episode?: {
episodeId: string;
seasonId: string;
};
seasons?: {
id: string;
number: number;
title: string;
episodes?: { id: string; number: number; title: string }[];
}[];
progress: {
time: number;
duration: number;
};
} | null;
declare global {
interface Window {
meta?: Record<string, WindowMeta>;
}
}
export function MetaAction() {
const descriptor = useVideoPlayerDescriptor();
const meta = useMeta(descriptor);
const progress = useProgress(descriptor);
useEffect(() => {
if (!window.meta) window.meta = {};
if (meta) {
window.meta[descriptor] = {
meta: meta.meta,
captions: meta.captions,
seasons: meta.seasons,
episode: meta.episode,
progress: {
time: progress.time,
duration: progress.duration,
},
};
}
return () => {
if (window.meta) delete window.meta[descriptor];
};
}, [meta, descriptor, progress]);
return null;
}

View File

@ -8,6 +8,8 @@ interface SourceControllerProps {
source: string; source: string;
type: MWStreamType; type: MWStreamType;
quality: MWStreamQuality; quality: MWStreamQuality;
providerId?: string;
embedId?: string;
} }
export function SourceController(props: SourceControllerProps) { export function SourceController(props: SourceControllerProps) {

View File

@ -18,12 +18,14 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
import { FloatingCardView } from "@/components/popout/FloatingCard"; import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView"; import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter"; import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { useSource } from "@/video/state/logic/source";
import { PopoutListEntry } from "./PopoutUtils"; import { PopoutListEntry } from "./PopoutUtils";
interface EmbedEntryProps { interface EmbedEntryProps {
name: string; name: string;
type: MWEmbedType; type: MWEmbedType;
url: string; url: string;
active: boolean;
onSelect: (stream: MWStream) => void; onSelect: (stream: MWStream) => void;
} }
@ -43,6 +45,7 @@ export function EmbedEntry(props: EmbedEntryProps) {
isOnDarkBackground isOnDarkBackground
loading={loading} loading={loading}
errored={!!error} errored={!!error}
active={props.active}
onClick={() => { onClick={() => {
scrapeEmbed(); scrapeEmbed();
}} }}
@ -61,6 +64,9 @@ export function SourceSelectionPopout(props: {
const descriptor = useVideoPlayerDescriptor(); const descriptor = useVideoPlayerDescriptor();
const controls = useControls(descriptor); const controls = useControls(descriptor);
const meta = useMeta(descriptor); const meta = useMeta(descriptor);
const { source } = useSource(descriptor);
const providerRef = useRef<string | null>(null);
const providers = useMemo( const providers = useMemo(
() => () =>
meta meta
@ -96,6 +102,8 @@ export function SourceSelectionPopout(props: {
quality: stream.quality, quality: stream.quality,
source: stream.streamUrl, source: stream.streamUrl,
type: stream.type, type: stream.type,
embedId: stream.embedId,
providerId: providerRef.current ?? undefined,
}); });
if (meta) { if (meta) {
controls.setMeta({ controls.setMeta({
@ -106,7 +114,6 @@ export function SourceSelectionPopout(props: {
controls.closePopout(); controls.closePopout();
} }
const providerRef = useRef<string | null>(null);
const selectProvider = (providerId?: string) => { const selectProvider = (providerId?: string) => {
if (!providerId) { if (!providerId) {
providerRef.current = null; providerRef.current = null;
@ -188,6 +195,7 @@ export function SourceSelectionPopout(props: {
{providers.map((v) => ( {providers.map((v) => (
<PopoutListEntry <PopoutListEntry
key={v.id} key={v.id}
active={v.id === source?.providerId}
onClick={() => { onClick={() => {
selectProvider(v.id); selectProvider(v.id);
}} }}
@ -234,6 +242,10 @@ export function SourceSelectionPopout(props: {
onClick={() => { onClick={() => {
if (scrapeResult.stream) selectSource(scrapeResult.stream); if (scrapeResult.stream) selectSource(scrapeResult.stream);
}} }}
active={
selectedProviderPopulated?.id === source?.providerId &&
selectedProviderPopulated?.id === source?.embedId
}
> >
Native source Native source
</PopoutListEntry> </PopoutListEntry>
@ -245,6 +257,7 @@ export function SourceSelectionPopout(props: {
name={v.displayName ?? ""} name={v.displayName ?? ""}
key={v.url} key={v.url}
url={v.url} url={v.url}
active={false} // TODO add embed id extractor
onSelect={(stream) => { onSelect={(stream) => {
selectSource(stream); selectSource(stream);
}} }}

View File

@ -9,6 +9,8 @@ export type VideoSourceEvent = {
quality: MWStreamQuality; quality: MWStreamQuality;
url: string; url: string;
type: MWStreamType; type: MWStreamType;
providerId?: string;
embedId?: string;
caption: null | { caption: null | {
id: string; id: string;
url: string; url: string;

View File

@ -133,6 +133,8 @@ export function createCastingStateProvider(
type: source.type, type: source.type,
url: source.source, url: source.source,
caption: null, caption: null,
embedId: source.embedId,
providerId: source.providerId,
}; };
resetStateForSource(descriptor, state); resetStateForSource(descriptor, state);
updateSource(descriptor, state); updateSource(descriptor, state);
@ -224,6 +226,8 @@ export function createCastingStateProvider(
quality: state.source.quality, quality: state.source.quality,
source: state.source.url, source: state.source.url,
type: state.source.type, type: state.source.type,
embedId: state.source.embedId,
providerId: state.source.providerId,
}); });
return { return {

View File

@ -4,6 +4,8 @@ type VideoPlayerSource = {
source: string; source: string;
type: MWStreamType; type: MWStreamType;
quality: MWStreamQuality; quality: MWStreamQuality;
providerId?: string;
embedId?: string;
} | null; } | null;
export type VideoPlayerStateController = { export type VideoPlayerStateController = {

View File

@ -189,6 +189,8 @@ export function createVideoStateProvider(
type: source.type, type: source.type,
url: source.source, url: source.source,
caption: null, caption: null,
embedId: source.embedId,
providerId: source.providerId,
}; };
updateSource(descriptor, state); updateSource(descriptor, state);
}, },
@ -334,6 +336,8 @@ export function createVideoStateProvider(
quality: state.source.quality, quality: state.source.quality,
source: state.source.url, source: state.source.url,
type: state.source.type, type: state.source.type,
embedId: state.source.embedId,
providerId: state.source.providerId,
}); });
return { return {

View File

@ -58,6 +58,8 @@ export type VideoPlayerState = {
quality: MWStreamQuality; quality: MWStreamQuality;
url: string; url: string;
type: MWStreamType; type: MWStreamType;
providerId?: string;
embedId?: string;
caption: null | { caption: null | {
url: string; url: string;
id: string; id: string;

View File

@ -146,6 +146,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
source={props.stream.streamUrl} source={props.stream.streamUrl}
type={props.stream.type} type={props.stream.type}
quality={props.stream.quality} quality={props.stream.quality}
embedId={props.stream.embedId}
providerId={props.stream.providerId}
/> />
<ProgressListenerController <ProgressListenerController
startAt={firstStartTime.current} startAt={firstStartTime.current}
@ -181,6 +183,7 @@ export function MediaView() {
return getMetaFromId(data.type, data.id, seasonId); return getMetaFromId(data.type, data.id, seasonId);
} }
); );
// TODO get stream from someplace that actually gets updated
const [stream, setStream] = useState<MWStream | null>(null); const [stream, setStream] = useState<MWStream | null>(null);
const lastSearchValue = useRef<(string | undefined)[] | null>(null); const lastSearchValue = useRef<(string | undefined)[] | null>(null);

6864
yarn.lock

File diff suppressed because it is too large Load Diff