From 52b063b10a5f3795e8ad9c53b12cd738e475365b Mon Sep 17 00:00:00 2001 From: Jelle van Snik Date: Sun, 15 Jan 2023 16:01:07 +0100 Subject: [PATCH] bunch of todos --- package.json | 1 + public/locales/en-GB/translation.json | 4 +- src/backend/helpers/embed.ts | 2 +- src/backend/helpers/fetch.ts | 35 ++++++ src/backend/helpers/provider.ts | 15 ++- src/backend/helpers/scrape.ts | 84 +++++++++++--- src/backend/index.ts | 3 - src/backend/metadata/getmeta.ts | 40 ++++--- src/backend/metadata/justwatch.ts | 6 +- src/backend/metadata/search.ts | 44 +++++--- src/backend/providers/gdriveplayer.ts | 23 ++-- src/components/layout/ErrorBoundary.tsx | 85 ++++++++++----- .../video/parts/VideoPlayerHeader.tsx | 2 +- src/hooks/useGoBack.ts | 12 ++ src/hooks/useScrape.ts | 20 +++- src/index.tsx | 3 + src/views/media/MediaErrorView.tsx | 49 +++++++++ src/views/media/MediaScrapeLog.tsx | 2 +- src/views/media/MediaView.tsx | 103 +++++++++++++----- src/views/notfound/NotFoundChecks.tsx | 17 --- src/views/notfound/NotFoundView.tsx | 17 ++- src/views/search/SearchResultsView.tsx | 1 - yarn.lock | 24 ++++ 23 files changed, 445 insertions(+), 147 deletions(-) create mode 100644 src/backend/helpers/fetch.ts create mode 100644 src/hooks/useGoBack.ts create mode 100644 src/views/media/MediaErrorView.tsx delete mode 100644 src/views/notfound/NotFoundChecks.tsx diff --git a/package.json b/package.json index 02c93cc2..e7947361 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/en-GB/translation.json b/public/locales/en-GB/translation.json index f50d3c0b..f83d56f7 100644 --- a/public/locales/en-GB/translation.json +++ b/public/locales/en-GB/translation.json @@ -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?" }, diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 88c0420d..9f99b28a 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -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; diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts new file mode 100644 index 00000000..9804ff40 --- /dev/null +++ b/src/backend/helpers/fetch.ts @@ -0,0 +1,35 @@ +import { conf } from "@/setup/config"; +import { ofetch } from "ofetch"; + +type P = Parameters>; +type R = ReturnType>; + +const baseFetch = ofetch.create({ + retry: 0, +}); + +export function makeUrl(url: string, data: Record) { + let parsedUrl: string = url; + Object.entries(data).forEach(([k, v]) => { + parsedUrl = parsedUrl.replace(`{${k}}`, encodeURIComponent(v)); + }); + return parsedUrl; +} + +export function mwFetch(url: string, ops: P[1]): R { + return baseFetch(url, ops); +} + +export function proxiedFetch(url: string, ops: P[1]): R { + const parsedUrl = new URL(url); + Object.entries(ops?.params ?? {}).forEach(([k, v]) => { + parsedUrl.searchParams.set(k, v); + }); + return baseFetch(conf().BASE_PROXY_URL, { + ...ops, + baseURL: undefined, + params: { + destination: parsedUrl.toString(), + }, + }); +} diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts index 545a39e6..348152b3 100644 --- a/src/backend/helpers/provider.ts +++ b/src/backend/helpers/provider.ts @@ -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[]; diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 0683a2e6..3ad57843 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -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 { 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; } diff --git a/src/backend/index.ts b/src/backend/index.ts index ee267e67..6924b860 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -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 diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index e8306664..fc25fddf 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -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 { - 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 { + 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); + let data: JWDetailedMeta; + try { + const url = makeUrl("/content/titles/{type}/{id}/locale/en_US", { + type: queryType, + id, + }); + data = await mwFetch(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" diff --git a/src/backend/metadata/justwatch.ts b/src/backend/metadata/justwatch.ts index 50712bac..31aa78ed 100644 --- a/src/backend/metadata/justwatch.ts +++ b/src/backend/metadata/justwatch.ts @@ -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, }; diff --git a/src/backend/metadata/search.ts b/src/backend/metadata/search.ts index 7b3c486e..4ad0434b 100644 --- a/src/backend/metadata/search.ts +++ b/src/backend/metadata/search.ts @@ -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(); +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 = { total_results: number; }; -export async function searchForMedia({ - searchQuery, - type, -}: MWQuery): Promise { +export async function searchForMedia(query: MWQuery): Promise { + 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>); + const data = await mwFetch>( + "/content/titles/en_US/popular", + { + baseURL: JW_API_BASE, + params: { + body: JSON.stringify(body), + }, + } + ); - return data.items.map((v) => formatJWMeta(v)); + const returnData = data.items.map((v) => formatJWMeta(v)); + cache.set(query, returnData, 3600); // cache for an hour + return returnData; } diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts index 60c7ac4f..4adcb144 100644 --- a/src/backend/providers/gdriveplayer.ts +++ b/src/backend/providers/gdriveplayer.ts @@ -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( + "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( diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 061ff5df..125a352b 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -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 ( +
+

+ {props.error.name} - {props.error.description} +

+

{props.error.path}

+
+ ); +} + +interface ErrorMessageProps { + error?: { + name: string; + description: string; + path: string; + }; + children?: React.ReactNode; +} + +export function ErrorMessage(props: ErrorMessageProps) { + return ( +
+
+ + Whoops, it broke + {props.children ? ( + props.children + ) : ( +

+ The app encountered an error and wasn't able to recover, please + report it to the{" "} + + Discord server + {" "} + or on{" "} + + GitHub + + . +

+ )} +
+ {props.error ? : null} +
+ ); +} + 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 ( -
-
- - Whoops, it broke -

- The app encountered an error and wasn't able to recover, please - report it to the{" "} - - Discord server - {" "} - or on{" "} - - GitHub - - . -

-
- {this.state.error ? ( -
-

- {this.state.error.name} - {this.state.error.description} -

-

{this.state.error.path}

-
- ) : null} -
- ); + return ; } } diff --git a/src/components/video/parts/VideoPlayerHeader.tsx b/src/components/video/parts/VideoPlayerHeader.tsx index 83138b19..2296caf7 100644 --- a/src/components/video/parts/VideoPlayerHeader.tsx +++ b/src/components/video/parts/VideoPlayerHeader.tsx @@ -7,7 +7,7 @@ interface VideoPlayerHeaderProps { } export function VideoPlayerHeader(props: VideoPlayerHeaderProps) { - const showDivider = props.title || props.onClick; + const showDivider = props.title && props.onClick; return (
diff --git a/src/hooks/useGoBack.ts b/src/hooks/useGoBack.ts new file mode 100644 index 00000000..3ecc29a6 --- /dev/null +++ b/src/hooks/useGoBack.ts @@ -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; +} diff --git a/src/hooks/useScrape.ts b/src/hooks/useScrape.ts index ca0004ed..413b638e 100644 --- a/src/hooks/useScrape.ts +++ b/src/hooks/useScrape.ts @@ -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([]); const [stream, setStream] = useState(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, diff --git a/src/index.tsx b/src/index.tsx index 1b20371c..fce8fd42 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,6 +33,9 @@ if (key) { // - devices: ipadOS // - features: HLS, error handling, preload interactions +// TODO general todos: +// - localize everything + ReactDOM.render( diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx new file mode 100644 index 00000000..586f123a --- /dev/null +++ b/src/views/media/MediaErrorView.tsx @@ -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 ( +
+
+ +
+ +

+ We failed to request the media you asked for, check your internet + connection and try again. +

+
+
+ ); +} + +export function MediaPlaybackErrorView(props: { title?: string }) { + const goBack = useGoBack(); + + return ( +
+
+ +
+ +

+ We encountered an error while playing the video you requested. If this + keeps happening please report the issue to the + + Discord server + {" "} + or on{" "} + + GitHub + + . +

+
+
+ ); +} diff --git a/src/views/media/MediaScrapeLog.tsx b/src/views/media/MediaScrapeLog.tsx index 5d7ed3b1..9f0b0a97 100644 --- a/src/views/media/MediaScrapeLog.tsx +++ b/src/views/media/MediaScrapeLog.tsx @@ -71,7 +71,7 @@ export function MediaScrapeLog(props: MediaScrapeLogProps) { > {props.events.map((v) => ( - + ))}
diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index f9484c1a..f0224953 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -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 (
@@ -48,44 +54,91 @@ function MediaViewScraping(props: MediaViewScrapingProps) { title={props.meta.meta.title} />
-
- -

Finding the best video for you

- +
+ {pending ? ( + <> + +

+ Finding the best video for you +

+ + ) : ( + <> + +

+ Whoops, could't find any videos for you +

+ + )} +
+ +
); } 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(null); + const [selected, setSelected] = useState(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(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 ; + if (loading) return ; + if (error) return ; + if (!meta || !selected) + return ( + + + + ); + + // scraping view will start scraping and return with onStream if (!stream) return ( - + ); + + // show stream once we have a stream return (
diff --git a/src/views/notfound/NotFoundChecks.tsx b/src/views/notfound/NotFoundChecks.tsx deleted file mode 100644 index cffd9b85..00000000 --- a/src/views/notfound/NotFoundChecks.tsx +++ /dev/null @@ -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; -} diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index 14cb7829..49584bb3 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -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 (
- + {props.video ? ( +
+ +
+ ) : ( + + )}
{props.children}
diff --git a/src/views/search/SearchResultsView.tsx b/src/views/search/SearchResultsView.tsx index 8638ec71..60a61115 100644 --- a/src/views/search/SearchResultsView.tsx +++ b/src/views/search/SearchResultsView.tsx @@ -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; diff --git a/yarn.lock b/yarn.lock index e3e9dda4..d3d5529c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"