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 { MWStreamQuality, MWStreamType } from "../helpers/streams";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerEmbedScraper({
id: "testembed",
rank: 23,
for: MWEmbedType.OPENLOAD,
async getStream({ progress, url }) {
console.log("scraping url: ", url);
async getStream({ progress }) {
await timeout(1000);
progress(25);
await timeout(1000);
progress(50);
await timeout(1000);
progress(75);
throw new Error("failed to load or something");
await timeout(1000);
return {
streamUrl: "hello-world",
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 {
OPENLOAD = "openload",
ANOTHER = "another",
}
export type MWEmbed = {

View File

@ -1,4 +1,4 @@
import { MWEmbedScraper } from "./embed";
import { MWEmbedScraper, MWEmbedType } from "./embed";
import { MWProvider } from "./provider";
let providers: MWProvider[] = [];
@ -15,8 +15,8 @@ export function registerEmbedScraper(embed: MWEmbedScraper) {
export function initializeScraperStore() {
// sort by ranking
providers = providers.sort((a, b) => a.rank - b.rank);
embeds = embeds.sort((a, b) => a.rank - b.rank);
providers = providers.sort((a, b) => b.rank - a.rank);
embeds = embeds.sort((a, b) => b.rank - a.rank);
// check for invalid ranks
let lastRank: null | number = null;
@ -50,6 +50,11 @@ export function initializeScraperStore() {
const embedIds = embeds.map((v) => v.id);
if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length)
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[] {
@ -59,3 +64,9 @@ export function getProviders(): MWProvider[] {
export function getEmbeds(): MWEmbedScraper[] {
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";
// TODO backend system:
// - run providers/embedscrapers in webworkers for multithreading and isolation
// - caption support
// - hooks to run all providers one by one
// - move over old providers to new system
@ -10,8 +9,11 @@ import { initializeScraperStore } from "./helpers/register";
// providers
// -- nothing here yet
import "./providers/testProvider";
import "./providers/testProviderTwo";
// embeds
// -- nothing here yet
import "./embeds/testEmbedScraper";
import "./embeds/testEmbedScraperTwo";
initializeScraperStore();

View File

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

View File

@ -11,3 +11,8 @@ export type MWMediaMeta = {
poster?: string;
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 { MWStreamQuality, MWStreamType } from "../helpers/streams";
import { MWMediaType } from "../metadata/types";
const timeout = (time: number) =>
new Promise<void>((resolve) => {
setTimeout(() => resolve(), time);
});
registerProvider({
id: "testprov",
rank: 42,
type: [MWMediaType.MOVIE],
async scrape({ progress, imdbId, tmdbId }) {
console.log("scraping provider for: ", imdbId, tmdbId);
async scrape({ progress }) {
await timeout(1000);
progress(25);
await timeout(1000);
progress(50);
await timeout(1000);
progress(75);
await timeout(1000);
// providers can optionally provide a stream themselves,
// incase they host their own streams instead of using embeds
return {
stream: {
streamUrl: "hello-world",
type: MWStreamType.HLS,
quality: MWStreamQuality.Q1080P,
},
embeds: [
{
type: MWEmbedType.OPENLOAD,
url: "https://google.com",
},
// {
// type: MWEmbedType.OPENLOAD,
// 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 { useTranslation } from "react-i18next";
import { MWMediaType, MWQuery } from "@/providers";
import { DropdownButton } from "./buttons/DropdownButton";
import { Icon, Icons } from "./Icon";
import { TextInputControl } from "./text-inputs/TextInputControl";

View File

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

View File

@ -1,10 +1,9 @@
import { Link } from "react-router-dom";
import { DotList } from "@/components/text/DotList";
import { MWSearchResult } from "@/backend/metadata/search";
import { MWMediaType } from "@/providers";
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
export interface MediaCardProps {
media: MWSearchResult;
media: MWMediaMeta;
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 { Episode } from "./EpisodeButton";
export interface WatchedEpisodeProps {
media: MWMedia;
media: MWMediaMeta;
onClick?: () => void;
active?: boolean;
}
export function WatchedEpisode(props: WatchedEpisodeProps) {
const { watched } = useWatchedContext();
const foundWatched = getWatchedFromPortable(watched.items, props.media);
const episode = getEpisodeFromMedia(props.media);
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
return (
<Episode
progress={watchedPercentage}
episodeNumber={episode?.episode?.sort ?? 1}
active={props.active}
onClick={props.onClick}
/>
);
// const { watched } = useWatchedContext();
// const foundWatched = getWatchedFromPortable(watched.items, props.media);
// // const episode = getEpisodeFromMedia(props.media);
// const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
// return (
// <Episode
// progress={watchedPercentage}
// episodeNumber={episode?.episode?.sort ?? 1}
// active={props.active}
// 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";
export interface WatchedMediaCardProps {
media: MWSearchResult;
media: MWMediaMeta;
}
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 { generatePath, useHistory, useRouteMatch } from "react-router-dom";
import { MWMediaType, MWQuery } from "@/providers";
export function useSearchQuery(): [
MWQuery,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { searchForMedia } from "@/backend/metadata/search";
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
import { SourceControl } from "@/components/video/controls/SourceControl";
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { MWMediaType } from "@/providers";
import { useCallback, useState } from "react";
// import { searchForMedia } from "@/backend/metadata/search";
// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
// import { SourceControl } from "@/components/video/controls/SourceControl";
// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
import { useScrape } from "@/hooks/useScrape";
// import { MWMediaType } from "@/providers";
// import { useCallback, useState } from "react";
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
@ -24,37 +25,58 @@ import { useCallback, useState } from "react";
// - devices: ipadOS
// - 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() {
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);
}
const { eventLog, pending, stream } = useScrape();
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>
<p>pending: {pending}</p>
<p>
stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality}
</p>
<hr />
{eventLog.map((v) => (
<div className="rounded-xl p-1 text-white">
<p>
{v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"}
</p>
</div>
))}
</div>
);
}

View File

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

View File

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

View File

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

View File

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