add provider scrape hookiboi

This commit is contained in:
Jelle van Snik 2023-01-12 22:04:28 +01:00
parent 094f9208a8
commit a9ac3e64db
29 changed files with 831 additions and 557 deletions

View File

@ -2,16 +2,25 @@ import { MWEmbedType } from "../helpers/embed";
import { registerEmbedScraper } from "../helpers/register"; import { registerEmbedScraper } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWStreamQuality, MWStreamType } from "../helpers/streams";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerEmbedScraper({ registerEmbedScraper({
id: "testembed", id: "testembed",
rank: 23, rank: 23,
for: MWEmbedType.OPENLOAD, for: MWEmbedType.OPENLOAD,
async getStream({ progress, url }) { async getStream({ progress }) {
console.log("scraping url: ", url); await timeout(1000);
progress(25); progress(25);
await timeout(1000);
progress(50); progress(50);
await timeout(1000);
progress(75); progress(75);
throw new Error("failed to load or something");
await timeout(1000);
return { return {
streamUrl: "hello-world", streamUrl: "hello-world",
type: MWStreamType.MP4, type: MWStreamType.MP4,

View File

@ -0,0 +1,24 @@
import { MWEmbedType } from "../helpers/embed";
import { registerEmbedScraper } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerEmbedScraper({
id: "testembedtwo",
rank: 19,
for: MWEmbedType.ANOTHER,
async getStream({ progress }) {
progress(75);
await timeout(1000);
return {
streamUrl: "hello-world-5",
type: MWStreamType.MP4,
quality: MWStreamQuality.Q1080P,
};
},
});

View File

@ -2,6 +2,7 @@ import { MWStream } from "./streams";
export enum MWEmbedType { export enum MWEmbedType {
OPENLOAD = "openload", OPENLOAD = "openload",
ANOTHER = "another",
} }
export type MWEmbed = { export type MWEmbed = {

View File

@ -1,4 +1,4 @@
import { MWEmbedScraper } from "./embed"; import { MWEmbedScraper, MWEmbedType } from "./embed";
import { MWProvider } from "./provider"; import { MWProvider } from "./provider";
let providers: MWProvider[] = []; let providers: MWProvider[] = [];
@ -15,8 +15,8 @@ export function registerEmbedScraper(embed: MWEmbedScraper) {
export function initializeScraperStore() { export function initializeScraperStore() {
// sort by ranking // sort by ranking
providers = providers.sort((a, b) => a.rank - b.rank); providers = providers.sort((a, b) => b.rank - a.rank);
embeds = embeds.sort((a, b) => a.rank - b.rank); embeds = embeds.sort((a, b) => b.rank - a.rank);
// check for invalid ranks // check for invalid ranks
let lastRank: null | number = null; let lastRank: null | number = null;
@ -50,6 +50,11 @@ export function initializeScraperStore() {
const embedIds = embeds.map((v) => v.id); const embedIds = embeds.map((v) => v.id);
if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length) if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length)
throw new Error("Duplicate IDS in embed scrapers"); throw new Error("Duplicate IDS in embed scrapers");
// check for duplicate embed types
const embedTypes = embeds.map((v) => v.for);
if (embedTypes.length > 0 && new Set(embedTypes).size !== embedTypes.length)
throw new Error("Duplicate types in embed scrapers");
} }
export function getProviders(): MWProvider[] { export function getProviders(): MWProvider[] {
@ -59,3 +64,9 @@ export function getProviders(): MWProvider[] {
export function getEmbeds(): MWEmbedScraper[] { export function getEmbeds(): MWEmbedScraper[] {
return embeds; return embeds;
} }
export function getEmbedScraperByType(
type: MWEmbedType
): MWEmbedScraper | null {
return getEmbeds().find((v) => v.for === type) ?? null;
}

View File

@ -0,0 +1,52 @@
import { MWEmbed, MWEmbedContext, MWEmbedScraper } from "./embed";
import {
MWProvider,
MWProviderContext,
MWProviderScrapeResult,
} from "./provider";
import { getEmbedScraperByType } from "./register";
import { MWStream } from "./streams";
function sortProviderResult(
ctx: MWProviderScrapeResult
): MWProviderScrapeResult {
ctx.embeds = ctx.embeds
.map<[MWEmbed, MWEmbedScraper | null]>((v) => [
v,
v.type ? getEmbedScraperByType(v.type) : null,
])
.sort(([, a], [, b]) => (b?.rank ?? 0) - (a?.rank ?? 0))
.map((v) => v[0]);
return ctx;
}
export async function runProvider(
provider: MWProvider,
ctx: MWProviderContext
): Promise<MWProviderScrapeResult> {
try {
const data = await provider.scrape(ctx);
return sortProviderResult(data);
} catch (err) {
console.error("Failed to run provider", {
id: provider.id,
ctx: { ...ctx },
});
throw err;
}
}
export async function runEmbedScraper(
scraper: MWEmbedScraper,
ctx: MWEmbedContext
): Promise<MWStream> {
try {
return await scraper.getStream(ctx);
} catch (err) {
console.error("Failed to run embed scraper", {
id: scraper.id,
ctx: { ...ctx },
});
throw err;
}
}

View File

@ -0,0 +1,117 @@
import { MWProviderScrapeResult } from "./provider";
import { getEmbedScraperByType, getProviders } from "./register";
import { runEmbedScraper, runProvider } from "./run";
import { MWStream } from "./streams";
interface MWProgressData {
type: "embed" | "provider";
id: string;
percentage: number;
errored: boolean;
}
interface MWNextData {
id: string;
type: "embed" | "provider";
}
export interface MWProviderRunContext {
tmdb: string;
imdb: string;
onProgress?: (data: MWProgressData) => void;
onNext?: (data: MWNextData) => void;
}
async function findBestEmbedStream(
result: MWProviderScrapeResult,
ctx: MWProviderRunContext
): Promise<MWStream | null> {
if (result.stream) return result.stream;
for (const embed of result.embeds) {
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" });
let stream: MWStream;
try {
stream = await runEmbedScraper(scraper, {
url: embed.url,
progress(num) {
ctx.onProgress?.({
errored: false,
id: scraper.id,
percentage: num,
type: "embed",
});
},
});
} catch {
ctx.onProgress?.({
errored: true,
id: scraper.id,
percentage: 100,
type: "embed",
});
continue;
}
ctx.onProgress?.({
errored: false,
id: scraper.id,
percentage: 100,
type: "embed",
});
return stream;
}
return null;
}
export async function findBestStream(
ctx: MWProviderRunContext
): Promise<MWStream | null> {
const providers = getProviders();
for (const provider of providers) {
ctx.onNext?.({ id: provider.id, type: "provider" });
let result: MWProviderScrapeResult;
try {
result = await runProvider(provider, {
imdbId: ctx.imdb,
tmdbId: ctx.tmdb,
progress(num) {
ctx.onProgress?.({
percentage: num,
errored: false,
id: provider.id,
type: "provider",
});
},
});
} catch (err) {
ctx.onProgress?.({
percentage: 100,
errored: true,
id: provider.id,
type: "provider",
});
continue;
}
ctx.onProgress?.({
errored: false,
id: provider.id,
percentage: 100,
type: "provider",
});
const stream = await findBestEmbedStream(result, ctx);
if (!stream) continue;
return stream;
}
return null;
}

View File

@ -1,7 +1,6 @@
import { initializeScraperStore } from "./helpers/register"; import { initializeScraperStore } from "./helpers/register";
// TODO backend system: // TODO backend system:
// - run providers/embedscrapers in webworkers for multithreading and isolation
// - caption support // - caption support
// - hooks to run all providers one by one // - hooks to run all providers one by one
// - move over old providers to new system // - move over old providers to new system
@ -10,8 +9,11 @@ import { initializeScraperStore } from "./helpers/register";
// providers // providers
// -- nothing here yet // -- nothing here yet
import "./providers/testProvider"; import "./providers/testProvider";
import "./providers/testProviderTwo";
// embeds // embeds
// -- nothing here yet // -- nothing here yet
import "./embeds/testEmbedScraper";
import "./embeds/testEmbedScraperTwo";
initializeScraperStore(); initializeScraperStore();

View File

@ -1,5 +1,4 @@
import { MWMediaType, MWQuery } from "@/providers"; import { MWMediaMeta, MWMediaType, MWQuery } from "./types";
import { MWMediaMeta } from "./types";
const JW_API_BASE = "https://apis.justwatch.com"; const JW_API_BASE = "https://apis.justwatch.com";

View File

@ -11,3 +11,8 @@ export type MWMediaMeta = {
poster?: string; poster?: string;
type: MWMediaType; type: MWMediaType;
}; };
export interface MWQuery {
searchQuery: string;
type: MWMediaType;
}

View File

@ -1,32 +1,35 @@
import { MWEmbedType } from "../helpers/embed";
import { registerProvider } from "../helpers/register"; import { registerProvider } from "../helpers/register";
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types"; import { MWMediaType } from "../metadata/types";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerProvider({ registerProvider({
id: "testprov", id: "testprov",
rank: 42, rank: 42,
type: [MWMediaType.MOVIE], type: [MWMediaType.MOVIE],
async scrape({ progress, imdbId, tmdbId }) { async scrape({ progress }) {
console.log("scraping provider for: ", imdbId, tmdbId); await timeout(1000);
progress(25); progress(25);
await timeout(1000);
progress(50); progress(50);
await timeout(1000);
progress(75); progress(75);
await timeout(1000);
// providers can optionally provide a stream themselves,
// incase they host their own streams instead of using embeds
return { return {
stream: {
streamUrl: "hello-world",
type: MWStreamType.HLS,
quality: MWStreamQuality.Q1080P,
},
embeds: [ embeds: [
{ // {
type: MWEmbedType.OPENLOAD, // type: MWEmbedType.OPENLOAD,
url: "https://google.com", // url: "https://google.com",
}, // },
// {
// type: MWEmbedType.ANOTHER,
// url: "https://google.com",
// },
], ],
}; };
}, },

View File

@ -0,0 +1,37 @@
import { MWEmbedType } from "../helpers/embed";
import { registerProvider } from "../helpers/register";
import { MWMediaType } from "../metadata/types";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerProvider({
id: "testprov2",
rank: 40,
type: [MWMediaType.MOVIE],
async scrape({ progress }) {
await timeout(1000);
progress(25);
await timeout(1000);
progress(50);
await timeout(1000);
progress(75);
await timeout(1000);
return {
embeds: [
{
type: MWEmbedType.OPENLOAD,
url: "https://google.com",
},
{
type: MWEmbedType.ANOTHER,
url: "https://google.com",
},
],
};
},
});

View File

@ -1,6 +1,6 @@
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { MWMediaType, MWQuery } from "@/providers";
import { DropdownButton } from "./buttons/DropdownButton"; import { DropdownButton } from "./buttons/DropdownButton";
import { Icon, Icons } from "./Icon"; import { Icon, Icons } from "./Icon";
import { TextInputControl } from "./text-inputs/TextInputControl"; import { TextInputControl } from "./text-inputs/TextInputControl";

View File

@ -7,17 +7,9 @@ import { Icons } from "@/components/Icon";
import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton"; import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { serializePortableMedia } from "@/hooks/usePortableMedia"; import { serializePortableMedia } from "@/hooks/usePortableMedia";
import {
convertMediaToPortable,
MWMedia,
MWMediaSeasons,
MWMediaSeason,
MWPortableMedia,
} from "@/providers";
import { getSeasonDataFromMedia } from "@/providers/methods/seasons";
export interface SeasonsProps { export interface SeasonsProps {
media: MWMedia; media: any;
} }
export function LoadingSeasons(props: { error?: boolean }) { export function LoadingSeasons(props: { error?: boolean }) {
@ -45,80 +37,73 @@ export function LoadingSeasons(props: { error?: boolean }) {
} }
export function Seasons(props: SeasonsProps) { export function Seasons(props: SeasonsProps) {
const { t } = useTranslation(); // const { t } = useTranslation();
// const [searchSeasons, loading, error, success] = useLoading(
const [searchSeasons, loading, error, success] = useLoading( // (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia)
(portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia) // );
); // const history = useHistory();
const history = useHistory(); // const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] }); // const seasonSelected = props.media.seasonId as string;
const seasonSelected = props.media.seasonId as string; // const episodeSelected = props.media.episodeId as string;
const episodeSelected = props.media.episodeId as string; // useEffect(() => {
// (async () => {
useEffect(() => { // const seasonData = await searchSeasons(props.media);
(async () => { // setSeasons(seasonData);
const seasonData = await searchSeasons(props.media); // })();
setSeasons(seasonData); // }, [searchSeasons, props.media]);
})(); // function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
}, [searchSeasons, props.media]); // const newMedia: MWMedia = { ...props.media };
// newMedia.episodeId = episodeId;
function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) { // newMedia.seasonId = seasonId;
const newMedia: MWMedia = { ...props.media }; // history.replace(
newMedia.episodeId = episodeId; // `/media/${newMedia.mediaType}/${serializePortableMedia(
newMedia.seasonId = seasonId; // convertMediaToPortable(newMedia)
history.replace( // )}`
`/media/${newMedia.mediaType}/${serializePortableMedia( // );
convertMediaToPortable(newMedia) // }
)}` // const mapSeason = (season: MWMediaSeason) => ({
); // id: season.id,
} // name: season.title || `${t("seasons.season", { season: season.sort })}`,
// });
const mapSeason = (season: MWMediaSeason) => ({ // const options = seasons.seasons.map(mapSeason);
id: season.id, // const foundSeason = seasons.seasons.find(
name: season.title || `${t("seasons.season", { season: season.sort })}`, // (season) => season.id === seasonSelected
}); // );
// const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
const options = seasons.seasons.map(mapSeason); // return (
// <>
const foundSeason = seasons.seasons.find( // {loading ? <LoadingSeasons /> : null}
(season) => season.id === seasonSelected // {error ? <LoadingSeasons error /> : null}
); // {success && seasons.seasons.length ? (
const selectedItem = foundSeason ? mapSeason(foundSeason) : null; // <>
// <Dropdown
return ( // selectedItem={selectedItem as OptionItem}
<> // options={options}
{loading ? <LoadingSeasons /> : null} // setSelectedItem={(seasonItem) =>
{error ? <LoadingSeasons error /> : null} // navigateToSeasonAndEpisode(
{success && seasons.seasons.length ? ( // seasonItem.id,
<> // seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
<Dropdown // .id as string
selectedItem={selectedItem as OptionItem} // )
options={options} // }
setSelectedItem={(seasonItem) => // />
navigateToSeasonAndEpisode( // {seasons.seasons
seasonItem.id, // .find((s) => s.id === seasonSelected)
seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0] // ?.episodes.map((v) => (
.id as string // <WatchedEpisode
) // key={v.id}
} // media={{
/> // ...props.media,
{seasons.seasons // seriesData: seasons,
.find((s) => s.id === seasonSelected) // episodeId: v.id,
?.episodes.map((v) => ( // seasonId: seasonSelected,
<WatchedEpisode // }}
key={v.id} // active={v.id === episodeSelected}
media={{ // onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
...props.media, // />
seriesData: seasons, // ))}
episodeId: v.id, // </>
seasonId: seasonSelected, // ) : null}
}} // </>
active={v.id === episodeSelected} // );
onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
/>
))}
</>
) : null}
</>
);
} }

View File

@ -1,10 +1,9 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { DotList } from "@/components/text/DotList"; import { DotList } from "@/components/text/DotList";
import { MWSearchResult } from "@/backend/metadata/search"; import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
import { MWMediaType } from "@/providers";
export interface MediaCardProps { export interface MediaCardProps {
media: MWSearchResult; media: MWMediaMeta;
linkable?: boolean; linkable?: boolean;
} }

View File

@ -1,25 +1,24 @@
import { getEpisodeFromMedia, MWMedia } from "@/providers"; import { MWMediaMeta } from "@/backend/metadata/types";
import { useWatchedContext, getWatchedFromPortable } from "@/state/watched"; import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
import { Episode } from "./EpisodeButton"; import { Episode } from "./EpisodeButton";
export interface WatchedEpisodeProps { export interface WatchedEpisodeProps {
media: MWMedia; media: MWMediaMeta;
onClick?: () => void; onClick?: () => void;
active?: boolean; active?: boolean;
} }
export function WatchedEpisode(props: WatchedEpisodeProps) { export function WatchedEpisode(props: WatchedEpisodeProps) {
const { watched } = useWatchedContext(); // const { watched } = useWatchedContext();
const foundWatched = getWatchedFromPortable(watched.items, props.media); // const foundWatched = getWatchedFromPortable(watched.items, props.media);
const episode = getEpisodeFromMedia(props.media); // // const episode = getEpisodeFromMedia(props.media);
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; // const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
// return (
return ( // <Episode
<Episode // progress={watchedPercentage}
progress={watchedPercentage} // episodeNumber={episode?.episode?.sort ?? 1}
episodeNumber={episode?.episode?.sort ?? 1} // active={props.active}
active={props.active} // onClick={props.onClick}
onClick={props.onClick} // />
/> // );
);
} }

View File

@ -1,8 +1,8 @@
import { MWSearchResult } from "@/backend/metadata/search"; import { MWMediaMeta } from "@/backend/metadata/types";
import { MediaCard } from "./MediaCard"; import { MediaCard } from "./MediaCard";
export interface WatchedMediaCardProps { export interface WatchedMediaCardProps {
media: MWSearchResult; media: MWMediaMeta;
} }
export function WatchedMediaCard(props: WatchedMediaCardProps) { export function WatchedMediaCard(props: WatchedMediaCardProps) {

View File

@ -1,30 +0,0 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { MWPortableMedia } from "@/providers";
export function deserializePortableMedia(media: string): MWPortableMedia {
return JSON.parse(atob(decodeURIComponent(media)));
}
export function serializePortableMedia(media: MWPortableMedia): string {
const data = encodeURIComponent(btoa(JSON.stringify(media)));
return data;
}
export function usePortableMedia(): MWPortableMedia | undefined {
const { media } = useParams<{ media: string }>();
const [mediaObject, setMediaObject] = useState<MWPortableMedia | undefined>(
undefined
);
useEffect(() => {
try {
setMediaObject(deserializePortableMedia(media));
} catch (err) {
console.error("Failed to deserialize portable media", err);
setMediaObject(undefined);
}
}, [media, setMediaObject]);
return mediaObject;
}

59
src/hooks/useScrape.ts Normal file
View File

@ -0,0 +1,59 @@
import { findBestStream } from "@/backend/helpers/scrape";
import { MWStream } from "@/backend/helpers/streams";
import { useEffect, useState } from "react";
interface ScrapeEventLog {
type: "provider" | "embed";
errored: boolean;
percentage: number;
id: string;
}
export function useScrape() {
const [eventLog, setEventLog] = useState<ScrapeEventLog[]>([]);
const [stream, setStream] = useState<MWStream | null>(null);
const [pending, setPending] = useState(true);
useEffect(() => {
setPending(true);
setStream(null);
setEventLog([]);
(async () => {
// TODO has test inputs
const scrapedStream = await findBestStream({
imdb: "test1",
tmdb: "test2",
onNext(ctx) {
setEventLog((arr) => [
...arr,
{
errored: false,
id: ctx.id,
type: ctx.type,
percentage: 0,
},
]);
},
onProgress(ctx) {
setEventLog((arr) => {
const item = arr.reverse().find((v) => v.id === ctx.id);
if (item) {
item.errored = ctx.errored;
item.percentage = ctx.percentage;
}
return [...arr];
});
},
});
setPending(false);
setStream(scrapedStream);
})();
}, []);
return {
stream,
pending,
eventLog,
};
}

View File

@ -1,6 +1,6 @@
import { MWMediaType, MWQuery } from "@/backend/metadata/types";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
import { MWMediaType, MWQuery } from "@/providers";
export function useSearchQuery(): [ export function useSearchQuery(): [
MWQuery, MWQuery,

View File

@ -1,5 +1,4 @@
import { Redirect, Route, Switch } from "react-router-dom"; import { Redirect, Route, Switch } from "react-router-dom";
import { MWMediaType } from "@/providers";
import { BookmarkContextProvider } from "@/state/bookmark"; import { BookmarkContextProvider } from "@/state/bookmark";
import { WatchedContextProvider } from "@/state/watched"; import { WatchedContextProvider } from "@/state/watched";
@ -7,6 +6,7 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView";
import { MediaView } from "@/views/MediaView"; import { MediaView } from "@/views/MediaView";
import { SearchView } from "@/views/search/SearchView"; import { SearchView } from "@/views/search/SearchView";
import { TestView } from "@/views/TestView"; import { TestView } from "@/views/TestView";
import { MWMediaType } from "@/backend/metadata/types";
function App() { function App() {
return ( return (

View File

@ -1,3 +1,4 @@
import { MWMediaMeta } from "@/backend/metadata/types";
import { import {
createContext, createContext,
ReactNode, ReactNode,
@ -6,7 +7,6 @@ import {
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
import { getProviderMetadata, MWMediaMeta } from "@/providers";
import { BookmarkStore } from "./store"; import { BookmarkStore } from "./store";
interface BookmarkStoreData { interface BookmarkStoreData {
@ -64,7 +64,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
const contextValue = useMemo( const contextValue = useMemo(
() => ({ () => ({
setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { setItemBookmark(media: any, bookmarked: boolean) {
setBookmarked((data: BookmarkStoreData) => { setBookmarked((data: BookmarkStoreData) => {
if (bookmarked) { if (bookmarked) {
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media); const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
@ -90,9 +90,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
}); });
}, },
getFilteredBookmarks() { getFilteredBookmarks() {
return bookmarkStorage.bookmarks.filter( return [];
(bookmark) => getProviderMetadata(bookmark.providerId)?.enabled
);
}, },
bookmarkStore: bookmarkStorage, bookmarkStore: bookmarkStorage,
}), }),

View File

@ -1,3 +1,4 @@
import { MWMediaMeta } from "@/backend/metadata/types";
import React, { import React, {
createContext, createContext,
ReactNode, ReactNode,
@ -6,7 +7,6 @@ import React, {
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "@/providers";
import { VideoProgressStore } from "./store"; import { VideoProgressStore } from "./store";
interface WatchedStoreItem extends MWMediaMeta { interface WatchedStoreItem extends MWMediaMeta {
@ -28,13 +28,7 @@ export function getWatchedFromPortable(
items: WatchedStoreItem[], items: WatchedStoreItem[],
media: MWMediaMeta media: MWMediaMeta
): WatchedStoreItem | undefined { ): WatchedStoreItem | undefined {
return items.find( return undefined;
(v) =>
v.mediaId === media.mediaId &&
v.providerId === media.providerId &&
v.episodeId === media.episodeId &&
v.seasonId === media.seasonId
);
} }
const WatchedContext = createContext<WatchedStoreDataWrapper>({ const WatchedContext = createContext<WatchedStoreDataWrapper>({
@ -73,76 +67,73 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
progress: number, progress: number,
total: number total: number
): void { ): void {
setWatched((data: WatchedStoreData) => { // setWatched((data: WatchedStoreData) => {
let item = getWatchedFromPortable(data.items, media); // let item = getWatchedFromPortable(data.items, media);
if (!item) { // if (!item) {
item = { // item = {
mediaId: media.mediaId, // mediaId: media.mediaId,
mediaType: media.mediaType, // mediaType: media.mediaType,
providerId: media.providerId, // providerId: media.providerId,
title: media.title, // title: media.title,
year: media.year, // year: media.year,
percentage: 0, // percentage: 0,
progress: 0, // progress: 0,
episodeId: media.episodeId, // episodeId: media.episodeId,
seasonId: media.seasonId, // seasonId: media.seasonId,
}; // };
data.items.push(item); // data.items.push(item);
} // }
// // update actual item
// update actual item // item.progress = progress;
item.progress = progress; // item.percentage = Math.round((progress / total) * 100);
item.percentage = Math.round((progress / total) * 100); // return data;
// });
return data;
});
}, },
getFilteredWatched() { getFilteredWatched() {
// remove disabled providers // remove disabled providers
let filtered = watched.items.filter( // let filtered = watched.items.filter(
(item) => getProviderMetadata(item.providerId)?.enabled // (item) => getProviderMetadata(item.providerId)?.enabled
); // );
// // get highest episode number for every anime/season
// get highest episode number for every anime/season // const highestEpisode: Record<string, [number, number]> = {};
const highestEpisode: Record<string, [number, number]> = {}; // const highestWatchedItem: Record<string, WatchedStoreItem> = {};
const highestWatchedItem: Record<string, WatchedStoreItem> = {}; // filtered = filtered.filter((item) => {
filtered = filtered.filter((item) => { // if (
if ( // [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType)
[MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType) // ) {
) { // const key = `${item.mediaType}-${item.mediaId}`;
const key = `${item.mediaType}-${item.mediaId}`; // const current: [number, number] = [
const current: [number, number] = [ // item.episodeId ? parseInt(item.episodeId, 10) : -1,
item.episodeId ? parseInt(item.episodeId, 10) : -1, // item.seasonId ? parseInt(item.seasonId, 10) : -1,
item.seasonId ? parseInt(item.seasonId, 10) : -1, // ];
]; // let existing = highestEpisode[key];
let existing = highestEpisode[key]; // if (!existing) {
if (!existing) { // existing = current;
existing = current; // highestEpisode[key] = current;
highestEpisode[key] = current; // highestWatchedItem[key] = item;
highestWatchedItem[key] = item; // }
} // if (
// current[0] > existing[0] ||
if ( // (current[0] === existing[0] && current[1] > existing[1])
current[0] > existing[0] || // ) {
(current[0] === existing[0] && current[1] > existing[1]) // highestEpisode[key] = current;
) { // highestWatchedItem[key] = item;
highestEpisode[key] = current; // }
highestWatchedItem[key] = item; // return false;
} // }
return false; // return true;
} // });
return true; // return [...filtered, ...Object.values(highestWatchedItem)];
});
return [...filtered, ...Object.values(highestWatchedItem)];
}, },
watched, watched,
}), }),
[watched, setWatched] [
/*watched, setWatched*/
]
); );
return ( return (
<WatchedContext.Provider value={contextValue}> <WatchedContext.Provider value={contextValue as any}>
{props.children} {props.children}
</WatchedContext.Provider> </WatchedContext.Provider>
); );

View File

@ -1,6 +1,4 @@
import { MWMediaType } from "@/providers";
import { versionedStoreBuilder } from "@/utils/storage"; import { versionedStoreBuilder } from "@/utils/storage";
import { WatchedStoreData } from "./context";
export const VideoProgressStore = versionedStoreBuilder() export const VideoProgressStore = versionedStoreBuilder()
.setKey("video-progress") .setKey("video-progress")
@ -9,79 +7,79 @@ export const VideoProgressStore = versionedStoreBuilder()
}) })
.addVersion({ .addVersion({
version: 1, version: 1,
migrate(data: any) { migrate() {
const output: WatchedStoreData = { items: [] }; // const output: WatchedStoreData = { items: [] };
if (!data || data.constructor !== Object) return output; // if (!data || data.constructor !== Object) return output;
Object.keys(data).forEach((scraperId) => { // Object.keys(data).forEach((scraperId) => {
if (scraperId === "--version") return; // if (scraperId === "--version") return;
if (scraperId === "save") return; // if (scraperId === "save") return;
if ( // if (
data[scraperId].movie && // data[scraperId].movie &&
data[scraperId].movie.constructor === Object // data[scraperId].movie.constructor === Object
) { // ) {
Object.keys(data[scraperId].movie).forEach((movieId) => { // Object.keys(data[scraperId].movie).forEach((movieId) => {
try { // try {
output.items.push({ // output.items.push({
mediaId: movieId.includes("player.php") // mediaId: movieId.includes("player.php")
? movieId.split("player.php%3Fimdb%3D")[1] // ? movieId.split("player.php%3Fimdb%3D")[1]
: movieId, // : movieId,
mediaType: MWMediaType.MOVIE, // mediaType: MWMediaType.MOVIE,
providerId: scraperId, // providerId: scraperId,
title: data[scraperId].movie[movieId].full.meta.title, // title: data[scraperId].movie[movieId].full.meta.title,
year: data[scraperId].movie[movieId].full.meta.year, // year: data[scraperId].movie[movieId].full.meta.year,
progress: data[scraperId].movie[movieId].full.currentlyAt, // progress: data[scraperId].movie[movieId].full.currentlyAt,
percentage: Math.round( // percentage: Math.round(
(data[scraperId].movie[movieId].full.currentlyAt / // (data[scraperId].movie[movieId].full.currentlyAt /
data[scraperId].movie[movieId].full.totalDuration) * // data[scraperId].movie[movieId].full.totalDuration) *
100 // 100
), // ),
}); // });
} catch (err) { // } catch (err) {
console.error( // console.error(
`Failed to migrate movie: ${scraperId}/${movieId}`, // `Failed to migrate movie: ${scraperId}/${movieId}`,
data[scraperId].movie[movieId] // data[scraperId].movie[movieId]
); // );
} // }
}); // });
} // }
if ( // if (
data[scraperId].show && // data[scraperId].show &&
data[scraperId].show.constructor === Object // data[scraperId].show.constructor === Object
) { // ) {
Object.keys(data[scraperId].show).forEach((showId) => { // Object.keys(data[scraperId].show).forEach((showId) => {
if (data[scraperId].show[showId].constructor !== Object) return; // if (data[scraperId].show[showId].constructor !== Object) return;
Object.keys(data[scraperId].show[showId]).forEach((episodeId) => { // Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
try { // try {
output.items.push({ // output.items.push({
mediaId: showId, // mediaId: showId,
mediaType: MWMediaType.SERIES, // mediaType: MWMediaType.SERIES,
providerId: scraperId, // providerId: scraperId,
title: data[scraperId].show[showId][episodeId].meta.title, // title: data[scraperId].show[showId][episodeId].meta.title,
year: data[scraperId].show[showId][episodeId].meta.year, // year: data[scraperId].show[showId][episodeId].meta.year,
percentage: Math.round( // percentage: Math.round(
(data[scraperId].show[showId][episodeId].currentlyAt / // (data[scraperId].show[showId][episodeId].currentlyAt /
data[scraperId].show[showId][episodeId].totalDuration) * // data[scraperId].show[showId][episodeId].totalDuration) *
100 // 100
), // ),
progress: data[scraperId].show[showId][episodeId].currentlyAt, // progress: data[scraperId].show[showId][episodeId].currentlyAt,
episodeId: // episodeId:
data[scraperId].show[showId][episodeId].show.episode, // data[scraperId].show[showId][episodeId].show.episode,
seasonId: data[scraperId].show[showId][episodeId].show.season, // seasonId: data[scraperId].show[showId][episodeId].show.season,
}); // });
} catch (err) { // } catch (err) {
console.error( // console.error(
`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`, // `Failed to migrate series: ${scraperId}/${showId}/${episodeId}`,
data[scraperId].show[showId][episodeId] // data[scraperId].show[showId][episodeId]
); // );
} // }
}); // });
}); // });
} // }
}); // });
return output; return output;
}, },

View File

@ -1,220 +1,215 @@
import { ReactElement, useCallback, useEffect, useState } from "react"; // import { ReactElement, useCallback, useEffect, useState } from "react";
import { useHistory } from "react-router-dom"; // import { useHistory } from "react-router-dom";
// import { useTranslation } from "react-i18next";
// import { IconPatch } from "@/components/buttons/IconPatch";
// import { Icons } from "@/components/Icon";
// import { Navigation } from "@/components/layout/Navigation";
// import { Paper } from "@/components/layout/Paper";
// import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
// import { ArrowLink } from "@/components/text/ArrowLink";
// import { DotList } from "@/components/text/DotList";
// import { Title } from "@/components/text/Title";
// import { useLoading } from "@/hooks/useLoading";
// import { usePortableMedia } from "@/hooks/usePortableMedia";
// import {
// getIfBookmarkedFromPortable,
// useBookmarkContext,
// } from "@/state/bookmark";
// import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
// import { SourceControl } from "@/components/video/controls/SourceControl";
// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
// import { Loading } from "@/components/layout/Loading";
// import { NotFoundChecks } from "./notfound/NotFoundChecks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconPatch } from "@/components/buttons/IconPatch"; import { useHistory } from "react-router-dom";
import { Icons } from "@/components/Icon";
import { Navigation } from "@/components/layout/Navigation"; import { Navigation } from "@/components/layout/Navigation";
import { Paper } from "@/components/layout/Paper";
import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { ArrowLink } from "@/components/text/ArrowLink"; import { ArrowLink } from "@/components/text/ArrowLink";
import { DotList } from "@/components/text/DotList";
import { Title } from "@/components/text/Title";
import { useLoading } from "@/hooks/useLoading";
import { usePortableMedia } from "@/hooks/usePortableMedia";
import {
MWPortableMedia,
getStream,
MWMediaStream,
MWMedia,
convertPortableToMedia,
getProviderFromId,
MWMediaProvider,
MWMediaType,
} from "@/providers";
import {
getIfBookmarkedFromPortable,
useBookmarkContext,
} from "@/state/bookmark";
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
import { SourceControl } from "@/components/video/controls/SourceControl";
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
import { Loading } from "@/components/layout/Loading";
import { NotFoundChecks } from "./notfound/NotFoundChecks";
interface StyledMediaViewProps { // interface StyledMediaViewProps {
media: MWMedia; // media: MWMedia;
stream: MWMediaStream; // stream: MWMediaStream;
} // }
export function SkeletonVideoPlayer(props: { error?: boolean }) { // export function SkeletonVideoPlayer(props: { error?: boolean }) {
return ( // return (
<div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl"> // <div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl">
{props.error ? ( // {props.error ? (
<div className="flex flex-col items-center"> // <div className="flex flex-col items-center">
<IconPatch icon={Icons.WARNING} className="text-red-400" /> // <IconPatch icon={Icons.WARNING} className="text-red-400" />
<p className="mt-5 text-white">Couldn&apos;t get your stream</p> // <p className="mt-5 text-white">Couldn&apos;t get your stream</p>
</div> // </div>
) : ( // ) : (
<div className="flex flex-col items-center"> // <div className="flex flex-col items-center">
<Loading /> // <Loading />
<p className="mt-3 text-white">Getting your stream...</p> // <p className="mt-3 text-white">Getting your stream...</p>
</div> // </div>
)} // )}
</div> // </div>
); // );
} // }
function StyledMediaView(props: StyledMediaViewProps) { // function StyledMediaView(props: StyledMediaViewProps) {
const reactHistory = useHistory(); // const reactHistory = useHistory();
const watchedStore = useWatchedContext(); // const watchedStore = useWatchedContext();
const startAtTime: number | undefined = getWatchedFromPortable( // const startAtTime: number | undefined = getWatchedFromPortable(
watchedStore.watched.items, // watchedStore.watched.items,
props.media // props.media
)?.progress; // )?.progress;
const updateProgress = useCallback( // const updateProgress = useCallback(
(time: number, duration: number) => { // (time: number, duration: number) => {
// Don't update stored progress if less than 30s into the video // // Don't update stored progress if less than 30s into the video
if (time <= 30) return; // if (time <= 30) return;
watchedStore.updateProgress(props.media, time, duration); // watchedStore.updateProgress(props.media, time, duration);
}, // },
[props, watchedStore] // [props, watchedStore]
); // );
const goBack = useCallback(() => { // const goBack = useCallback(() => {
if (reactHistory.action !== "POP") reactHistory.goBack(); // if (reactHistory.action !== "POP") reactHistory.goBack();
else reactHistory.push("/"); // else reactHistory.push("/");
}, [reactHistory]); // }, [reactHistory]);
return ( // return (
<div className="overflow-hidden lg:rounded-xl"> // <div className="overflow-hidden lg:rounded-xl">
<DecoratedVideoPlayer title={props.media.title} onGoBack={goBack}> // <DecoratedVideoPlayer title={props.media.title} onGoBack={goBack}>
<SourceControl source={props.stream.url} type={props.stream.type} /> // <SourceControl source={props.stream.url} type={props.stream.type} />
<ProgressListenerControl // <ProgressListenerControl
startAt={startAtTime} // startAt={startAtTime}
onProgress={updateProgress} // onProgress={updateProgress}
/> // />
</DecoratedVideoPlayer> // </DecoratedVideoPlayer>
</div> // </div>
); // );
} // }
interface StyledMediaFooterProps { // interface StyledMediaFooterProps {
media: MWMedia; // media: MWMedia;
provider: MWMediaProvider; // provider: MWMediaProvider;
} // }
function StyledMediaFooter(props: StyledMediaFooterProps) { // function StyledMediaFooter(props: StyledMediaFooterProps) {
const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext(); // const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext();
const isBookmarked = getIfBookmarkedFromPortable( // const isBookmarked = getIfBookmarkedFromPortable(
getFilteredBookmarks(), // getFilteredBookmarks(),
props.media // props.media
); // );
return ( // return (
<Paper className="mt-5"> // <Paper className="mt-5">
<div className="flex"> // <div className="flex">
<div className="flex-1"> // <div className="flex-1">
<Title>{props.media.title}</Title> // <Title>{props.media.title}</Title>
<DotList // <DotList
className="mt-3 text-sm" // className="mt-3 text-sm"
content={[ // content={[
props.provider.displayName, // props.provider.displayName,
props.media.mediaType, // props.media.mediaType,
props.media.year, // props.media.year,
]} // ]}
/> // />
</div> // </div>
<div> // <div>
<IconPatch // <IconPatch
icon={Icons.BOOKMARK} // icon={Icons.BOOKMARK}
active={isBookmarked} // active={isBookmarked}
onClick={() => setItemBookmark(props.media, !isBookmarked)} // onClick={() => setItemBookmark(props.media, !isBookmarked)}
clickable // clickable
/> // />
</div> // </div>
</div> // </div>
{props.media.mediaType !== MWMediaType.MOVIE ? ( // {props.media.mediaType !== MWMediaType.MOVIE ? (
<Seasons media={props.media} /> // <Seasons media={props.media} />
) : null} // ) : null}
</Paper> // </Paper>
); // );
} // }
function LoadingMediaFooter(props: { error?: boolean }) { // function LoadingMediaFooter(props: { error?: boolean }) {
const { t } = useTranslation(); // const { t } = useTranslation();
return ( // return (
<Paper className="mt-5"> // <Paper className="mt-5">
<div className="flex"> // <div className="flex">
<div className="flex-1"> // <div className="flex-1">
<div className="mb-2 h-4 w-48 rounded-full bg-denim-500" /> // <div className="mb-2 h-4 w-48 rounded-full bg-denim-500" />
<div> // <div>
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" /> // <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" /> // <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
</div> // </div>
{props.error ? ( // {props.error ? (
<div className="flex items-center space-x-3"> // <div className="flex items-center space-x-3">
<IconPatch icon={Icons.WARNING} className="text-red-400" /> // <IconPatch icon={Icons.WARNING} className="text-red-400" />
<p>{t("media.invalidUrl")}</p> // <p>{t("media.invalidUrl")}</p>
</div> // </div>
) : ( // ) : (
<LoadingSeasons /> // <LoadingSeasons />
)} // )}
</div> // </div>
</div> // </div>
</Paper> // </Paper>
); // );
} // }
function MediaViewContent(props: { portable: MWPortableMedia }) { // function MediaViewContent(props: { portable: MWPortableMedia }) {
const mediaPortable = props.portable; // const mediaPortable = props.portable;
const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>(); // const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>();
const [media, setMedia] = useState<MWMedia | undefined>(); // const [media, setMedia] = useState<MWMedia | undefined>();
const [fetchMedia, loadingPortable, errorPortable] = useLoading( // const [fetchMedia, loadingPortable, errorPortable] = useLoading(
(portable: MWPortableMedia) => convertPortableToMedia(portable) // (portable: MWPortableMedia) => convertPortableToMedia(portable)
); // );
const [fetchStream, loadingStream, errorStream] = useLoading( // const [fetchStream, loadingStream, errorStream] = useLoading(
(portable: MWPortableMedia) => getStream(portable) // (portable: MWPortableMedia) => getStream(portable)
); // );
useEffect(() => { // useEffect(() => {
(async () => { // (async () => {
if (mediaPortable) { // if (mediaPortable) {
setMedia(await fetchMedia(mediaPortable)); // setMedia(await fetchMedia(mediaPortable));
} // }
})(); // })();
}, [mediaPortable, setMedia, fetchMedia]); // }, [mediaPortable, setMedia, fetchMedia]);
useEffect(() => { // useEffect(() => {
(async () => { // (async () => {
if (mediaPortable) { // if (mediaPortable) {
setStreamUrl(await fetchStream(mediaPortable)); // setStreamUrl(await fetchStream(mediaPortable));
} // }
})(); // })();
}, [mediaPortable, setStreamUrl, fetchStream]); // }, [mediaPortable, setStreamUrl, fetchStream]);
let playerContent: ReactElement | null = null; // let playerContent: ReactElement | null = null;
if (loadingStream) playerContent = <SkeletonVideoPlayer />; // if (loadingStream) playerContent = <SkeletonVideoPlayer />;
else if (errorStream) playerContent = <SkeletonVideoPlayer error />; // else if (errorStream) playerContent = <SkeletonVideoPlayer error />;
else if (media && streamUrl) // else if (media && streamUrl)
playerContent = <StyledMediaView media={media} stream={streamUrl} />; // playerContent = <StyledMediaView media={media} stream={streamUrl} />;
let footerContent: ReactElement | null = null; // let footerContent: ReactElement | null = null;
if (loadingPortable) footerContent = <LoadingMediaFooter />; // if (loadingPortable) footerContent = <LoadingMediaFooter />;
else if (errorPortable) footerContent = <LoadingMediaFooter error />; // else if (errorPortable) footerContent = <LoadingMediaFooter error />;
else if (mediaPortable && media) // else if (mediaPortable && media)
footerContent = ( // footerContent = (
<StyledMediaFooter // <StyledMediaFooter
provider={ // provider={
getProviderFromId(mediaPortable.providerId) as MWMediaProvider // getProviderFromId(mediaPortable.providerId) as MWMediaProvider
} // }
media={media} // media={media}
/> // />
); // );
return ( // return (
<> // <>
{playerContent} // {playerContent}
{footerContent} // {footerContent}
</> // </>
); // );
} // }
export function MediaView() { export function MediaView() {
const { t } = useTranslation(); const { t } = useTranslation();
const mediaPortable: MWPortableMedia | undefined = usePortableMedia(); // const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
const reactHistory = useHistory(); const reactHistory = useHistory();
return ( return (
@ -230,11 +225,11 @@ export function MediaView() {
linkText={t("media.arrowText")} linkText={t("media.arrowText")}
/> />
</Navigation> </Navigation>
<NotFoundChecks portable={mediaPortable}> {/* <NotFoundChecks portable={mediaPortable}>
<div className="container mx-auto mt-40 mb-16 max-w-[1100px]"> <div className="container mx-auto mt-40 mb-16 max-w-[1100px]">
<MediaViewContent portable={mediaPortable as MWPortableMedia} /> <MediaViewContent portable={mediaPortable as MWPortableMedia} />
</div> </div>
</NotFoundChecks> </NotFoundChecks> */}
</div> </div>
); );
} }

View File

@ -1,9 +1,10 @@
import { searchForMedia } from "@/backend/metadata/search"; // import { searchForMedia } from "@/backend/metadata/search";
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl"; // import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
import { SourceControl } from "@/components/video/controls/SourceControl"; // import { SourceControl } from "@/components/video/controls/SourceControl";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer"; // import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWMediaType } from "@/providers"; import { useScrape } from "@/hooks/useScrape";
import { useCallback, useState } from "react"; // import { MWMediaType } from "@/providers";
// import { useCallback, useState } from "react";
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657 // test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
@ -24,37 +25,58 @@ import { useCallback, useState } from "react";
// - devices: ipadOS // - devices: ipadOS
// - features: HLS, error handling, preload interactions // - features: HLS, error handling, preload interactions
// export function TestView() {
// const [show, setShow] = useState(true);
// const handleClick = useCallback(() => {
// setShow((v) => !v);
// }, [setShow]);
// if (!show) {
// return <p onClick={handleClick}>Click me to show</p>;
// }
// async function search() {
// const test = await searchForMedia({
// searchQuery: "tron",
// type: MWMediaType.MOVIE,
// });
// console.log(test);
// }
// return (
// <div className="w-[40rem] max-w-full">
// <DecoratedVideoPlayer>
// <SourceControl
// source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
// type="mp4"
// />
// <ProgressListenerControl
// startAt={283}
// onProgress={(a, b) => console.log(a, b)}
// />
// </DecoratedVideoPlayer>
// <p onClick={() => search()}>click me to search</p>
// </div>
// );
// }
export function TestView() { export function TestView() {
const [show, setShow] = useState(true); const { eventLog, pending, stream } = useScrape();
const handleClick = useCallback(() => {
setShow((v) => !v);
}, [setShow]);
if (!show) {
return <p onClick={handleClick}>Click me to show</p>;
}
async function search() {
const test = await searchForMedia({
searchQuery: "tron",
type: MWMediaType.MOVIE,
});
console.log(test);
}
return ( return (
<div className="w-[40rem] max-w-full"> <div>
<DecoratedVideoPlayer> <p>pending: {pending}</p>
<SourceControl <p>
source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality}
type="mp4" </p>
/> <hr />
<ProgressListenerControl {eventLog.map((v) => (
startAt={283} <div className="rounded-xl p-1 text-white">
onProgress={(a, b) => console.log(a, b)} <p>
/> {v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"}
</DecoratedVideoPlayer> </p>
<p onClick={() => search()}>click me to search</p> </div>
))}
</div> </div>
); );
} }

View File

@ -1,9 +1,8 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import { getProviderMetadata, MWPortableMedia } from "@/providers"; // import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
export interface NotFoundChecksProps { export interface NotFoundChecksProps {
portable: MWPortableMedia | undefined; // portable: MWPortableMedia | undefined;
children?: ReactElement; children?: ReactElement;
} }
@ -13,17 +12,17 @@ export interface NotFoundChecksProps {
export function NotFoundChecks( export function NotFoundChecks(
props: NotFoundChecksProps props: NotFoundChecksProps
): ReactElement | null { ): ReactElement | null {
const providerMeta = props.portable // const providerMeta = props.portable
? getProviderMetadata(props.portable.providerId) // ? getProviderMetadata(props.portable.providerId)
: undefined; // : undefined;
if (!providerMeta || !providerMeta.exists) { // if (!providerMeta || !providerMeta.exists) {
return <NotFoundMedia />; // return <NotFoundMedia />;
} // }
if (!providerMeta.enabled) { // if (!providerMeta.enabled) {
return <NotFoundProvider />; // return <NotFoundProvider />;
} // }
return props.children || null; return props.children || null;
} }

View File

@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { SectionHeading } from "@/components/layout/SectionHeading"; import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid"; import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { import {
getIfBookmarkedFromPortable, getIfBookmarkedFromPortable,
useBookmarkContext, useBookmarkContext,
@ -22,12 +21,12 @@ function Bookmarks() {
icon={Icons.BOOKMARK} icon={Icons.BOOKMARK}
> >
<MediaGrid> <MediaGrid>
{bookmarks.map((v) => ( {/* {bookmarks.map((v) => (
<WatchedMediaCard <WatchedMediaCard
key={[v.mediaId, v.providerId].join("|")} key={[v.mediaId, v.providerId].join("|")}
media={v} media={v}
/> />
))} ))} */}
</MediaGrid> </MediaGrid>
</SectionHeading> </SectionHeading>
); );
@ -51,13 +50,13 @@ function Watched() {
icon={Icons.CLOCK} icon={Icons.CLOCK}
> >
<MediaGrid> <MediaGrid>
{watchedItems.map((v) => ( {/* {watchedItems.map((v) => (
<WatchedMediaCard <WatchedMediaCard
key={[v.mediaId, v.providerId].join("|")} key={[v.mediaId, v.providerId].join("|")}
media={v} media={v}
series series
/> />
))} ))} */}
</MediaGrid> </MediaGrid>
</SectionHeading> </SectionHeading>
); );

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useDebounce } from "@/hooks/useDebounce"; import { useDebounce } from "@/hooks/useDebounce";
import { MWQuery } from "@/providers"; import { MWQuery } from "@/backend/metadata/types";
import { HomeView } from "./HomeView"; import { HomeView } from "./HomeView";
import { SearchLoadingView } from "./SearchLoadingView"; import { SearchLoadingView } from "./SearchLoadingView";
import { SearchResultsView } from "./SearchResultsView"; import { SearchResultsView } from "./SearchResultsView";

View File

@ -6,8 +6,8 @@ import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid"; import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useLoading } from "@/hooks/useLoading"; import { useLoading } from "@/hooks/useLoading";
import { MWQuery } from "@/providers"; import { searchForMedia } from "@/backend/metadata/search";
import { MWSearchResult, searchForMedia } from "@/backend/metadata/search"; import { MWMediaMeta, MWQuery } from "@/backend/metadata/types";
import { SearchLoadingView } from "./SearchLoadingView"; import { SearchLoadingView } from "./SearchLoadingView";
function SearchSuffix(props: { failed?: boolean; results?: number }) { function SearchSuffix(props: { failed?: boolean; results?: number }) {
@ -46,7 +46,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) { export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [results, setResults] = useState<MWSearchResult[]>([]); const [results, setResults] = useState<MWMediaMeta[]>([]);
const [runSearchQuery, loading, error] = useLoading((query: MWQuery) => const [runSearchQuery, loading, error] = useLoading((query: MWQuery) =>
searchForMedia(query) searchForMedia(query)
); );