mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
episode ids , shorter debounce and flixHQ provider
This commit is contained in:
parent
5a01a68ce4
commit
f472f04735
24 changed files with 337 additions and 82 deletions
|
@ -6,6 +6,7 @@
|
|||
"dependencies": {
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"crypto-js": "^4.1.1",
|
||||
"fscreen": "^1.2.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"ofetch": "^1.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-stickynode": "^4.1.0",
|
||||
|
|
|
@ -21,7 +21,22 @@ export function mwFetch<T>(url: string, ops: P<T>[1]): R<T> {
|
|||
}
|
||||
|
||||
export function proxiedFetch<T>(url: string, ops: P<T>[1]): R<T> {
|
||||
const parsedUrl = new URL(url);
|
||||
let combinedUrl = ops?.baseURL ?? "";
|
||||
if (
|
||||
combinedUrl.length > 0 &&
|
||||
combinedUrl.endsWith("/") &&
|
||||
url.startsWith("/")
|
||||
)
|
||||
combinedUrl += url.slice(1);
|
||||
else if (
|
||||
combinedUrl.length > 0 &&
|
||||
!combinedUrl.endsWith("/") &&
|
||||
!url.startsWith("/")
|
||||
)
|
||||
combinedUrl += `/${url}`;
|
||||
else combinedUrl += url;
|
||||
|
||||
const parsedUrl = new URL(combinedUrl);
|
||||
Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
|
||||
parsedUrl.searchParams.set(k, v);
|
||||
});
|
||||
|
|
|
@ -20,8 +20,8 @@ type MWProviderTypeSpecific =
|
|||
}
|
||||
| {
|
||||
type: MWMediaType.SERIES;
|
||||
episode: number;
|
||||
season: number;
|
||||
episode: string;
|
||||
season: string;
|
||||
};
|
||||
export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase;
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ type MWProviderRunContextTypeSpecific =
|
|||
}
|
||||
| {
|
||||
type: MWMediaType.SERIES;
|
||||
episode: number;
|
||||
season: number;
|
||||
episode: string;
|
||||
season: string;
|
||||
};
|
||||
|
||||
export type MWProviderRunContext = MWProviderRunContextBase &
|
||||
|
|
|
@ -2,6 +2,7 @@ import { initializeScraperStore } from "./helpers/register";
|
|||
|
||||
// providers
|
||||
import "./providers/gdriveplayer";
|
||||
import "./providers/flixhq";
|
||||
|
||||
// embeds
|
||||
// -- nothing here yet
|
||||
|
|
|
@ -3,6 +3,7 @@ import { makeUrl, mwFetch } from "../helpers/fetch";
|
|||
import {
|
||||
formatJWMeta,
|
||||
JWMediaResult,
|
||||
JWSeasonMetaResult,
|
||||
JW_API_BASE,
|
||||
mediaTypeToJW,
|
||||
} from "./justwatch";
|
||||
|
@ -33,7 +34,8 @@ export interface DetailedMeta {
|
|||
|
||||
export async function getMetaFromId(
|
||||
type: MWMediaType,
|
||||
id: string
|
||||
id: string,
|
||||
seasonId?: string
|
||||
): Promise<DetailedMeta | null> {
|
||||
const queryType = mediaTypeToJW(type);
|
||||
|
||||
|
@ -61,8 +63,17 @@ export async function getMetaFromId(
|
|||
|
||||
if (!imdbId || !tmdbId) throw new Error("not enough info");
|
||||
|
||||
let seasonData: JWSeasonMetaResult | undefined;
|
||||
if (data.object_type === "show") {
|
||||
const seasonToScrape = seasonId ?? data.seasons?.[0].id.toString() ?? "";
|
||||
const url = makeUrl("/content/titles/show_season/{id}/locale/en_US", {
|
||||
id: seasonToScrape,
|
||||
});
|
||||
seasonData = await mwFetch<any>(url, { baseURL: JW_API_BASE });
|
||||
}
|
||||
|
||||
return {
|
||||
meta: formatJWMeta(data),
|
||||
meta: formatJWMeta(data, seasonData),
|
||||
imdbId,
|
||||
tmdbId,
|
||||
};
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
import { MWMediaType } from "./types";
|
||||
import { MWMediaMeta, MWMediaType, MWSeasonMeta } from "./types";
|
||||
|
||||
export const JW_API_BASE = "https://apis.justwatch.com";
|
||||
export const JW_IMAGE_BASE = "https://images.justwatch.com";
|
||||
|
||||
export type JWContentTypes = "movie" | "show";
|
||||
|
||||
export type JWSeasonShort = {
|
||||
title: string;
|
||||
id: number;
|
||||
season_number: number;
|
||||
};
|
||||
|
||||
export type JWEpisodeShort = {
|
||||
title: string;
|
||||
id: number;
|
||||
episode_number: number;
|
||||
};
|
||||
|
||||
export type JWMediaResult = {
|
||||
title: string;
|
||||
poster?: string;
|
||||
|
@ -12,6 +24,14 @@ export type JWMediaResult = {
|
|||
original_release_year: number;
|
||||
jw_entity_id: string;
|
||||
object_type: JWContentTypes;
|
||||
seasons?: JWSeasonShort[];
|
||||
};
|
||||
|
||||
export type JWSeasonMetaResult = {
|
||||
title: string;
|
||||
id: string;
|
||||
season_number: number;
|
||||
episodes: JWEpisodeShort[];
|
||||
};
|
||||
|
||||
export function mediaTypeToJW(type: MWMediaType): JWContentTypes {
|
||||
|
@ -26,8 +46,24 @@ export function JWMediaToMediaType(type: string): MWMediaType {
|
|||
throw new Error("unsupported type");
|
||||
}
|
||||
|
||||
export function formatJWMeta(media: JWMediaResult) {
|
||||
export function formatJWMeta(
|
||||
media: JWMediaResult,
|
||||
season?: JWSeasonMetaResult
|
||||
): MWMediaMeta {
|
||||
const type = JWMediaToMediaType(media.object_type);
|
||||
let seasons: undefined | MWSeasonMeta[];
|
||||
if (type === MWMediaType.SERIES) {
|
||||
seasons = media.seasons
|
||||
?.sort((a, b) => a.season_number - b.season_number)
|
||||
.map(
|
||||
(v): MWSeasonMeta => ({
|
||||
id: v.id.toString(),
|
||||
number: v.season_number,
|
||||
title: v.title,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
title: media.title,
|
||||
id: media.id.toString(),
|
||||
|
@ -36,5 +72,41 @@ export function formatJWMeta(media: JWMediaResult) {
|
|||
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
|
||||
: undefined,
|
||||
type,
|
||||
seasons: seasons as any,
|
||||
seasonData: season
|
||||
? ({
|
||||
id: season.id.toString(),
|
||||
number: season.season_number,
|
||||
title: season.title,
|
||||
episodes: season.episodes
|
||||
.sort((a, b) => a.episode_number - b.episode_number)
|
||||
.map((v) => ({
|
||||
id: v.id.toString(),
|
||||
number: v.episode_number,
|
||||
title: v.title,
|
||||
})),
|
||||
} as any)
|
||||
: (undefined as any),
|
||||
};
|
||||
}
|
||||
|
||||
export function JWMediaToId(media: MWMediaMeta): string {
|
||||
return ["JW", mediaTypeToJW(media.type), media.id].join("-");
|
||||
}
|
||||
|
||||
export function decodeJWId(
|
||||
paramId: string
|
||||
): { id: string; type: MWMediaType } | null {
|
||||
const [prefix, type, id] = paramId.split("-", 3);
|
||||
if (prefix !== "JW") return null;
|
||||
let mediaType;
|
||||
try {
|
||||
mediaType = JWMediaToMediaType(type);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: mediaType,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,14 +4,43 @@ export enum MWMediaType {
|
|||
ANIME = "anime",
|
||||
}
|
||||
|
||||
export type MWMediaMeta = {
|
||||
export type MWSeasonMeta = {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type MWSeasonWithEpisodeMeta = {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
episodes: {
|
||||
id: string;
|
||||
number: number;
|
||||
title: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type MWMediaMetaBase = {
|
||||
title: string;
|
||||
id: string;
|
||||
year: string;
|
||||
poster?: string;
|
||||
type: MWMediaType;
|
||||
};
|
||||
|
||||
type MWMediaMetaSpecific =
|
||||
| {
|
||||
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||
seasons: undefined;
|
||||
}
|
||||
| {
|
||||
type: MWMediaType.SERIES;
|
||||
seasons: MWSeasonMeta[];
|
||||
seasonData: MWSeasonWithEpisodeMeta;
|
||||
};
|
||||
|
||||
export type MWMediaMeta = MWMediaMetaBase & MWMediaMetaSpecific;
|
||||
|
||||
export interface MWQuery {
|
||||
searchQuery: string;
|
||||
type: MWMediaType;
|
||||
|
|
|
@ -1,37 +1,63 @@
|
|||
import { proxiedFetch } from "../helpers/fetch";
|
||||
import { registerProvider } from "../helpers/register";
|
||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||
import { MWMediaType } from "../metadata/types";
|
||||
|
||||
const timeout = (time: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(() => resolve(), time);
|
||||
});
|
||||
const flixHqBase = "https://api.consumet.org/movies/flixhq";
|
||||
|
||||
registerProvider({
|
||||
id: "testprov",
|
||||
rank: 42,
|
||||
id: "flixhq",
|
||||
displayName: "FlixHQ",
|
||||
rank: 100,
|
||||
type: [MWMediaType.MOVIE],
|
||||
disabled: true,
|
||||
|
||||
async scrape({ progress }) {
|
||||
await timeout(1000);
|
||||
async scrape({ media, progress }) {
|
||||
// search for relevant item
|
||||
const searchResults = await proxiedFetch<any>(
|
||||
`/${encodeURIComponent(media.meta.title)}`,
|
||||
{
|
||||
baseURL: flixHqBase,
|
||||
}
|
||||
);
|
||||
// TODO fuzzy match or normalize title before comparison
|
||||
const foundItem = searchResults.results.find((v: any) => {
|
||||
return v.title === media.meta.title && v.releaseDate === media.meta.year;
|
||||
});
|
||||
if (!foundItem) throw new Error("No watchable item found");
|
||||
const flixId = foundItem.id;
|
||||
|
||||
// get media info
|
||||
progress(25);
|
||||
await timeout(1000);
|
||||
progress(50);
|
||||
await timeout(1000);
|
||||
const mediaInfo = await proxiedFetch<any>("/info", {
|
||||
baseURL: flixHqBase,
|
||||
params: {
|
||||
id: flixId,
|
||||
},
|
||||
});
|
||||
|
||||
// get stream info from media
|
||||
progress(75);
|
||||
await timeout(1000);
|
||||
const watchInfo = await proxiedFetch<any>("/watch", {
|
||||
baseURL: flixHqBase,
|
||||
params: {
|
||||
episodeId: mediaInfo.episodes[0].id,
|
||||
mediaId: flixId,
|
||||
},
|
||||
});
|
||||
|
||||
// get best quality source
|
||||
const source = watchInfo.sources.reduce((p: any, c: any) =>
|
||||
c.quality > p.quality ? c : p
|
||||
);
|
||||
|
||||
return {
|
||||
embeds: [
|
||||
// {
|
||||
// type: MWEmbedType.OPENLOAD,
|
||||
// url: "https://google.com",
|
||||
// },
|
||||
// {
|
||||
// type: MWEmbedType.ANOTHER,
|
||||
// url: "https://google.com",
|
||||
// },
|
||||
],
|
||||
embeds: [],
|
||||
stream: {
|
||||
streamUrl: source.url,
|
||||
quality: MWStreamQuality.QUNKNOWN,
|
||||
type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4,
|
||||
captions: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ export function EditButton(props: EditButtonProps) {
|
|||
>
|
||||
<span ref={parent}>
|
||||
{props.editing ? (
|
||||
<span className="mx-4">Stop editing</span>
|
||||
<span className="mx-4 whitespace-nowrap">Stop editing</span>
|
||||
) : (
|
||||
<Icon icon={Icons.EDIT} />
|
||||
)}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { DotList } from "@/components/text/DotList";
|
||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||
import { mediaTypeToJW } from "@/backend/metadata/justwatch";
|
||||
import { JWMediaToId } from "@/backend/metadata/justwatch";
|
||||
import { Icons } from "../Icon";
|
||||
import { IconPatch } from "../buttons/IconPatch";
|
||||
|
||||
|
@ -107,9 +107,7 @@ export function MediaCard(props: MediaCardProps) {
|
|||
const canLink = props.linkable && !props.closable;
|
||||
|
||||
const link = canLink
|
||||
? `/media/${encodeURIComponent(
|
||||
mediaTypeToJW(props.media.type)
|
||||
)}-${encodeURIComponent(props.media.id)}`
|
||||
? `/media/${encodeURIComponent(JWMediaToId(props.media))}`
|
||||
: "#";
|
||||
|
||||
if (!props.linkable) return <span>{content}</span>;
|
||||
|
|
|
@ -1,26 +1,46 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useVideoPlayerState } from "../VideoContext";
|
||||
|
||||
interface ShowControlProps {
|
||||
series?: {
|
||||
episode: number;
|
||||
season: number;
|
||||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
title?: string;
|
||||
onSelect?: (state: { episodeId?: string; seasonId?: string }) => void;
|
||||
}
|
||||
|
||||
export function ShowControl(props: ShowControlProps) {
|
||||
const { videoState } = useVideoPlayerState();
|
||||
const lastState = useRef<{
|
||||
episodeId?: string;
|
||||
seasonId?: string;
|
||||
} | null>({
|
||||
episodeId: props.series?.episodeId,
|
||||
seasonId: props.series?.seasonId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
videoState.setShowData({
|
||||
current: props.series,
|
||||
isSeries: !!props.series,
|
||||
title: props.title,
|
||||
});
|
||||
// we only want it to run when props change, not when videoState changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentState = {
|
||||
episodeId: videoState.seasonData.current?.episodeId,
|
||||
seasonId: videoState.seasonData.current?.seasonId,
|
||||
};
|
||||
if (
|
||||
currentState.episodeId !== lastState.current?.episodeId ||
|
||||
currentState.seasonId !== lastState.current?.seasonId
|
||||
) {
|
||||
lastState.current = currentState;
|
||||
props.onSelect?.(currentState);
|
||||
}
|
||||
}, [videoState, props]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ export function VolumeControl(props: Props) {
|
|||
</div>
|
||||
<div
|
||||
className={`linear -ml-2 w-0 overflow-hidden transition-[width,opacity] duration-300 ${
|
||||
hoveredOnce ? "!w-24 opacity-100" : "w-4 opacity-0"
|
||||
hoveredOnce || dragging ? "!w-24 opacity-100" : "w-4 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -13,11 +13,10 @@ import { getStoredVolume, setStoredVolume } from "./volumeStore";
|
|||
|
||||
interface ShowData {
|
||||
current?: {
|
||||
episode: number;
|
||||
season: number;
|
||||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
isSeries: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface PlayerControls {
|
||||
|
|
|
@ -26,10 +26,9 @@ export type PlayerState = {
|
|||
seasonData: {
|
||||
isSeries: boolean;
|
||||
current?: {
|
||||
episode: number;
|
||||
season: number;
|
||||
episodeId: string;
|
||||
seasonId: string;
|
||||
};
|
||||
title?: string;
|
||||
};
|
||||
error: null | {
|
||||
name: string;
|
||||
|
|
|
@ -15,8 +15,8 @@ export interface ScrapeEventLog {
|
|||
export type SelectedMediaData =
|
||||
| {
|
||||
type: MWMediaType.SERIES;
|
||||
episode: number;
|
||||
season: number;
|
||||
episode: string;
|
||||
season: string;
|
||||
}
|
||||
| {
|
||||
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||
|
|
|
@ -42,7 +42,6 @@ if (key) {
|
|||
|
||||
// TODO general todos:
|
||||
// - localize everything
|
||||
// - add titles to pages
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
|
|
@ -16,6 +16,11 @@ function App() {
|
|||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||
</Route>
|
||||
<Route exact path="/media/:media" component={MediaView} />
|
||||
<Route
|
||||
exact
|
||||
path="/media/:media/:season/:episode"
|
||||
component={MediaView}
|
||||
/>
|
||||
<Route exact path="/search/:type/:query?" component={SearchView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
|
|
|
@ -4,12 +4,16 @@ import { Link } from "@/components/text/Link";
|
|||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||
import { useGoBack } from "@/hooks/useGoBack";
|
||||
import { conf } from "@/setup/config";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
export function MediaFetchErrorView() {
|
||||
const goBack = useGoBack();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex-1">
|
||||
<Helmet>
|
||||
<title>Failed to load meta</title>
|
||||
</Helmet>
|
||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||
<VideoPlayerHeader onClick={goBack} />
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { useParams } from "react-router-dom";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
||||
import { MWStream } from "@/backend/helpers/streams";
|
||||
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||
import { JWMediaToMediaType } from "@/backend/metadata/justwatch";
|
||||
import { decodeJWId } from "@/backend/metadata/justwatch";
|
||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
|
@ -23,6 +24,9 @@ import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
|||
function MediaViewLoading(props: { onGoBack(): void }) {
|
||||
return (
|
||||
<div className="relative flex h-screen items-center justify-center">
|
||||
<Helmet>
|
||||
<title>Loading...</title>
|
||||
</Helmet>
|
||||
<div className="absolute inset-x-0 top-0 p-6">
|
||||
<VideoPlayerHeader onClick={props.onGoBack} />
|
||||
</div>
|
||||
|
@ -51,6 +55,9 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
|||
|
||||
return (
|
||||
<div className="relative flex h-screen items-center justify-center">
|
||||
<Helmet>
|
||||
<title>{props.meta.meta.title}</title>
|
||||
</Helmet>
|
||||
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
||||
<VideoPlayerHeader onClick={props.onGoBack} media={props.meta.meta} />
|
||||
</div>
|
||||
|
@ -85,6 +92,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
|||
interface MediaViewPlayerProps {
|
||||
meta: DetailedMeta;
|
||||
stream: MWStream;
|
||||
selected: SelectedMediaData;
|
||||
}
|
||||
export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
||||
const goBack = useGoBack();
|
||||
|
@ -96,8 +104,13 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.stream]);
|
||||
|
||||
// TODO show episode title
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen">
|
||||
<Helmet>
|
||||
<title>{props.meta.meta.title}</title>
|
||||
</Helmet>
|
||||
<DecoratedVideoPlayer media={props.meta.meta} onGoBack={goBack} autoPlay>
|
||||
<SourceControl
|
||||
source={props.stream.streamUrl}
|
||||
|
@ -107,44 +120,71 @@ export function MediaViewPlayer(props: MediaViewPlayerProps) {
|
|||
startAt={firstStartTime.current}
|
||||
onProgress={updateProgress}
|
||||
/>
|
||||
<ShowControl series={{ episode: 5, season: 2 }} title="hello world" />
|
||||
{props.selected.type === MWMediaType.SERIES ? (
|
||||
<ShowControl
|
||||
series={{
|
||||
seasonId: props.selected.season,
|
||||
episodeId: props.selected.episode,
|
||||
}}
|
||||
onSelect={(d) => console.log("selected stuff", d)}
|
||||
/>
|
||||
) : null}
|
||||
</DecoratedVideoPlayer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MediaView() {
|
||||
const params = useParams<{ media: string }>();
|
||||
const params = useParams<{
|
||||
media: string;
|
||||
episode?: string;
|
||||
season?: string;
|
||||
}>();
|
||||
const goBack = useGoBack();
|
||||
const history = useHistory();
|
||||
|
||||
const [meta, setMeta] = useState<DetailedMeta | null>(null);
|
||||
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
|
||||
const [exec, loading, error] = useLoading(async (mediaParams: string) => {
|
||||
let type: MWMediaType;
|
||||
let id = "";
|
||||
try {
|
||||
const [t, i] = mediaParams.split("-", 2);
|
||||
type = JWMediaToMediaType(t);
|
||||
id = i;
|
||||
} catch (err) {
|
||||
return null;
|
||||
const [exec, loading, error] = useLoading(
|
||||
async (mediaParams: string, seasonId?: string) => {
|
||||
const data = decodeJWId(mediaParams);
|
||||
if (!data) return null;
|
||||
return getMetaFromId(data.type, data.id, seasonId);
|
||||
}
|
||||
return getMetaFromId(type, id);
|
||||
});
|
||||
);
|
||||
const [stream, setStream] = useState<MWStream | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
exec(params.media).then((v) => {
|
||||
console.log("I am being ran");
|
||||
exec(params.media, params.season).then((v) => {
|
||||
setMeta(v ?? null);
|
||||
if (v)
|
||||
if (v) {
|
||||
if (v.meta.type !== MWMediaType.SERIES) {
|
||||
setSelected({
|
||||
type: v.meta.type,
|
||||
episode: 0 as any,
|
||||
season: 0 as any,
|
||||
season: undefined,
|
||||
episode: undefined,
|
||||
});
|
||||
else setSelected(null);
|
||||
} else {
|
||||
const season = params.season ?? v.meta.seasonData.id;
|
||||
const episode = params.episode ?? v.meta.seasonData.episodes[0].id;
|
||||
setSelected({
|
||||
type: MWMediaType.SERIES,
|
||||
season,
|
||||
episode,
|
||||
});
|
||||
}, [exec, params.media]);
|
||||
if (season !== params.season || episode !== params.episode)
|
||||
history.replace(
|
||||
`/media/${encodeURIComponent(params.media)}/${encodeURIComponent(
|
||||
season
|
||||
)}/${encodeURIComponent(episode)}`
|
||||
);
|
||||
}
|
||||
} else setSelected(null);
|
||||
});
|
||||
// dont rerender when params changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exec, history]);
|
||||
|
||||
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
||||
if (error) return <MediaFetchErrorView />;
|
||||
|
@ -167,5 +207,5 @@ export function MediaView() {
|
|||
);
|
||||
|
||||
// show stream once we have a stream
|
||||
return <MediaViewPlayer meta={meta} stream={stream} />;
|
||||
return <MediaViewPlayer meta={meta} stream={stream} selected={selected} />;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ArrowLink } from "@/components/text/ArrowLink";
|
|||
import { Title } from "@/components/text/Title";
|
||||
import { useGoBack } from "@/hooks/useGoBack";
|
||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
export function NotFoundWrapper(props: {
|
||||
children?: ReactNode;
|
||||
|
@ -16,6 +17,9 @@ export function NotFoundWrapper(props: {
|
|||
|
||||
return (
|
||||
<div className="h-screen flex-1">
|
||||
<Helmet>
|
||||
<title>Not found</title>
|
||||
</Helmet>
|
||||
{props.video ? (
|
||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||
<VideoPlayerHeader onClick={goBack} />
|
||||
|
|
|
@ -13,7 +13,7 @@ export function SearchResultsPartial({ search }: SearchResultsPartialProps) {
|
|||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
|
||||
const debouncedSearch = useDebounce<MWQuery>(search, 500);
|
||||
useEffect(() => {
|
||||
setSearching(search.searchQuery !== "");
|
||||
setLoading(search.searchQuery !== "");
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SearchBarInput } from "@/components/SearchBar";
|
|||
import { Title } from "@/components/text/Title";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { SearchResultsPartial } from "./SearchResultsPartial";
|
||||
|
||||
export function SearchView() {
|
||||
|
@ -22,6 +23,9 @@ export function SearchView() {
|
|||
return (
|
||||
<>
|
||||
<div className="relative z-10 mb-24">
|
||||
<Helmet>
|
||||
<title>movie-web</title>
|
||||
</Helmet>
|
||||
<Navigation bg={showBg} />
|
||||
<ThinContainer>
|
||||
<div className="mt-44 space-y-16 text-center">
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -321,6 +321,13 @@
|
|||
dependencies:
|
||||
"@types/react" "^17"
|
||||
|
||||
"@types/react-helmet@^6.1.6":
|
||||
"integrity" "sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A=="
|
||||
"resolved" "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.6.tgz"
|
||||
"version" "6.1.6"
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^5.3.3":
|
||||
"integrity" "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="
|
||||
"resolved" "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz"
|
||||
|
@ -2881,7 +2888,7 @@
|
|||
dependencies:
|
||||
"read" "1"
|
||||
|
||||
"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.8.1":
|
||||
"prop-types@^15.6.0", "prop-types@^15.6.2", "prop-types@^15.7.2", "prop-types@^15.8.1":
|
||||
"integrity" "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="
|
||||
"resolved" "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
"version" "15.8.1"
|
||||
|
@ -2924,6 +2931,21 @@
|
|||
"object-assign" "^4.1.1"
|
||||
"scheduler" "^0.20.2"
|
||||
|
||||
"react-fast-compare@^3.1.1":
|
||||
"integrity" "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||
"resolved" "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz"
|
||||
"version" "3.2.0"
|
||||
|
||||
"react-helmet@^6.1.0":
|
||||
"integrity" "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw=="
|
||||
"resolved" "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz"
|
||||
"version" "6.1.0"
|
||||
dependencies:
|
||||
"object-assign" "^4.1.1"
|
||||
"prop-types" "^15.7.2"
|
||||
"react-fast-compare" "^3.1.1"
|
||||
"react-side-effect" "^2.1.0"
|
||||
|
||||
"react-i18next@^12.1.1":
|
||||
"integrity" "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA=="
|
||||
"resolved" "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz"
|
||||
|
@ -2965,6 +2987,11 @@
|
|||
"tiny-invariant" "^1.0.2"
|
||||
"tiny-warning" "^1.0.0"
|
||||
|
||||
"react-side-effect@^2.1.0":
|
||||
"integrity" "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw=="
|
||||
"resolved" "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz"
|
||||
"version" "2.1.2"
|
||||
|
||||
"react-stickynode@^4.1.0":
|
||||
"integrity" "sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ=="
|
||||
"resolved" "https://registry.npmjs.org/react-stickynode/-/react-stickynode-4.1.0.tgz"
|
||||
|
@ -2986,7 +3013,7 @@
|
|||
"loose-envify" "^1.4.0"
|
||||
"prop-types" "^15.6.2"
|
||||
|
||||
"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.6.0", "react@17.0.2":
|
||||
"react@^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.3.0 || ^17.0.0 || ^18.0.0", "react@^17.0.2", "react@>= 16.8.0", "react@>=15", "react@>=16.3.0", "react@>=16.6.0", "react@17.0.2":
|
||||
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
|
||||
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
||||
"version" "17.0.2"
|
||||
|
|
Loading…
Reference in a new issue