mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 04:11:52 +01:00
add provider scrape hookiboi
This commit is contained in:
parent
094f9208a8
commit
a9ac3e64db
@ -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,
|
||||
|
24
src/backend/embeds/testEmbedScraperTwo.ts
Normal file
24
src/backend/embeds/testEmbedScraperTwo.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
@ -2,6 +2,7 @@ import { MWStream } from "./streams";
|
||||
|
||||
export enum MWEmbedType {
|
||||
OPENLOAD = "openload",
|
||||
ANOTHER = "another",
|
||||
}
|
||||
|
||||
export type MWEmbed = {
|
||||
|
@ -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;
|
||||
}
|
||||
|
52
src/backend/helpers/run.ts
Normal file
52
src/backend/helpers/run.ts
Normal 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;
|
||||
}
|
||||
}
|
117
src/backend/helpers/scrape.ts
Normal file
117
src/backend/helpers/scrape.ts
Normal 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;
|
||||
}
|
@ -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();
|
||||
|
@ -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";
|
||||
|
||||
|
@ -11,3 +11,8 @@ export type MWMediaMeta = {
|
||||
poster?: string;
|
||||
type: MWMediaType;
|
||||
};
|
||||
|
||||
export interface MWQuery {
|
||||
searchQuery: string;
|
||||
type: MWMediaType;
|
||||
}
|
||||
|
@ -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",
|
||||
// },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
37
src/backend/providers/testProviderTwo.ts
Normal file
37
src/backend/providers/testProviderTwo.ts
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
@ -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";
|
||||
|
@ -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}
|
||||
// </>
|
||||
// );
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
// />
|
||||
// );
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
59
src/hooks/useScrape.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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'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'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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user