bunch of todos

This commit is contained in:
Jelle van Snik 2023-01-15 16:01:07 +01:00
parent 8e522e18d4
commit 52b063b10a
23 changed files with 445 additions and 147 deletions

View file

@ -15,6 +15,7 @@
"json5": "^2.2.0",
"lodash.throttle": "^4.1.1",
"nanoid": "^4.0.0",
"ofetch": "^1.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^12.1.1",

View file

@ -4,15 +4,13 @@
},
"search": {
"loading": "Fetching your favourite shows...",
"providersFailed": "{{fails}}/{{total}} providers failed!",
"allResults": "That's all we have!",
"noResults": "We couldn't find anything!",
"allFailed": "All providers have failed!",
"allFailed": "Failed to find media, try again!",
"headingTitle": "Search results",
"headingLink": "Back to home",
"bookmarks": "Bookmarks",
"continueWatching": "Continue Watching",
"tagline": "Because watching legally is boring",
"title": "What do you want to watch?",
"placeholder": "What do you want to watch?"
},

View file

@ -2,7 +2,6 @@ import { MWStream } from "./streams";
export enum MWEmbedType {
OPENLOAD = "openload",
ANOTHER = "another",
}
export type MWEmbed = {
@ -17,6 +16,7 @@ export type MWEmbedContext = {
export type MWEmbedScraper = {
id: string;
displayName: string;
for: MWEmbedType;
rank: number;
disabled?: boolean;

View file

@ -0,0 +1,35 @@
import { conf } from "@/setup/config";
import { ofetch } from "ofetch";
type P<T> = Parameters<typeof ofetch<T>>;
type R<T> = ReturnType<typeof ofetch<T>>;
const baseFetch = ofetch.create({
retry: 0,
});
export function makeUrl(url: string, data: Record<string, string>) {
let parsedUrl: string = url;
Object.entries(data).forEach(([k, v]) => {
parsedUrl = parsedUrl.replace(`{${k}}`, encodeURIComponent(v));
});
return parsedUrl;
}
export function mwFetch<T>(url: string, ops: P<T>[1]): R<T> {
return baseFetch<T>(url, ops);
}
export function proxiedFetch<T>(url: string, ops: P<T>[1]): R<T> {
const parsedUrl = new URL(url);
Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
parsedUrl.searchParams.set(k, v);
});
return baseFetch<T>(conf().BASE_PROXY_URL, {
...ops,
baseURL: undefined,
params: {
destination: parsedUrl.toString(),
},
});
}

View file

@ -8,13 +8,26 @@ export type MWProviderScrapeResult = {
embeds: MWEmbed[];
};
export type MWProviderContext = {
type MWProviderBase = {
progress(percentage: number): void;
media: DetailedMeta;
};
type MWProviderTypeSpecific =
| {
type: MWMediaType.MOVIE | MWMediaType.ANIME;
episode?: undefined;
season?: undefined;
}
| {
type: MWMediaType.SERIES;
episode: number;
season: number;
};
export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase;
export type MWProvider = {
id: string;
displayName: string;
rank: number;
disabled?: boolean;
type: MWMediaType[];

View file

@ -1,38 +1,60 @@
import { MWProviderScrapeResult } from "./provider";
import { MWProviderContext, MWProviderScrapeResult } from "./provider";
import { getEmbedScraperByType, getProviders } from "./register";
import { runEmbedScraper, runProvider } from "./run";
import { MWStream } from "./streams";
import { DetailedMeta } from "../metadata/getmeta";
import { MWMediaType } from "../metadata/types";
interface MWProgressData {
type: "embed" | "provider";
id: string;
eventId: string;
percentage: number;
errored: boolean;
}
interface MWNextData {
id: string;
eventId: string;
type: "embed" | "provider";
}
export interface MWProviderRunContext {
type MWProviderRunContextBase = {
media: DetailedMeta;
onProgress?: (data: MWProgressData) => void;
onNext?: (data: MWNextData) => void;
}
};
type MWProviderRunContextTypeSpecific =
| {
type: MWMediaType.MOVIE | MWMediaType.ANIME;
episode: undefined;
season: undefined;
}
| {
type: MWMediaType.SERIES;
episode: number;
season: number;
};
export type MWProviderRunContext = MWProviderRunContextBase &
MWProviderRunContextTypeSpecific;
async function findBestEmbedStream(
result: MWProviderScrapeResult,
providerId: string,
ctx: MWProviderRunContext
): Promise<MWStream | null> {
if (result.stream) return result.stream;
let embedNum = 0;
for (const embed of result.embeds) {
embedNum += 1;
if (!embed.type) continue;
const scraper = getEmbedScraperByType(embed.type);
if (!scraper) throw new Error("Type for embed not found");
ctx.onNext?.({ id: scraper.id, type: "embed" });
const eventId = [providerId, scraper.id, embedNum].join("|");
ctx.onNext?.({ id: scraper.id, type: "embed", eventId });
let stream: MWStream;
try {
@ -41,6 +63,7 @@ async function findBestEmbedStream(
progress(num) {
ctx.onProgress?.({
errored: false,
eventId,
id: scraper.id,
percentage: num,
type: "embed",
@ -50,6 +73,7 @@ async function findBestEmbedStream(
} catch {
ctx.onProgress?.({
errored: true,
eventId,
id: scraper.id,
percentage: 100,
type: "embed",
@ -59,6 +83,7 @@ async function findBestEmbedStream(
ctx.onProgress?.({
errored: false,
eventId,
id: scraper.id,
percentage: 100,
type: "embed",
@ -76,24 +101,48 @@ export async function findBestStream(
const providers = getProviders();
for (const provider of providers) {
ctx.onNext?.({ id: provider.id, type: "provider" });
const eventId = provider.id;
ctx.onNext?.({ id: provider.id, type: "provider", eventId });
let result: MWProviderScrapeResult;
try {
result = await runProvider(provider, {
media: ctx.media,
progress(num) {
ctx.onProgress?.({
percentage: num,
errored: false,
id: provider.id,
type: "provider",
});
},
});
let context: MWProviderContext;
if (ctx.type === MWMediaType.SERIES) {
context = {
media: ctx.media,
type: ctx.type,
episode: ctx.episode,
season: ctx.season,
progress(num) {
ctx.onProgress?.({
percentage: num,
eventId,
errored: false,
id: provider.id,
type: "provider",
});
},
};
} else {
context = {
media: ctx.media,
type: ctx.type,
progress(num) {
ctx.onProgress?.({
percentage: num,
eventId,
errored: false,
id: provider.id,
type: "provider",
});
},
};
}
result = await runProvider(provider, context);
} catch (err) {
ctx.onProgress?.({
percentage: 100,
errored: true,
eventId,
id: provider.id,
type: "provider",
});
@ -103,11 +152,12 @@ export async function findBestStream(
ctx.onProgress?.({
errored: false,
id: provider.id,
eventId,
percentage: 100,
type: "provider",
});
const stream = await findBestEmbedStream(result, ctx);
const stream = await findBestEmbedStream(result, provider.id, ctx);
if (!stream) continue;
return stream;
}

View file

@ -2,13 +2,10 @@ import { initializeScraperStore } from "./helpers/register";
// TODO backend system:
// - caption support
// - hooks to run all providers one by one
// - move over old providers to new system
// - implement jons providers/embedscrapers
// - show/episode support
// providers
// -- nothing here yet
import "./providers/gdriveplayer";
// embeds

View file

@ -1,10 +1,13 @@
import { formatJWMeta, JWMediaResult } from "./justwatch";
import { FetchError } from "ofetch";
import { makeUrl, mwFetch } from "../helpers/fetch";
import {
formatJWMeta,
JWMediaResult,
JW_API_BASE,
mediaTypeToJW,
} from "./justwatch";
import { MWMediaMeta, MWMediaType } from "./types";
const JW_API_BASE = "https://apis.justwatch.com";
// http://localhost:5173/#/media/movie-439596/
type JWExternalIdType =
| "eidr"
| "imdb_latest"
@ -31,18 +34,23 @@ export interface DetailedMeta {
export async function getMetaFromId(
type: MWMediaType,
id: string
): Promise<DetailedMeta> {
let queryType = "";
if (type === MWMediaType.MOVIE) queryType = "movie";
else if (type === MWMediaType.SERIES) queryType = "show";
else if (type === MWMediaType.ANIME)
throw new Error("Anime search type is not supported");
): Promise<DetailedMeta | null> {
const queryType = mediaTypeToJW(type);
const data = await fetch(
`${JW_API_BASE}/content/titles/${queryType}/${encodeURIComponent(
id
)}/locale/en_US`
).then((res) => res.json() as Promise<JWDetailedMeta>);
let data: JWDetailedMeta;
try {
const url = makeUrl("/content/titles/{type}/{id}/locale/en_US", {
type: queryType,
id,
});
data = await mwFetch<JWDetailedMeta>(url, { baseURL: JW_API_BASE });
} catch (err) {
if (err instanceof FetchError) {
// 400 and 404 are treated as not found
if (err.statusCode === 400 || err.statusCode === 404) return null;
}
throw err;
}
const imdbId = data.external_ids.find(
(v) => v.provider === "imdb_latest"

View file

@ -1,6 +1,7 @@
import { MWMediaType } 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";
@ -32,10 +33,7 @@ export function formatJWMeta(media: JWMediaResult) {
id: media.id.toString(),
year: media.original_release_year.toString(),
poster: media.poster
? `https://images.justwatch.com${media.poster.replace(
"{profile}",
"s166"
)}`
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
: undefined,
type,
};

View file

@ -1,10 +1,19 @@
import { SimpleCache } from "@/utils/cache";
import { mwFetch } from "../helpers/fetch";
import {
formatJWMeta,
JWContentTypes,
JWMediaResult,
JW_API_BASE,
mediaTypeToJW,
} from "./justwatch";
import { MWMediaMeta, MWMediaType, MWQuery } from "./types";
import { MWMediaMeta, MWQuery } from "./types";
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
cache.setCompare((a, b) => {
return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
});
cache.initialize();
type JWSearchQuery = {
content_types: JWContentTypes[];
@ -21,26 +30,29 @@ type JWPage<T> = {
total_results: number;
};
export async function searchForMedia({
searchQuery,
type,
}: MWQuery): Promise<MWMediaMeta[]> {
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
const { searchQuery, type } = query;
const contentType = mediaTypeToJW(type);
const body: JWSearchQuery = {
content_types: [],
content_types: [contentType],
page: 1,
query: searchQuery,
page_size: 40,
};
if (type === MWMediaType.MOVIE) body.content_types.push("movie");
else if (type === MWMediaType.SERIES) body.content_types.push("show");
else if (type === MWMediaType.ANIME)
throw new Error("Anime search type is not supported");
const data = await fetch(
`${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent(
JSON.stringify(body)
)}`
).then((res) => res.json() as Promise<JWPage<JWMediaResult>>);
const data = await mwFetch<JWPage<JWMediaResult>>(
"/content/titles/en_US/popular",
{
baseURL: JW_API_BASE,
params: {
body: JSON.stringify(body),
},
}
);
return data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
const returnData = data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
cache.set(query, returnData, 3600); // cache for an hour
return returnData;
}

View file

@ -1,10 +1,10 @@
import { conf } from "@/setup/config";
import { unpack } from "unpacker";
import CryptoJS from "crypto-js";
import { registerProvider } from "@/backend/helpers/register";
import { MWMediaType } from "@/backend/metadata/types";
import { MWStreamQuality } from "@/backend/helpers/streams";
import { unpack } from "unpacker";
import CryptoJS from "crypto-js";
import { proxiedFetch } from "../helpers/fetch";
const format = {
stringify: (cipher: any) => {
@ -34,16 +34,20 @@ const format = {
registerProvider({
id: "gdriveplayer",
displayName: "gdriveplayer",
rank: 69,
type: [MWMediaType.MOVIE],
async scrape({ progress, media: { imdbId } }) {
progress(10);
const streamRes = await fetch(
`${
conf().CORS_PROXY_URL
}https://database.gdriveplayer.us/player.php?imdb=${imdbId}`
).then((d) => d.text());
const streamRes = await proxiedFetch<string>(
"https://database.gdriveplayer.us/player.php",
{
params: {
imdb: imdbId,
},
}
);
progress(90);
const page = new DOMParser().parseFromString(streamRes, "text/html");
@ -67,6 +71,7 @@ registerProvider({
{ format }
).toString(CryptoJS.enc.Utf8)
);
// eslint-disable-next-line
const sources = JSON.parse(
JSON.stringify(

View file

@ -5,6 +5,62 @@ import { Link } from "@/components/text/Link";
import { Title } from "@/components/text/Title";
import { conf } from "@/setup/config";
interface ErrorShowcaseProps {
error: {
name: string;
description: string;
path: string;
};
}
export function ErrorShowcase(props: ErrorShowcaseProps) {
return (
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
<p className="mb-1 break-words font-bold text-white">
{props.error.name} - {props.error.description}
</p>
<p className="break-words">{props.error.path}</p>
</div>
);
}
interface ErrorMessageProps {
error?: {
name: string;
description: string;
path: string;
};
children?: React.ReactNode;
}
export function ErrorMessage(props: ErrorMessageProps) {
return (
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
<div className="flex flex-col items-center justify-start text-center">
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
<Title>Whoops, it broke</Title>
{props.children ? (
props.children
) : (
<p className="my-6 max-w-lg">
The app encountered an error and wasn&apos;t able to recover, please
report it to the{" "}
<Link url={conf().DISCORD_LINK} newTab>
Discord server
</Link>{" "}
or on{" "}
<Link url={conf().GITHUB_LINK} newTab>
GitHub
</Link>
.
</p>
)}
</div>
{props.error ? <ErrorShowcase error={props.error} /> : null}
</div>
);
}
interface ErrorBoundaryState {
hasError: boolean;
error?: {
@ -50,33 +106,6 @@ export class ErrorBoundary extends Component<
render() {
if (!this.state.hasError) return this.props.children as any;
return (
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
<div className="flex flex-col items-center justify-start text-center">
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
<Title>Whoops, it broke</Title>
<p className="my-6 max-w-lg">
The app encountered an error and wasn&apos;t able to recover, please
report it to the{" "}
<Link url={conf().DISCORD_LINK} newTab>
Discord server
</Link>{" "}
or on{" "}
<Link url={conf().GITHUB_LINK} newTab>
GitHub
</Link>
.
</p>
</div>
{this.state.error ? (
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
<p className="mb-1 break-words font-bold text-white">
{this.state.error.name} - {this.state.error.description}
</p>
<p className="break-words">{this.state.error.path}</p>
</div>
) : null}
</div>
);
return <ErrorMessage error={this.state.error} />;
}
}

View file

@ -7,7 +7,7 @@ interface VideoPlayerHeaderProps {
}
export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
const showDivider = props.title || props.onClick;
const showDivider = props.title && props.onClick;
return (
<div className="flex items-center">
<div className="flex flex-1 items-center">

12
src/hooks/useGoBack.ts Normal file
View file

@ -0,0 +1,12 @@
import { useCallback } from "react";
import { useHistory } from "react-router-dom";
export function useGoBack() {
const reactHistory = useHistory();
const goBack = useCallback(() => {
if (reactHistory.action !== "POP") reactHistory.goBack();
else reactHistory.push("/");
}, [reactHistory]);
return goBack;
}

View file

@ -1,16 +1,30 @@
import { findBestStream } from "@/backend/helpers/scrape";
import { MWStream } from "@/backend/helpers/streams";
import { DetailedMeta } from "@/backend/metadata/getmeta";
import { MWMediaType } from "@/backend/metadata/types";
import { useEffect, useState } from "react";
export interface ScrapeEventLog {
type: "provider" | "embed";
errored: boolean;
percentage: number;
eventId: string;
id: string;
}
export function useScrape(meta: DetailedMeta) {
export type SelectedMediaData =
| {
type: MWMediaType.SERIES;
episode: number;
season: number;
}
| {
type: MWMediaType.MOVIE | MWMediaType.ANIME;
episode: undefined;
season: undefined;
};
export function useScrape(meta: DetailedMeta, selected: SelectedMediaData) {
const [eventLog, setEventLog] = useState<ScrapeEventLog[]>([]);
const [stream, setStream] = useState<MWStream | null>(null);
const [pending, setPending] = useState(true);
@ -22,12 +36,14 @@ export function useScrape(meta: DetailedMeta) {
(async () => {
const scrapedStream = await findBestStream({
media: meta,
...selected,
onNext(ctx) {
setEventLog((arr) => [
...arr,
{
errored: false,
id: ctx.id,
eventId: ctx.eventId,
type: ctx.type,
percentage: 0,
},
@ -48,7 +64,7 @@ export function useScrape(meta: DetailedMeta) {
setPending(false);
setStream(scrapedStream);
})();
}, [meta]);
}, [meta, selected]);
return {
stream,

View file

@ -33,6 +33,9 @@ if (key) {
// - devices: ipadOS
// - features: HLS, error handling, preload interactions
// TODO general todos:
// - localize everything
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>

View file

@ -0,0 +1,49 @@
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
import { Link } from "@/components/text/Link";
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
import { useGoBack } from "@/hooks/useGoBack";
import { conf } from "@/setup/config";
export function MediaFetchErrorView() {
const goBack = useGoBack();
return (
<div className="h-screen flex-1">
<div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} />
</div>
<ErrorMessage>
<p className="my-6 max-w-lg">
We failed to request the media you asked for, check your internet
connection and try again.
</p>
</ErrorMessage>
</div>
);
}
export function MediaPlaybackErrorView(props: { title?: string }) {
const goBack = useGoBack();
return (
<div className="h-screen flex-1">
<div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} title={props.title} />
</div>
<ErrorMessage>
<p className="my-6 max-w-lg">
We encountered an error while playing the video you requested. If this
keeps happening please report the issue to the
<Link url={conf().DISCORD_LINK} newTab>
Discord server
</Link>{" "}
or on{" "}
<Link url={conf().GITHUB_LINK} newTab>
GitHub
</Link>
.
</p>
</ErrorMessage>
</div>
);
}

View file

@ -71,7 +71,7 @@ export function MediaScrapeLog(props: MediaScrapeLogProps) {
>
<MediaScrapePillSkeleton />
{props.events.map((v) => (
<MediaScrapePill event={v} key={v.id} />
<MediaScrapePill event={v} key={v.eventId} />
))}
<MediaScrapePillSkeleton />
</div>

View file

@ -1,14 +1,21 @@
import { useHistory, useParams } from "react-router-dom";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWStream } from "@/backend/helpers/streams";
import { useScrape } from "@/hooks/useScrape";
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 { SourceControl } from "@/components/video/controls/SourceControl";
import { Loading } from "@/components/layout/Loading";
import { useLoading } from "@/hooks/useLoading";
import { MWMediaType } from "@/backend/metadata/types";
import { useGoBack } from "@/hooks/useGoBack";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { MediaFetchErrorView } from "./MediaErrorView";
import { MediaScrapeLog } from "./MediaScrapeLog";
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
function MediaViewLoading(props: { onGoBack(): void }) {
return (
@ -28,9 +35,10 @@ interface MediaViewScrapingProps {
onStream(stream: MWStream): void;
onGoBack(): void;
meta: DetailedMeta;
selected: SelectedMediaData;
}
function MediaViewScraping(props: MediaViewScrapingProps) {
const { eventLog, stream } = useScrape(props.meta);
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
useEffect(() => {
if (stream) {
@ -38,8 +46,6 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
}
}, [stream, props]);
// TODO error screen if no streams found
return (
<div className="relative flex h-screen items-center justify-center">
<div className="absolute inset-x-0 top-0 py-6 px-8">
@ -48,44 +54,91 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
title={props.meta.meta.title}
/>
</div>
<div className="flex flex-col items-center">
<Loading className="mb-4" />
<p className="mb-8 text-denim-700">Finding the best video for you</p>
<MediaScrapeLog events={eventLog} />
<div className="flex flex-col items-center transition-opacity duration-200">
{pending ? (
<>
<Loading />
<p className="mb-8 text-denim-700">
Finding the best video for you
</p>
</>
) : (
<>
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
<p className="mb-8 text-denim-700">
Whoops, could&apos;t find any videos for you
</p>
</>
)}
<div
className={`flex flex-col items-center transition-opacity duration-200 ${
pending ? "opacity-100" : "opacity-0"
}`}
>
<MediaScrapeLog events={eventLog} />
</div>
</div>
</div>
);
}
export function MediaView() {
const reactHistory = useHistory();
const params = useParams<{ media: string }>();
const goBack = useCallback(() => {
if (reactHistory.action !== "POP") reactHistory.goBack();
else reactHistory.push("/");
}, [reactHistory]);
const goBack = useGoBack();
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;
}
return getMetaFromId(type, id);
});
const [stream, setStream] = useState<MWStream | null>(null);
useEffect(() => {
// TODO handle errors
(async () => {
const [t, id] = params.media.split("-", 2);
const type = JWMediaToMediaType(t);
const fetchedMeta = await getMetaFromId(type, id);
setMeta(fetchedMeta);
})();
}, [setMeta, params]);
exec(params.media).then((v) => {
setMeta(v ?? null);
if (v)
setSelected({
type: v.meta.type,
episode: 0 as any,
season: 0 as any,
});
else setSelected(null);
});
}, [exec, params.media]);
// TODO watched store
// TODO error page with video header
if (!meta) return <MediaViewLoading onGoBack={goBack} />;
if (loading) return <MediaViewLoading onGoBack={goBack} />;
if (error) return <MediaFetchErrorView />;
if (!meta || !selected)
return (
<NotFoundWrapper video>
<NotFoundMedia />
</NotFoundWrapper>
);
// scraping view will start scraping and return with onStream
if (!stream)
return (
<MediaViewScraping meta={meta} onGoBack={goBack} onStream={setStream} />
<MediaViewScraping
meta={meta}
selected={selected}
onGoBack={goBack}
onStream={setStream}
/>
);
// show stream once we have a stream
return (
<div className="h-screen w-screen">
<DecoratedVideoPlayer title={meta.meta.title} onGoBack={goBack} autoPlay>

View file

@ -1,17 +0,0 @@
import { ReactElement } from "react";
export interface NotFoundChecksProps {
id: string;
children?: ReactElement;
}
/*
** Component that only renders children if the passed in data is fully correct
*/
export function NotFoundChecks(
props: NotFoundChecksProps
): ReactElement | null {
// TODO do notfound check
return props.children || null;
}

View file

@ -5,11 +5,24 @@ import { Icons } from "@/components/Icon";
import { Navigation } from "@/components/layout/Navigation";
import { ArrowLink } from "@/components/text/ArrowLink";
import { Title } from "@/components/text/Title";
import { useGoBack } from "@/hooks/useGoBack";
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
export function NotFoundWrapper(props: {
children?: ReactNode;
video?: boolean;
}) {
const goBack = useGoBack();
function NotFoundWrapper(props: { children?: ReactNode }) {
return (
<div className="h-screen flex-1">
<Navigation />
{props.video ? (
<div className="fixed inset-x-0 top-0 py-6 px-8">
<VideoPlayerHeader onClick={goBack} />
</div>
) : (
<Navigation />
)}
<div className="flex h-full flex-col items-center justify-center p-5 text-center">
{props.children}
</div>

View file

@ -52,7 +52,6 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
);
useEffect(() => {
// TODO use cache
async function runSearch(query: MWQuery) {
const searchResults = await runSearchQuery(query);
if (!searchResults) return;

View file

@ -974,6 +974,11 @@
"depd@^1.1.2":
"version" "1.1.2"
"destr@^1.2.1":
"integrity" "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA=="
"resolved" "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz"
"version" "1.2.2"
"detective@^5.2.1":
"integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw=="
"resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz"
@ -2363,6 +2368,11 @@
"negotiator@^0.6.3":
"version" "0.6.3"
"node-fetch-native@^1.0.1":
"integrity" "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg=="
"resolved" "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz"
"version" "1.0.1"
"node-fetch@2.6.7":
"integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="
"resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
@ -2631,6 +2641,15 @@
"define-properties" "^1.1.4"
"es-abstract" "^1.20.4"
"ofetch@^1.0.0":
"integrity" "sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ=="
"resolved" "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz"
"version" "1.0.0"
dependencies:
"destr" "^1.2.1"
"node-fetch-native" "^1.0.1"
"ufo" "^1.0.0"
"once@^1.3.0":
"integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
"resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
@ -3430,6 +3449,11 @@
"resolved" "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
"version" "4.9.4"
"ufo@^1.0.0":
"integrity" "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA=="
"resolved" "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz"
"version" "1.0.1"
"unbox-primitive@^1.0.2":
"integrity" "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="
"resolved" "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"