mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 05:11:52 +01:00
Merge branch 'dev' of https://github.com/frost768/movie-web into feat/subtitle-rendering
This commit is contained in:
commit
f0c9103e0d
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "movie-web",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.6",
|
||||
"private": true,
|
||||
"homepage": "https://movie.squeezebox.dev",
|
||||
"dependencies": {
|
||||
@ -65,7 +65,6 @@
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/react": "^17.0.39",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-stickynode": "^4.0.0",
|
||||
@ -95,6 +94,7 @@
|
||||
"vite-plugin-package-version": "^1.0.2",
|
||||
"vite-plugin-pwa": "^0.14.4",
|
||||
"vitest": "^0.28.5",
|
||||
"workbox-window": "^6.5.4"
|
||||
"workbox-window": "^6.5.4",
|
||||
"@types/react-helmet": "^6.1.6"
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ registerEmbedScraper({
|
||||
async getStream() {
|
||||
// throw new Error("Oh well 2")
|
||||
return {
|
||||
embedId: "",
|
||||
streamUrl: "",
|
||||
quality: MWStreamQuality.Q1080P,
|
||||
captions: [],
|
||||
|
@ -3,7 +3,7 @@ import { registerEmbedScraper } from "@/backend/helpers/register";
|
||||
import {
|
||||
MWStreamQuality,
|
||||
MWStreamType,
|
||||
MWStream,
|
||||
MWEmbedStream,
|
||||
} from "@/backend/helpers/streams";
|
||||
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||
|
||||
@ -13,7 +13,7 @@ const URL_API = `${URL_BASE}/api`;
|
||||
const URL_API_SOURCE = `${URL_API}/source`;
|
||||
|
||||
async function scrape(embed: string) {
|
||||
const sources: MWStream[] = [];
|
||||
const sources: MWEmbedStream[] = [];
|
||||
|
||||
const embedID = embed.split("/").pop();
|
||||
|
||||
@ -28,6 +28,7 @@ async function scrape(embed: string) {
|
||||
|
||||
for (const stream of streams) {
|
||||
sources.push({
|
||||
embedId: "",
|
||||
streamUrl: stream.file as string,
|
||||
quality: stream.label as MWStreamQuality,
|
||||
type: stream.type as MWStreamType,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MWStream } from "./streams";
|
||||
import { MWEmbedStream } from "./streams";
|
||||
|
||||
export enum MWEmbedType {
|
||||
M4UFREE = "m4ufree",
|
||||
@ -23,5 +23,5 @@ export type MWEmbedScraper = {
|
||||
rank: number;
|
||||
disabled?: boolean;
|
||||
|
||||
getStream(ctx: MWEmbedContext): Promise<MWStream>;
|
||||
getStream(ctx: MWEmbedContext): Promise<MWEmbedStream>;
|
||||
};
|
||||
|
@ -43,7 +43,13 @@ async function findBestEmbedStream(
|
||||
providerId: string,
|
||||
ctx: MWProviderRunContext
|
||||
): Promise<MWStream | null> {
|
||||
if (result.stream) return result.stream;
|
||||
if (result.stream) {
|
||||
return {
|
||||
...result.stream,
|
||||
providerId,
|
||||
embedId: providerId,
|
||||
};
|
||||
}
|
||||
|
||||
let embedNum = 0;
|
||||
for (const embed of result.embeds) {
|
||||
@ -89,6 +95,7 @@ async function findBestEmbedStream(
|
||||
type: "embed",
|
||||
});
|
||||
|
||||
stream.providerId = providerId;
|
||||
return stream;
|
||||
}
|
||||
|
||||
|
@ -28,5 +28,11 @@ export type MWStream = {
|
||||
streamUrl: string;
|
||||
type: MWStreamType;
|
||||
quality: MWStreamQuality;
|
||||
providerId?: string;
|
||||
embedId?: string;
|
||||
captions: MWCaption[];
|
||||
};
|
||||
|
||||
export type MWEmbedStream = MWStream & {
|
||||
embedId: string;
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ registerProvider({
|
||||
rank: 100,
|
||||
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||
|
||||
async scrape({ media, progress }) {
|
||||
async scrape({ media, episode, progress }) {
|
||||
if (!this.type.includes(media.meta.type)) {
|
||||
throw new Error("Unsupported type");
|
||||
}
|
||||
@ -97,10 +97,23 @@ registerProvider({
|
||||
if (!mediaInfo.episodes) throw new Error("No watchable item found");
|
||||
// get stream info from media
|
||||
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", {
|
||||
baseURL: flixHqBase,
|
||||
params: {
|
||||
episodeId: mediaInfo.episodes[0].id,
|
||||
episodeId,
|
||||
mediaId: flixId,
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,11 @@
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
@ -10,6 +16,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FloatingContainer(props: Props) {
|
||||
const [portalElement, setPortalElement] = useState<Element | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const target = useRef<Element | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -34,23 +42,34 @@ export function FloatingContainer(props: Props) {
|
||||
[props]
|
||||
);
|
||||
|
||||
return createPortal(
|
||||
<Transition show={props.show} animation="none">
|
||||
<div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none">
|
||||
<Transition animation="fade" isChild>
|
||||
<div
|
||||
onClick={click}
|
||||
className={[
|
||||
"absolute inset-0",
|
||||
props.darken ? "bg-black opacity-90" : "",
|
||||
].join(" ")}
|
||||
/>
|
||||
</Transition>
|
||||
<Transition animation="slide-up" className="h-0" isChild>
|
||||
{props.children}
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>,
|
||||
document.body
|
||||
useEffect(() => {
|
||||
const element = ref.current?.closest(".popout-location");
|
||||
setPortalElement(element ?? document.body);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{portalElement
|
||||
? createPortal(
|
||||
<Transition show={props.show} animation="none">
|
||||
<div className="popout-wrapper pointer-events-auto fixed inset-0 z-[999] select-none">
|
||||
<Transition animation="fade" isChild>
|
||||
<div
|
||||
onClick={click}
|
||||
className={[
|
||||
"absolute inset-0",
|
||||
props.darken ? "bg-black opacity-90" : "",
|
||||
].join(" ")}
|
||||
/>
|
||||
</Transition>
|
||||
<Transition animation="slide-up" className="h-0" isChild>
|
||||
{props.children}
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>,
|
||||
portalElement
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
|
||||
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";
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
useVideoPlayerDescriptor,
|
||||
VideoPlayerContextProvider,
|
||||
} from "../state/hooks";
|
||||
import { MetaAction } from "./actions/MetaAction";
|
||||
import { VideoElementInternal } from "./internal/VideoElementInternal";
|
||||
|
||||
export interface VideoPlayerBaseProps {
|
||||
@ -38,12 +39,13 @@ function VideoPlayerBaseWithState(props: VideoPlayerBaseProps) {
|
||||
<div
|
||||
ref={ref}
|
||||
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
|
||||
? "[border-left:env(safe-area-inset-left)_solid_transparent] [border-right:env(safe-area-inset-right)_solid_transparent]"
|
||||
: "",
|
||||
].join(" ")}
|
||||
>
|
||||
<MetaAction />
|
||||
<VideoElementInternal autoPlay={props.autoPlay} />
|
||||
<CastingInternal />
|
||||
<WrapperRegisterInternal wrapper={ref.current} />
|
||||
|
59
src/video/components/actions/MetaAction.tsx
Normal file
59
src/video/components/actions/MetaAction.tsx
Normal 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;
|
||||
}
|
@ -8,6 +8,8 @@ interface SourceControllerProps {
|
||||
source: string;
|
||||
type: MWStreamType;
|
||||
quality: MWStreamQuality;
|
||||
providerId?: string;
|
||||
embedId?: string;
|
||||
}
|
||||
|
||||
export function SourceController(props: SourceControllerProps) {
|
||||
|
@ -18,12 +18,14 @@ import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
||||
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||
import { FloatingView } from "@/components/popout/FloatingView";
|
||||
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||
import { useSource } from "@/video/state/logic/source";
|
||||
import { PopoutListEntry } from "./PopoutUtils";
|
||||
|
||||
interface EmbedEntryProps {
|
||||
name: string;
|
||||
type: MWEmbedType;
|
||||
url: string;
|
||||
active: boolean;
|
||||
onSelect: (stream: MWStream) => void;
|
||||
}
|
||||
|
||||
@ -43,6 +45,7 @@ export function EmbedEntry(props: EmbedEntryProps) {
|
||||
isOnDarkBackground
|
||||
loading={loading}
|
||||
errored={!!error}
|
||||
active={props.active}
|
||||
onClick={() => {
|
||||
scrapeEmbed();
|
||||
}}
|
||||
@ -61,6 +64,9 @@ export function SourceSelectionPopout(props: {
|
||||
const descriptor = useVideoPlayerDescriptor();
|
||||
const controls = useControls(descriptor);
|
||||
const meta = useMeta(descriptor);
|
||||
const { source } = useSource(descriptor);
|
||||
const providerRef = useRef<string | null>(null);
|
||||
|
||||
const providers = useMemo(
|
||||
() =>
|
||||
meta
|
||||
@ -96,6 +102,8 @@ export function SourceSelectionPopout(props: {
|
||||
quality: stream.quality,
|
||||
source: stream.streamUrl,
|
||||
type: stream.type,
|
||||
embedId: stream.embedId,
|
||||
providerId: providerRef.current ?? undefined,
|
||||
});
|
||||
if (meta) {
|
||||
controls.setMeta({
|
||||
@ -106,7 +114,6 @@ export function SourceSelectionPopout(props: {
|
||||
controls.closePopout();
|
||||
}
|
||||
|
||||
const providerRef = useRef<string | null>(null);
|
||||
const selectProvider = (providerId?: string) => {
|
||||
if (!providerId) {
|
||||
providerRef.current = null;
|
||||
@ -188,6 +195,7 @@ export function SourceSelectionPopout(props: {
|
||||
{providers.map((v) => (
|
||||
<PopoutListEntry
|
||||
key={v.id}
|
||||
active={v.id === source?.providerId}
|
||||
onClick={() => {
|
||||
selectProvider(v.id);
|
||||
}}
|
||||
@ -234,6 +242,10 @@ export function SourceSelectionPopout(props: {
|
||||
onClick={() => {
|
||||
if (scrapeResult.stream) selectSource(scrapeResult.stream);
|
||||
}}
|
||||
active={
|
||||
selectedProviderPopulated?.id === source?.providerId &&
|
||||
selectedProviderPopulated?.id === source?.embedId
|
||||
}
|
||||
>
|
||||
Native source
|
||||
</PopoutListEntry>
|
||||
@ -245,6 +257,7 @@ export function SourceSelectionPopout(props: {
|
||||
name={v.displayName ?? ""}
|
||||
key={v.url}
|
||||
url={v.url}
|
||||
active={false} // TODO add embed id extractor
|
||||
onSelect={(stream) => {
|
||||
selectSource(stream);
|
||||
}}
|
||||
|
@ -9,6 +9,8 @@ export type VideoSourceEvent = {
|
||||
quality: MWStreamQuality;
|
||||
url: string;
|
||||
type: MWStreamType;
|
||||
providerId?: string;
|
||||
embedId?: string;
|
||||
caption: null | {
|
||||
id: string;
|
||||
url: string;
|
||||
|
@ -133,6 +133,8 @@ export function createCastingStateProvider(
|
||||
type: source.type,
|
||||
url: source.source,
|
||||
caption: null,
|
||||
embedId: source.embedId,
|
||||
providerId: source.providerId,
|
||||
};
|
||||
resetStateForSource(descriptor, state);
|
||||
updateSource(descriptor, state);
|
||||
@ -224,6 +226,8 @@ export function createCastingStateProvider(
|
||||
quality: state.source.quality,
|
||||
source: state.source.url,
|
||||
type: state.source.type,
|
||||
embedId: state.source.embedId,
|
||||
providerId: state.source.providerId,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -4,6 +4,8 @@ type VideoPlayerSource = {
|
||||
source: string;
|
||||
type: MWStreamType;
|
||||
quality: MWStreamQuality;
|
||||
providerId?: string;
|
||||
embedId?: string;
|
||||
} | null;
|
||||
|
||||
export type VideoPlayerStateController = {
|
||||
|
@ -189,6 +189,8 @@ export function createVideoStateProvider(
|
||||
type: source.type,
|
||||
url: source.source,
|
||||
caption: null,
|
||||
embedId: source.embedId,
|
||||
providerId: source.providerId,
|
||||
};
|
||||
updateSource(descriptor, state);
|
||||
},
|
||||
@ -334,6 +336,8 @@ export function createVideoStateProvider(
|
||||
quality: state.source.quality,
|
||||
source: state.source.url,
|
||||
type: state.source.type,
|
||||
embedId: state.source.embedId,
|
||||
providerId: state.source.providerId,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -58,6 +58,8 @@ export type VideoPlayerState = {
|
||||
quality: MWStreamQuality;
|
||||
url: string;
|
||||
type: MWStreamType;
|
||||
providerId?: string;
|
||||
embedId?: string;
|
||||
caption: null | {
|
||||
url: string;
|
||||
id: string;
|
||||
|
@ -146,6 +146,8 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
||||
source={props.stream.streamUrl}
|
||||
type={props.stream.type}
|
||||
quality={props.stream.quality}
|
||||
embedId={props.stream.embedId}
|
||||
providerId={props.stream.providerId}
|
||||
/>
|
||||
<ProgressListenerController
|
||||
startAt={firstStartTime.current}
|
||||
@ -181,6 +183,7 @@ export function MediaView() {
|
||||
return getMetaFromId(data.type, data.id, seasonId);
|
||||
}
|
||||
);
|
||||
// TODO get stream from someplace that actually gets updated
|
||||
const [stream, setStream] = useState<MWStream | null>(null);
|
||||
|
||||
const lastSearchValue = useRef<(string | undefined)[] | null>(null);
|
||||
|
Loading…
Reference in New Issue
Block a user