mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 07:11:50 +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 { registerEmbedScraper } from "../helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
||||||
|
|
||||||
|
const timeout = (time: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => resolve(), time);
|
||||||
|
});
|
||||||
|
|
||||||
registerEmbedScraper({
|
registerEmbedScraper({
|
||||||
id: "testembed",
|
id: "testembed",
|
||||||
rank: 23,
|
rank: 23,
|
||||||
for: MWEmbedType.OPENLOAD,
|
for: MWEmbedType.OPENLOAD,
|
||||||
|
|
||||||
async getStream({ progress, url }) {
|
async getStream({ progress }) {
|
||||||
console.log("scraping url: ", url);
|
await timeout(1000);
|
||||||
progress(25);
|
progress(25);
|
||||||
|
await timeout(1000);
|
||||||
progress(50);
|
progress(50);
|
||||||
|
await timeout(1000);
|
||||||
progress(75);
|
progress(75);
|
||||||
|
throw new Error("failed to load or something");
|
||||||
|
await timeout(1000);
|
||||||
return {
|
return {
|
||||||
streamUrl: "hello-world",
|
streamUrl: "hello-world",
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
|
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 {
|
export enum MWEmbedType {
|
||||||
OPENLOAD = "openload",
|
OPENLOAD = "openload",
|
||||||
|
ANOTHER = "another",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MWEmbed = {
|
export type MWEmbed = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MWEmbedScraper } from "./embed";
|
import { MWEmbedScraper, MWEmbedType } from "./embed";
|
||||||
import { MWProvider } from "./provider";
|
import { MWProvider } from "./provider";
|
||||||
|
|
||||||
let providers: MWProvider[] = [];
|
let providers: MWProvider[] = [];
|
||||||
@ -15,8 +15,8 @@ export function registerEmbedScraper(embed: MWEmbedScraper) {
|
|||||||
|
|
||||||
export function initializeScraperStore() {
|
export function initializeScraperStore() {
|
||||||
// sort by ranking
|
// sort by ranking
|
||||||
providers = providers.sort((a, b) => a.rank - b.rank);
|
providers = providers.sort((a, b) => b.rank - a.rank);
|
||||||
embeds = embeds.sort((a, b) => a.rank - b.rank);
|
embeds = embeds.sort((a, b) => b.rank - a.rank);
|
||||||
|
|
||||||
// check for invalid ranks
|
// check for invalid ranks
|
||||||
let lastRank: null | number = null;
|
let lastRank: null | number = null;
|
||||||
@ -50,6 +50,11 @@ export function initializeScraperStore() {
|
|||||||
const embedIds = embeds.map((v) => v.id);
|
const embedIds = embeds.map((v) => v.id);
|
||||||
if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length)
|
if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length)
|
||||||
throw new Error("Duplicate IDS in embed scrapers");
|
throw new Error("Duplicate IDS in embed scrapers");
|
||||||
|
|
||||||
|
// check for duplicate embed types
|
||||||
|
const embedTypes = embeds.map((v) => v.for);
|
||||||
|
if (embedTypes.length > 0 && new Set(embedTypes).size !== embedTypes.length)
|
||||||
|
throw new Error("Duplicate types in embed scrapers");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProviders(): MWProvider[] {
|
export function getProviders(): MWProvider[] {
|
||||||
@ -59,3 +64,9 @@ export function getProviders(): MWProvider[] {
|
|||||||
export function getEmbeds(): MWEmbedScraper[] {
|
export function getEmbeds(): MWEmbedScraper[] {
|
||||||
return embeds;
|
return embeds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEmbedScraperByType(
|
||||||
|
type: MWEmbedType
|
||||||
|
): MWEmbedScraper | null {
|
||||||
|
return getEmbeds().find((v) => v.for === type) ?? null;
|
||||||
|
}
|
||||||
|
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";
|
import { initializeScraperStore } from "./helpers/register";
|
||||||
|
|
||||||
// TODO backend system:
|
// TODO backend system:
|
||||||
// - run providers/embedscrapers in webworkers for multithreading and isolation
|
|
||||||
// - caption support
|
// - caption support
|
||||||
// - hooks to run all providers one by one
|
// - hooks to run all providers one by one
|
||||||
// - move over old providers to new system
|
// - move over old providers to new system
|
||||||
@ -10,8 +9,11 @@ import { initializeScraperStore } from "./helpers/register";
|
|||||||
// providers
|
// providers
|
||||||
// -- nothing here yet
|
// -- nothing here yet
|
||||||
import "./providers/testProvider";
|
import "./providers/testProvider";
|
||||||
|
import "./providers/testProviderTwo";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
// -- nothing here yet
|
// -- nothing here yet
|
||||||
|
import "./embeds/testEmbedScraper";
|
||||||
|
import "./embeds/testEmbedScraperTwo";
|
||||||
|
|
||||||
initializeScraperStore();
|
initializeScraperStore();
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { MWMediaType, MWQuery } from "@/providers";
|
import { MWMediaMeta, MWMediaType, MWQuery } from "./types";
|
||||||
import { MWMediaMeta } from "./types";
|
|
||||||
|
|
||||||
const JW_API_BASE = "https://apis.justwatch.com";
|
const JW_API_BASE = "https://apis.justwatch.com";
|
||||||
|
|
||||||
|
@ -11,3 +11,8 @@ export type MWMediaMeta = {
|
|||||||
poster?: string;
|
poster?: string;
|
||||||
type: MWMediaType;
|
type: MWMediaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MWQuery {
|
||||||
|
searchQuery: string;
|
||||||
|
type: MWMediaType;
|
||||||
|
}
|
||||||
|
@ -1,32 +1,35 @@
|
|||||||
import { MWEmbedType } from "../helpers/embed";
|
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "../helpers/streams";
|
|
||||||
import { MWMediaType } from "../metadata/types";
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
|
const timeout = (time: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => resolve(), time);
|
||||||
|
});
|
||||||
|
|
||||||
registerProvider({
|
registerProvider({
|
||||||
id: "testprov",
|
id: "testprov",
|
||||||
rank: 42,
|
rank: 42,
|
||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
|
|
||||||
async scrape({ progress, imdbId, tmdbId }) {
|
async scrape({ progress }) {
|
||||||
console.log("scraping provider for: ", imdbId, tmdbId);
|
await timeout(1000);
|
||||||
progress(25);
|
progress(25);
|
||||||
|
await timeout(1000);
|
||||||
progress(50);
|
progress(50);
|
||||||
|
await timeout(1000);
|
||||||
progress(75);
|
progress(75);
|
||||||
|
await timeout(1000);
|
||||||
|
|
||||||
// providers can optionally provide a stream themselves,
|
|
||||||
// incase they host their own streams instead of using embeds
|
|
||||||
return {
|
return {
|
||||||
stream: {
|
|
||||||
streamUrl: "hello-world",
|
|
||||||
type: MWStreamType.HLS,
|
|
||||||
quality: MWStreamQuality.Q1080P,
|
|
||||||
},
|
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
// {
|
||||||
type: MWEmbedType.OPENLOAD,
|
// type: MWEmbedType.OPENLOAD,
|
||||||
url: "https://google.com",
|
// url: "https://google.com",
|
||||||
},
|
// },
|
||||||
|
// {
|
||||||
|
// type: MWEmbedType.ANOTHER,
|
||||||
|
// url: "https://google.com",
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
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 { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MWMediaType, MWQuery } from "@/providers";
|
|
||||||
import { DropdownButton } from "./buttons/DropdownButton";
|
import { DropdownButton } from "./buttons/DropdownButton";
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "./Icon";
|
||||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||||
|
@ -7,17 +7,9 @@ import { Icons } from "@/components/Icon";
|
|||||||
import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
|
import { WatchedEpisode } from "@/components/media/WatchedEpisodeButton";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { serializePortableMedia } from "@/hooks/usePortableMedia";
|
import { serializePortableMedia } from "@/hooks/usePortableMedia";
|
||||||
import {
|
|
||||||
convertMediaToPortable,
|
|
||||||
MWMedia,
|
|
||||||
MWMediaSeasons,
|
|
||||||
MWMediaSeason,
|
|
||||||
MWPortableMedia,
|
|
||||||
} from "@/providers";
|
|
||||||
import { getSeasonDataFromMedia } from "@/providers/methods/seasons";
|
|
||||||
|
|
||||||
export interface SeasonsProps {
|
export interface SeasonsProps {
|
||||||
media: MWMedia;
|
media: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSeasons(props: { error?: boolean }) {
|
export function LoadingSeasons(props: { error?: boolean }) {
|
||||||
@ -45,80 +37,73 @@ export function LoadingSeasons(props: { error?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Seasons(props: SeasonsProps) {
|
export function Seasons(props: SeasonsProps) {
|
||||||
const { t } = useTranslation();
|
// const { t } = useTranslation();
|
||||||
|
// const [searchSeasons, loading, error, success] = useLoading(
|
||||||
const [searchSeasons, loading, error, success] = useLoading(
|
// (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia)
|
||||||
(portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia)
|
// );
|
||||||
);
|
// const history = useHistory();
|
||||||
const history = useHistory();
|
// const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
|
||||||
const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
|
// const seasonSelected = props.media.seasonId as string;
|
||||||
const seasonSelected = props.media.seasonId as string;
|
// const episodeSelected = props.media.episodeId as string;
|
||||||
const episodeSelected = props.media.episodeId as string;
|
// useEffect(() => {
|
||||||
|
// (async () => {
|
||||||
useEffect(() => {
|
// const seasonData = await searchSeasons(props.media);
|
||||||
(async () => {
|
// setSeasons(seasonData);
|
||||||
const seasonData = await searchSeasons(props.media);
|
// })();
|
||||||
setSeasons(seasonData);
|
// }, [searchSeasons, props.media]);
|
||||||
})();
|
// function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
|
||||||
}, [searchSeasons, props.media]);
|
// const newMedia: MWMedia = { ...props.media };
|
||||||
|
// newMedia.episodeId = episodeId;
|
||||||
function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
|
// newMedia.seasonId = seasonId;
|
||||||
const newMedia: MWMedia = { ...props.media };
|
// history.replace(
|
||||||
newMedia.episodeId = episodeId;
|
// `/media/${newMedia.mediaType}/${serializePortableMedia(
|
||||||
newMedia.seasonId = seasonId;
|
// convertMediaToPortable(newMedia)
|
||||||
history.replace(
|
// )}`
|
||||||
`/media/${newMedia.mediaType}/${serializePortableMedia(
|
// );
|
||||||
convertMediaToPortable(newMedia)
|
// }
|
||||||
)}`
|
// const mapSeason = (season: MWMediaSeason) => ({
|
||||||
);
|
// id: season.id,
|
||||||
}
|
// name: season.title || `${t("seasons.season", { season: season.sort })}`,
|
||||||
|
// });
|
||||||
const mapSeason = (season: MWMediaSeason) => ({
|
// const options = seasons.seasons.map(mapSeason);
|
||||||
id: season.id,
|
// const foundSeason = seasons.seasons.find(
|
||||||
name: season.title || `${t("seasons.season", { season: season.sort })}`,
|
// (season) => season.id === seasonSelected
|
||||||
});
|
// );
|
||||||
|
// const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
|
||||||
const options = seasons.seasons.map(mapSeason);
|
// return (
|
||||||
|
// <>
|
||||||
const foundSeason = seasons.seasons.find(
|
// {loading ? <LoadingSeasons /> : null}
|
||||||
(season) => season.id === seasonSelected
|
// {error ? <LoadingSeasons error /> : null}
|
||||||
);
|
// {success && seasons.seasons.length ? (
|
||||||
const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
|
// <>
|
||||||
|
// <Dropdown
|
||||||
return (
|
// selectedItem={selectedItem as OptionItem}
|
||||||
<>
|
// options={options}
|
||||||
{loading ? <LoadingSeasons /> : null}
|
// setSelectedItem={(seasonItem) =>
|
||||||
{error ? <LoadingSeasons error /> : null}
|
// navigateToSeasonAndEpisode(
|
||||||
{success && seasons.seasons.length ? (
|
// seasonItem.id,
|
||||||
<>
|
// seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
|
||||||
<Dropdown
|
// .id as string
|
||||||
selectedItem={selectedItem as OptionItem}
|
// )
|
||||||
options={options}
|
// }
|
||||||
setSelectedItem={(seasonItem) =>
|
// />
|
||||||
navigateToSeasonAndEpisode(
|
// {seasons.seasons
|
||||||
seasonItem.id,
|
// .find((s) => s.id === seasonSelected)
|
||||||
seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
|
// ?.episodes.map((v) => (
|
||||||
.id as string
|
// <WatchedEpisode
|
||||||
)
|
// key={v.id}
|
||||||
}
|
// media={{
|
||||||
/>
|
// ...props.media,
|
||||||
{seasons.seasons
|
// seriesData: seasons,
|
||||||
.find((s) => s.id === seasonSelected)
|
// episodeId: v.id,
|
||||||
?.episodes.map((v) => (
|
// seasonId: seasonSelected,
|
||||||
<WatchedEpisode
|
// }}
|
||||||
key={v.id}
|
// active={v.id === episodeSelected}
|
||||||
media={{
|
// onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
|
||||||
...props.media,
|
// />
|
||||||
seriesData: seasons,
|
// ))}
|
||||||
episodeId: v.id,
|
// </>
|
||||||
seasonId: seasonSelected,
|
// ) : null}
|
||||||
}}
|
// </>
|
||||||
active={v.id === episodeSelected}
|
// );
|
||||||
onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DotList } from "@/components/text/DotList";
|
import { DotList } from "@/components/text/DotList";
|
||||||
import { MWSearchResult } from "@/backend/metadata/search";
|
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types";
|
||||||
import { MWMediaType } from "@/providers";
|
|
||||||
|
|
||||||
export interface MediaCardProps {
|
export interface MediaCardProps {
|
||||||
media: MWSearchResult;
|
media: MWMediaMeta;
|
||||||
linkable?: boolean;
|
linkable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
import { getEpisodeFromMedia, MWMedia } from "@/providers";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
|
import { useWatchedContext, getWatchedFromPortable } from "@/state/watched";
|
||||||
import { Episode } from "./EpisodeButton";
|
import { Episode } from "./EpisodeButton";
|
||||||
|
|
||||||
export interface WatchedEpisodeProps {
|
export interface WatchedEpisodeProps {
|
||||||
media: MWMedia;
|
media: MWMediaMeta;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WatchedEpisode(props: WatchedEpisodeProps) {
|
export function WatchedEpisode(props: WatchedEpisodeProps) {
|
||||||
const { watched } = useWatchedContext();
|
// const { watched } = useWatchedContext();
|
||||||
const foundWatched = getWatchedFromPortable(watched.items, props.media);
|
// const foundWatched = getWatchedFromPortable(watched.items, props.media);
|
||||||
const episode = getEpisodeFromMedia(props.media);
|
// // const episode = getEpisodeFromMedia(props.media);
|
||||||
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
|
// const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
|
||||||
|
// return (
|
||||||
return (
|
// <Episode
|
||||||
<Episode
|
// progress={watchedPercentage}
|
||||||
progress={watchedPercentage}
|
// episodeNumber={episode?.episode?.sort ?? 1}
|
||||||
episodeNumber={episode?.episode?.sort ?? 1}
|
// active={props.active}
|
||||||
active={props.active}
|
// onClick={props.onClick}
|
||||||
onClick={props.onClick}
|
// />
|
||||||
/>
|
// );
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { MWSearchResult } from "@/backend/metadata/search";
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import { MediaCard } from "./MediaCard";
|
import { MediaCard } from "./MediaCard";
|
||||||
|
|
||||||
export interface WatchedMediaCardProps {
|
export interface WatchedMediaCardProps {
|
||||||
media: MWSearchResult;
|
media: MWMediaMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
||||||
|
@ -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 React, { useRef, useState } from "react";
|
||||||
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
import { MWMediaType, MWQuery } from "@/providers";
|
|
||||||
|
|
||||||
export function useSearchQuery(): [
|
export function useSearchQuery(): [
|
||||||
MWQuery,
|
MWQuery,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
import { Redirect, Route, Switch } from "react-router-dom";
|
||||||
import { MWMediaType } from "@/providers";
|
|
||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
import { WatchedContextProvider } from "@/state/watched";
|
import { WatchedContextProvider } from "@/state/watched";
|
||||||
|
|
||||||
@ -7,6 +6,7 @@ import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
|||||||
import { MediaView } from "@/views/MediaView";
|
import { MediaView } from "@/views/MediaView";
|
||||||
import { SearchView } from "@/views/search/SearchView";
|
import { SearchView } from "@/views/search/SearchView";
|
||||||
import { TestView } from "@/views/TestView";
|
import { TestView } from "@/views/TestView";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -6,7 +7,6 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { getProviderMetadata, MWMediaMeta } from "@/providers";
|
|
||||||
import { BookmarkStore } from "./store";
|
import { BookmarkStore } from "./store";
|
||||||
|
|
||||||
interface BookmarkStoreData {
|
interface BookmarkStoreData {
|
||||||
@ -64,7 +64,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
|
|||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
setItemBookmark(media: MWMediaMeta, bookmarked: boolean) {
|
setItemBookmark(media: any, bookmarked: boolean) {
|
||||||
setBookmarked((data: BookmarkStoreData) => {
|
setBookmarked((data: BookmarkStoreData) => {
|
||||||
if (bookmarked) {
|
if (bookmarked) {
|
||||||
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
||||||
@ -90,9 +90,7 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFilteredBookmarks() {
|
getFilteredBookmarks() {
|
||||||
return bookmarkStorage.bookmarks.filter(
|
return [];
|
||||||
(bookmark) => getProviderMetadata(bookmark.providerId)?.enabled
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
bookmarkStore: bookmarkStorage,
|
bookmarkStore: bookmarkStorage,
|
||||||
}),
|
}),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MWMediaMeta } from "@/backend/metadata/types";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@ -6,7 +7,6 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "@/providers";
|
|
||||||
import { VideoProgressStore } from "./store";
|
import { VideoProgressStore } from "./store";
|
||||||
|
|
||||||
interface WatchedStoreItem extends MWMediaMeta {
|
interface WatchedStoreItem extends MWMediaMeta {
|
||||||
@ -28,13 +28,7 @@ export function getWatchedFromPortable(
|
|||||||
items: WatchedStoreItem[],
|
items: WatchedStoreItem[],
|
||||||
media: MWMediaMeta
|
media: MWMediaMeta
|
||||||
): WatchedStoreItem | undefined {
|
): WatchedStoreItem | undefined {
|
||||||
return items.find(
|
return undefined;
|
||||||
(v) =>
|
|
||||||
v.mediaId === media.mediaId &&
|
|
||||||
v.providerId === media.providerId &&
|
|
||||||
v.episodeId === media.episodeId &&
|
|
||||||
v.seasonId === media.seasonId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WatchedContext = createContext<WatchedStoreDataWrapper>({
|
const WatchedContext = createContext<WatchedStoreDataWrapper>({
|
||||||
@ -73,76 +67,73 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
|||||||
progress: number,
|
progress: number,
|
||||||
total: number
|
total: number
|
||||||
): void {
|
): void {
|
||||||
setWatched((data: WatchedStoreData) => {
|
// setWatched((data: WatchedStoreData) => {
|
||||||
let item = getWatchedFromPortable(data.items, media);
|
// let item = getWatchedFromPortable(data.items, media);
|
||||||
if (!item) {
|
// if (!item) {
|
||||||
item = {
|
// item = {
|
||||||
mediaId: media.mediaId,
|
// mediaId: media.mediaId,
|
||||||
mediaType: media.mediaType,
|
// mediaType: media.mediaType,
|
||||||
providerId: media.providerId,
|
// providerId: media.providerId,
|
||||||
title: media.title,
|
// title: media.title,
|
||||||
year: media.year,
|
// year: media.year,
|
||||||
percentage: 0,
|
// percentage: 0,
|
||||||
progress: 0,
|
// progress: 0,
|
||||||
episodeId: media.episodeId,
|
// episodeId: media.episodeId,
|
||||||
seasonId: media.seasonId,
|
// seasonId: media.seasonId,
|
||||||
};
|
// };
|
||||||
data.items.push(item);
|
// data.items.push(item);
|
||||||
}
|
// }
|
||||||
|
// // update actual item
|
||||||
// update actual item
|
// item.progress = progress;
|
||||||
item.progress = progress;
|
// item.percentage = Math.round((progress / total) * 100);
|
||||||
item.percentage = Math.round((progress / total) * 100);
|
// return data;
|
||||||
|
// });
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
getFilteredWatched() {
|
getFilteredWatched() {
|
||||||
// remove disabled providers
|
// remove disabled providers
|
||||||
let filtered = watched.items.filter(
|
// let filtered = watched.items.filter(
|
||||||
(item) => getProviderMetadata(item.providerId)?.enabled
|
// (item) => getProviderMetadata(item.providerId)?.enabled
|
||||||
);
|
// );
|
||||||
|
// // get highest episode number for every anime/season
|
||||||
// get highest episode number for every anime/season
|
// const highestEpisode: Record<string, [number, number]> = {};
|
||||||
const highestEpisode: Record<string, [number, number]> = {};
|
// const highestWatchedItem: Record<string, WatchedStoreItem> = {};
|
||||||
const highestWatchedItem: Record<string, WatchedStoreItem> = {};
|
// filtered = filtered.filter((item) => {
|
||||||
filtered = filtered.filter((item) => {
|
// if (
|
||||||
if (
|
// [MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType)
|
||||||
[MWMediaType.ANIME, MWMediaType.SERIES].includes(item.mediaType)
|
// ) {
|
||||||
) {
|
// const key = `${item.mediaType}-${item.mediaId}`;
|
||||||
const key = `${item.mediaType}-${item.mediaId}`;
|
// const current: [number, number] = [
|
||||||
const current: [number, number] = [
|
// item.episodeId ? parseInt(item.episodeId, 10) : -1,
|
||||||
item.episodeId ? parseInt(item.episodeId, 10) : -1,
|
// item.seasonId ? parseInt(item.seasonId, 10) : -1,
|
||||||
item.seasonId ? parseInt(item.seasonId, 10) : -1,
|
// ];
|
||||||
];
|
// let existing = highestEpisode[key];
|
||||||
let existing = highestEpisode[key];
|
// if (!existing) {
|
||||||
if (!existing) {
|
// existing = current;
|
||||||
existing = current;
|
// highestEpisode[key] = current;
|
||||||
highestEpisode[key] = current;
|
// highestWatchedItem[key] = item;
|
||||||
highestWatchedItem[key] = item;
|
// }
|
||||||
}
|
// if (
|
||||||
|
// current[0] > existing[0] ||
|
||||||
if (
|
// (current[0] === existing[0] && current[1] > existing[1])
|
||||||
current[0] > existing[0] ||
|
// ) {
|
||||||
(current[0] === existing[0] && current[1] > existing[1])
|
// highestEpisode[key] = current;
|
||||||
) {
|
// highestWatchedItem[key] = item;
|
||||||
highestEpisode[key] = current;
|
// }
|
||||||
highestWatchedItem[key] = item;
|
// return false;
|
||||||
}
|
// }
|
||||||
return false;
|
// return true;
|
||||||
}
|
// });
|
||||||
return true;
|
// return [...filtered, ...Object.values(highestWatchedItem)];
|
||||||
});
|
|
||||||
|
|
||||||
return [...filtered, ...Object.values(highestWatchedItem)];
|
|
||||||
},
|
},
|
||||||
watched,
|
watched,
|
||||||
}),
|
}),
|
||||||
[watched, setWatched]
|
[
|
||||||
|
/*watched, setWatched*/
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WatchedContext.Provider value={contextValue}>
|
<WatchedContext.Provider value={contextValue as any}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</WatchedContext.Provider>
|
</WatchedContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { MWMediaType } from "@/providers";
|
|
||||||
import { versionedStoreBuilder } from "@/utils/storage";
|
import { versionedStoreBuilder } from "@/utils/storage";
|
||||||
import { WatchedStoreData } from "./context";
|
|
||||||
|
|
||||||
export const VideoProgressStore = versionedStoreBuilder()
|
export const VideoProgressStore = versionedStoreBuilder()
|
||||||
.setKey("video-progress")
|
.setKey("video-progress")
|
||||||
@ -9,79 +7,79 @@ export const VideoProgressStore = versionedStoreBuilder()
|
|||||||
})
|
})
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 1,
|
version: 1,
|
||||||
migrate(data: any) {
|
migrate() {
|
||||||
const output: WatchedStoreData = { items: [] };
|
// const output: WatchedStoreData = { items: [] };
|
||||||
|
|
||||||
if (!data || data.constructor !== Object) return output;
|
// if (!data || data.constructor !== Object) return output;
|
||||||
|
|
||||||
Object.keys(data).forEach((scraperId) => {
|
// Object.keys(data).forEach((scraperId) => {
|
||||||
if (scraperId === "--version") return;
|
// if (scraperId === "--version") return;
|
||||||
if (scraperId === "save") return;
|
// if (scraperId === "save") return;
|
||||||
|
|
||||||
if (
|
// if (
|
||||||
data[scraperId].movie &&
|
// data[scraperId].movie &&
|
||||||
data[scraperId].movie.constructor === Object
|
// data[scraperId].movie.constructor === Object
|
||||||
) {
|
// ) {
|
||||||
Object.keys(data[scraperId].movie).forEach((movieId) => {
|
// Object.keys(data[scraperId].movie).forEach((movieId) => {
|
||||||
try {
|
// try {
|
||||||
output.items.push({
|
// output.items.push({
|
||||||
mediaId: movieId.includes("player.php")
|
// mediaId: movieId.includes("player.php")
|
||||||
? movieId.split("player.php%3Fimdb%3D")[1]
|
// ? movieId.split("player.php%3Fimdb%3D")[1]
|
||||||
: movieId,
|
// : movieId,
|
||||||
mediaType: MWMediaType.MOVIE,
|
// mediaType: MWMediaType.MOVIE,
|
||||||
providerId: scraperId,
|
// providerId: scraperId,
|
||||||
title: data[scraperId].movie[movieId].full.meta.title,
|
// title: data[scraperId].movie[movieId].full.meta.title,
|
||||||
year: data[scraperId].movie[movieId].full.meta.year,
|
// year: data[scraperId].movie[movieId].full.meta.year,
|
||||||
progress: data[scraperId].movie[movieId].full.currentlyAt,
|
// progress: data[scraperId].movie[movieId].full.currentlyAt,
|
||||||
percentage: Math.round(
|
// percentage: Math.round(
|
||||||
(data[scraperId].movie[movieId].full.currentlyAt /
|
// (data[scraperId].movie[movieId].full.currentlyAt /
|
||||||
data[scraperId].movie[movieId].full.totalDuration) *
|
// data[scraperId].movie[movieId].full.totalDuration) *
|
||||||
100
|
// 100
|
||||||
),
|
// ),
|
||||||
});
|
// });
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error(
|
// console.error(
|
||||||
`Failed to migrate movie: ${scraperId}/${movieId}`,
|
// `Failed to migrate movie: ${scraperId}/${movieId}`,
|
||||||
data[scraperId].movie[movieId]
|
// data[scraperId].movie[movieId]
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (
|
// if (
|
||||||
data[scraperId].show &&
|
// data[scraperId].show &&
|
||||||
data[scraperId].show.constructor === Object
|
// data[scraperId].show.constructor === Object
|
||||||
) {
|
// ) {
|
||||||
Object.keys(data[scraperId].show).forEach((showId) => {
|
// Object.keys(data[scraperId].show).forEach((showId) => {
|
||||||
if (data[scraperId].show[showId].constructor !== Object) return;
|
// if (data[scraperId].show[showId].constructor !== Object) return;
|
||||||
Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
|
// Object.keys(data[scraperId].show[showId]).forEach((episodeId) => {
|
||||||
try {
|
// try {
|
||||||
output.items.push({
|
// output.items.push({
|
||||||
mediaId: showId,
|
// mediaId: showId,
|
||||||
mediaType: MWMediaType.SERIES,
|
// mediaType: MWMediaType.SERIES,
|
||||||
providerId: scraperId,
|
// providerId: scraperId,
|
||||||
title: data[scraperId].show[showId][episodeId].meta.title,
|
// title: data[scraperId].show[showId][episodeId].meta.title,
|
||||||
year: data[scraperId].show[showId][episodeId].meta.year,
|
// year: data[scraperId].show[showId][episodeId].meta.year,
|
||||||
percentage: Math.round(
|
// percentage: Math.round(
|
||||||
(data[scraperId].show[showId][episodeId].currentlyAt /
|
// (data[scraperId].show[showId][episodeId].currentlyAt /
|
||||||
data[scraperId].show[showId][episodeId].totalDuration) *
|
// data[scraperId].show[showId][episodeId].totalDuration) *
|
||||||
100
|
// 100
|
||||||
),
|
// ),
|
||||||
progress: data[scraperId].show[showId][episodeId].currentlyAt,
|
// progress: data[scraperId].show[showId][episodeId].currentlyAt,
|
||||||
episodeId:
|
// episodeId:
|
||||||
data[scraperId].show[showId][episodeId].show.episode,
|
// data[scraperId].show[showId][episodeId].show.episode,
|
||||||
seasonId: data[scraperId].show[showId][episodeId].show.season,
|
// seasonId: data[scraperId].show[showId][episodeId].show.season,
|
||||||
});
|
// });
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error(
|
// console.error(
|
||||||
`Failed to migrate series: ${scraperId}/${showId}/${episodeId}`,
|
// `Failed to migrate series: ${scraperId}/${showId}/${episodeId}`,
|
||||||
data[scraperId].show[showId][episodeId]
|
// data[scraperId].show[showId][episodeId]
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
|
@ -1,220 +1,215 @@
|
|||||||
import { ReactElement, useCallback, useEffect, useState } from "react";
|
// import { ReactElement, useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
// import { useHistory } from "react-router-dom";
|
||||||
|
// import { useTranslation } from "react-i18next";
|
||||||
|
// import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
// import { Icons } from "@/components/Icon";
|
||||||
|
// import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
// import { Paper } from "@/components/layout/Paper";
|
||||||
|
// import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
|
||||||
|
// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
||||||
|
// import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
|
// import { DotList } from "@/components/text/DotList";
|
||||||
|
// import { Title } from "@/components/text/Title";
|
||||||
|
// import { useLoading } from "@/hooks/useLoading";
|
||||||
|
// import { usePortableMedia } from "@/hooks/usePortableMedia";
|
||||||
|
// import {
|
||||||
|
// getIfBookmarkedFromPortable,
|
||||||
|
// useBookmarkContext,
|
||||||
|
// } from "@/state/bookmark";
|
||||||
|
// import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
|
||||||
|
// import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||||
|
// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
||||||
|
// import { Loading } from "@/components/layout/Loading";
|
||||||
|
// import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Icons } from "@/components/Icon";
|
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
import { Paper } from "@/components/layout/Paper";
|
|
||||||
import { LoadingSeasons, Seasons } from "@/components/layout/Seasons";
|
|
||||||
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
|
||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
import { DotList } from "@/components/text/DotList";
|
|
||||||
import { Title } from "@/components/text/Title";
|
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
|
||||||
import { usePortableMedia } from "@/hooks/usePortableMedia";
|
|
||||||
import {
|
|
||||||
MWPortableMedia,
|
|
||||||
getStream,
|
|
||||||
MWMediaStream,
|
|
||||||
MWMedia,
|
|
||||||
convertPortableToMedia,
|
|
||||||
getProviderFromId,
|
|
||||||
MWMediaProvider,
|
|
||||||
MWMediaType,
|
|
||||||
} from "@/providers";
|
|
||||||
import {
|
|
||||||
getIfBookmarkedFromPortable,
|
|
||||||
useBookmarkContext,
|
|
||||||
} from "@/state/bookmark";
|
|
||||||
import { getWatchedFromPortable, useWatchedContext } from "@/state/watched";
|
|
||||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
|
||||||
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
|
||||||
import { Loading } from "@/components/layout/Loading";
|
|
||||||
import { NotFoundChecks } from "./notfound/NotFoundChecks";
|
|
||||||
|
|
||||||
interface StyledMediaViewProps {
|
// interface StyledMediaViewProps {
|
||||||
media: MWMedia;
|
// media: MWMedia;
|
||||||
stream: MWMediaStream;
|
// stream: MWMediaStream;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function SkeletonVideoPlayer(props: { error?: boolean }) {
|
// export function SkeletonVideoPlayer(props: { error?: boolean }) {
|
||||||
return (
|
// return (
|
||||||
<div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl">
|
// <div className="flex aspect-video w-full items-center justify-center bg-denim-200 lg:rounded-xl">
|
||||||
{props.error ? (
|
// {props.error ? (
|
||||||
<div className="flex flex-col items-center">
|
// <div className="flex flex-col items-center">
|
||||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
// <IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||||
<p className="mt-5 text-white">Couldn't get your stream</p>
|
// <p className="mt-5 text-white">Couldn't get your stream</p>
|
||||||
</div>
|
// </div>
|
||||||
) : (
|
// ) : (
|
||||||
<div className="flex flex-col items-center">
|
// <div className="flex flex-col items-center">
|
||||||
<Loading />
|
// <Loading />
|
||||||
<p className="mt-3 text-white">Getting your stream...</p>
|
// <p className="mt-3 text-white">Getting your stream...</p>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
function StyledMediaView(props: StyledMediaViewProps) {
|
// function StyledMediaView(props: StyledMediaViewProps) {
|
||||||
const reactHistory = useHistory();
|
// const reactHistory = useHistory();
|
||||||
const watchedStore = useWatchedContext();
|
// const watchedStore = useWatchedContext();
|
||||||
const startAtTime: number | undefined = getWatchedFromPortable(
|
// const startAtTime: number | undefined = getWatchedFromPortable(
|
||||||
watchedStore.watched.items,
|
// watchedStore.watched.items,
|
||||||
props.media
|
// props.media
|
||||||
)?.progress;
|
// )?.progress;
|
||||||
|
|
||||||
const updateProgress = useCallback(
|
// const updateProgress = useCallback(
|
||||||
(time: number, duration: number) => {
|
// (time: number, duration: number) => {
|
||||||
// Don't update stored progress if less than 30s into the video
|
// // Don't update stored progress if less than 30s into the video
|
||||||
if (time <= 30) return;
|
// if (time <= 30) return;
|
||||||
watchedStore.updateProgress(props.media, time, duration);
|
// watchedStore.updateProgress(props.media, time, duration);
|
||||||
},
|
// },
|
||||||
[props, watchedStore]
|
// [props, watchedStore]
|
||||||
);
|
// );
|
||||||
|
|
||||||
const goBack = useCallback(() => {
|
// const goBack = useCallback(() => {
|
||||||
if (reactHistory.action !== "POP") reactHistory.goBack();
|
// if (reactHistory.action !== "POP") reactHistory.goBack();
|
||||||
else reactHistory.push("/");
|
// else reactHistory.push("/");
|
||||||
}, [reactHistory]);
|
// }, [reactHistory]);
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className="overflow-hidden lg:rounded-xl">
|
// <div className="overflow-hidden lg:rounded-xl">
|
||||||
<DecoratedVideoPlayer title={props.media.title} onGoBack={goBack}>
|
// <DecoratedVideoPlayer title={props.media.title} onGoBack={goBack}>
|
||||||
<SourceControl source={props.stream.url} type={props.stream.type} />
|
// <SourceControl source={props.stream.url} type={props.stream.type} />
|
||||||
<ProgressListenerControl
|
// <ProgressListenerControl
|
||||||
startAt={startAtTime}
|
// startAt={startAtTime}
|
||||||
onProgress={updateProgress}
|
// onProgress={updateProgress}
|
||||||
/>
|
// />
|
||||||
</DecoratedVideoPlayer>
|
// </DecoratedVideoPlayer>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface StyledMediaFooterProps {
|
// interface StyledMediaFooterProps {
|
||||||
media: MWMedia;
|
// media: MWMedia;
|
||||||
provider: MWMediaProvider;
|
// provider: MWMediaProvider;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function StyledMediaFooter(props: StyledMediaFooterProps) {
|
// function StyledMediaFooter(props: StyledMediaFooterProps) {
|
||||||
const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext();
|
// const { setItemBookmark, getFilteredBookmarks } = useBookmarkContext();
|
||||||
const isBookmarked = getIfBookmarkedFromPortable(
|
// const isBookmarked = getIfBookmarkedFromPortable(
|
||||||
getFilteredBookmarks(),
|
// getFilteredBookmarks(),
|
||||||
props.media
|
// props.media
|
||||||
);
|
// );
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<Paper className="mt-5">
|
// <Paper className="mt-5">
|
||||||
<div className="flex">
|
// <div className="flex">
|
||||||
<div className="flex-1">
|
// <div className="flex-1">
|
||||||
<Title>{props.media.title}</Title>
|
// <Title>{props.media.title}</Title>
|
||||||
<DotList
|
// <DotList
|
||||||
className="mt-3 text-sm"
|
// className="mt-3 text-sm"
|
||||||
content={[
|
// content={[
|
||||||
props.provider.displayName,
|
// props.provider.displayName,
|
||||||
props.media.mediaType,
|
// props.media.mediaType,
|
||||||
props.media.year,
|
// props.media.year,
|
||||||
]}
|
// ]}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
<div>
|
// <div>
|
||||||
<IconPatch
|
// <IconPatch
|
||||||
icon={Icons.BOOKMARK}
|
// icon={Icons.BOOKMARK}
|
||||||
active={isBookmarked}
|
// active={isBookmarked}
|
||||||
onClick={() => setItemBookmark(props.media, !isBookmarked)}
|
// onClick={() => setItemBookmark(props.media, !isBookmarked)}
|
||||||
clickable
|
// clickable
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
{props.media.mediaType !== MWMediaType.MOVIE ? (
|
// {props.media.mediaType !== MWMediaType.MOVIE ? (
|
||||||
<Seasons media={props.media} />
|
// <Seasons media={props.media} />
|
||||||
) : null}
|
// ) : null}
|
||||||
</Paper>
|
// </Paper>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
function LoadingMediaFooter(props: { error?: boolean }) {
|
// function LoadingMediaFooter(props: { error?: boolean }) {
|
||||||
const { t } = useTranslation();
|
// const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<Paper className="mt-5">
|
// <Paper className="mt-5">
|
||||||
<div className="flex">
|
// <div className="flex">
|
||||||
<div className="flex-1">
|
// <div className="flex-1">
|
||||||
<div className="mb-2 h-4 w-48 rounded-full bg-denim-500" />
|
// <div className="mb-2 h-4 w-48 rounded-full bg-denim-500" />
|
||||||
<div>
|
// <div>
|
||||||
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
// <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
||||||
<span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
// <span className="mr-4 inline-block h-2 w-12 rounded-full bg-denim-400" />
|
||||||
</div>
|
// </div>
|
||||||
{props.error ? (
|
// {props.error ? (
|
||||||
<div className="flex items-center space-x-3">
|
// <div className="flex items-center space-x-3">
|
||||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
// <IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||||
<p>{t("media.invalidUrl")}</p>
|
// <p>{t("media.invalidUrl")}</p>
|
||||||
</div>
|
// </div>
|
||||||
) : (
|
// ) : (
|
||||||
<LoadingSeasons />
|
// <LoadingSeasons />
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</Paper>
|
// </Paper>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
function MediaViewContent(props: { portable: MWPortableMedia }) {
|
// function MediaViewContent(props: { portable: MWPortableMedia }) {
|
||||||
const mediaPortable = props.portable;
|
// const mediaPortable = props.portable;
|
||||||
const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>();
|
// const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>();
|
||||||
const [media, setMedia] = useState<MWMedia | undefined>();
|
// const [media, setMedia] = useState<MWMedia | undefined>();
|
||||||
const [fetchMedia, loadingPortable, errorPortable] = useLoading(
|
// const [fetchMedia, loadingPortable, errorPortable] = useLoading(
|
||||||
(portable: MWPortableMedia) => convertPortableToMedia(portable)
|
// (portable: MWPortableMedia) => convertPortableToMedia(portable)
|
||||||
);
|
// );
|
||||||
const [fetchStream, loadingStream, errorStream] = useLoading(
|
// const [fetchStream, loadingStream, errorStream] = useLoading(
|
||||||
(portable: MWPortableMedia) => getStream(portable)
|
// (portable: MWPortableMedia) => getStream(portable)
|
||||||
);
|
// );
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
(async () => {
|
// (async () => {
|
||||||
if (mediaPortable) {
|
// if (mediaPortable) {
|
||||||
setMedia(await fetchMedia(mediaPortable));
|
// setMedia(await fetchMedia(mediaPortable));
|
||||||
}
|
// }
|
||||||
})();
|
// })();
|
||||||
}, [mediaPortable, setMedia, fetchMedia]);
|
// }, [mediaPortable, setMedia, fetchMedia]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
(async () => {
|
// (async () => {
|
||||||
if (mediaPortable) {
|
// if (mediaPortable) {
|
||||||
setStreamUrl(await fetchStream(mediaPortable));
|
// setStreamUrl(await fetchStream(mediaPortable));
|
||||||
}
|
// }
|
||||||
})();
|
// })();
|
||||||
}, [mediaPortable, setStreamUrl, fetchStream]);
|
// }, [mediaPortable, setStreamUrl, fetchStream]);
|
||||||
|
|
||||||
let playerContent: ReactElement | null = null;
|
// let playerContent: ReactElement | null = null;
|
||||||
if (loadingStream) playerContent = <SkeletonVideoPlayer />;
|
// if (loadingStream) playerContent = <SkeletonVideoPlayer />;
|
||||||
else if (errorStream) playerContent = <SkeletonVideoPlayer error />;
|
// else if (errorStream) playerContent = <SkeletonVideoPlayer error />;
|
||||||
else if (media && streamUrl)
|
// else if (media && streamUrl)
|
||||||
playerContent = <StyledMediaView media={media} stream={streamUrl} />;
|
// playerContent = <StyledMediaView media={media} stream={streamUrl} />;
|
||||||
|
|
||||||
let footerContent: ReactElement | null = null;
|
// let footerContent: ReactElement | null = null;
|
||||||
if (loadingPortable) footerContent = <LoadingMediaFooter />;
|
// if (loadingPortable) footerContent = <LoadingMediaFooter />;
|
||||||
else if (errorPortable) footerContent = <LoadingMediaFooter error />;
|
// else if (errorPortable) footerContent = <LoadingMediaFooter error />;
|
||||||
else if (mediaPortable && media)
|
// else if (mediaPortable && media)
|
||||||
footerContent = (
|
// footerContent = (
|
||||||
<StyledMediaFooter
|
// <StyledMediaFooter
|
||||||
provider={
|
// provider={
|
||||||
getProviderFromId(mediaPortable.providerId) as MWMediaProvider
|
// getProviderFromId(mediaPortable.providerId) as MWMediaProvider
|
||||||
}
|
// }
|
||||||
media={media}
|
// media={media}
|
||||||
/>
|
// />
|
||||||
);
|
// );
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
{playerContent}
|
// {playerContent}
|
||||||
{footerContent}
|
// {footerContent}
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function MediaView() {
|
export function MediaView() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
|
// const mediaPortable: MWPortableMedia | undefined = usePortableMedia();
|
||||||
const reactHistory = useHistory();
|
const reactHistory = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -230,11 +225,11 @@ export function MediaView() {
|
|||||||
linkText={t("media.arrowText")}
|
linkText={t("media.arrowText")}
|
||||||
/>
|
/>
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<NotFoundChecks portable={mediaPortable}>
|
{/* <NotFoundChecks portable={mediaPortable}>
|
||||||
<div className="container mx-auto mt-40 mb-16 max-w-[1100px]">
|
<div className="container mx-auto mt-40 mb-16 max-w-[1100px]">
|
||||||
<MediaViewContent portable={mediaPortable as MWPortableMedia} />
|
<MediaViewContent portable={mediaPortable as MWPortableMedia} />
|
||||||
</div>
|
</div>
|
||||||
</NotFoundChecks>
|
</NotFoundChecks> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { searchForMedia } from "@/backend/metadata/search";
|
// import { searchForMedia } from "@/backend/metadata/search";
|
||||||
import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
// import { ProgressListenerControl } from "@/components/video/controls/ProgressListenerControl";
|
||||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
// import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||||
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
// import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
||||||
import { MWMediaType } from "@/providers";
|
import { useScrape } from "@/hooks/useScrape";
|
||||||
import { useCallback, useState } from "react";
|
// import { MWMediaType } from "@/providers";
|
||||||
|
// import { useCallback, useState } from "react";
|
||||||
|
|
||||||
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
|
// test videos: https://gist.github.com/jsturgis/3b19447b304616f18657
|
||||||
|
|
||||||
@ -24,37 +25,58 @@ import { useCallback, useState } from "react";
|
|||||||
// - devices: ipadOS
|
// - devices: ipadOS
|
||||||
// - features: HLS, error handling, preload interactions
|
// - features: HLS, error handling, preload interactions
|
||||||
|
|
||||||
|
// export function TestView() {
|
||||||
|
// const [show, setShow] = useState(true);
|
||||||
|
// const handleClick = useCallback(() => {
|
||||||
|
// setShow((v) => !v);
|
||||||
|
// }, [setShow]);
|
||||||
|
|
||||||
|
// if (!show) {
|
||||||
|
// return <p onClick={handleClick}>Click me to show</p>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async function search() {
|
||||||
|
// const test = await searchForMedia({
|
||||||
|
// searchQuery: "tron",
|
||||||
|
// type: MWMediaType.MOVIE,
|
||||||
|
// });
|
||||||
|
// console.log(test);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="w-[40rem] max-w-full">
|
||||||
|
// <DecoratedVideoPlayer>
|
||||||
|
// <SourceControl
|
||||||
|
// source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
||||||
|
// type="mp4"
|
||||||
|
// />
|
||||||
|
// <ProgressListenerControl
|
||||||
|
// startAt={283}
|
||||||
|
// onProgress={(a, b) => console.log(a, b)}
|
||||||
|
// />
|
||||||
|
// </DecoratedVideoPlayer>
|
||||||
|
// <p onClick={() => search()}>click me to search</p>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
export function TestView() {
|
export function TestView() {
|
||||||
const [show, setShow] = useState(true);
|
const { eventLog, pending, stream } = useScrape();
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
setShow((v) => !v);
|
|
||||||
}, [setShow]);
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
return <p onClick={handleClick}>Click me to show</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function search() {
|
|
||||||
const test = await searchForMedia({
|
|
||||||
searchQuery: "tron",
|
|
||||||
type: MWMediaType.MOVIE,
|
|
||||||
});
|
|
||||||
console.log(test);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[40rem] max-w-full">
|
<div>
|
||||||
<DecoratedVideoPlayer>
|
<p>pending: {pending}</p>
|
||||||
<SourceControl
|
<p>
|
||||||
source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4"
|
stream: {stream?.streamUrl} - {stream?.type} - {stream?.quality}
|
||||||
type="mp4"
|
</p>
|
||||||
/>
|
<hr />
|
||||||
<ProgressListenerControl
|
{eventLog.map((v) => (
|
||||||
startAt={283}
|
<div className="rounded-xl p-1 text-white">
|
||||||
onProgress={(a, b) => console.log(a, b)}
|
<p>
|
||||||
/>
|
{v.percentage}% - {v.type} - {v.errored ? "ERROR" : "pending"}
|
||||||
</DecoratedVideoPlayer>
|
</p>
|
||||||
<p onClick={() => search()}>click me to search</p>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { getProviderMetadata, MWPortableMedia } from "@/providers";
|
// import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
|
||||||
import { NotFoundMedia, NotFoundProvider } from "./NotFoundView";
|
|
||||||
|
|
||||||
export interface NotFoundChecksProps {
|
export interface NotFoundChecksProps {
|
||||||
portable: MWPortableMedia | undefined;
|
// portable: MWPortableMedia | undefined;
|
||||||
children?: ReactElement;
|
children?: ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,17 +12,17 @@ export interface NotFoundChecksProps {
|
|||||||
export function NotFoundChecks(
|
export function NotFoundChecks(
|
||||||
props: NotFoundChecksProps
|
props: NotFoundChecksProps
|
||||||
): ReactElement | null {
|
): ReactElement | null {
|
||||||
const providerMeta = props.portable
|
// const providerMeta = props.portable
|
||||||
? getProviderMetadata(props.portable.providerId)
|
// ? getProviderMetadata(props.portable.providerId)
|
||||||
: undefined;
|
// : undefined;
|
||||||
|
|
||||||
if (!providerMeta || !providerMeta.exists) {
|
// if (!providerMeta || !providerMeta.exists) {
|
||||||
return <NotFoundMedia />;
|
// return <NotFoundMedia />;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!providerMeta.enabled) {
|
// if (!providerMeta.enabled) {
|
||||||
return <NotFoundProvider />;
|
// return <NotFoundProvider />;
|
||||||
}
|
// }
|
||||||
|
|
||||||
return props.children || null;
|
return props.children || null;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
|
||||||
import {
|
import {
|
||||||
getIfBookmarkedFromPortable,
|
getIfBookmarkedFromPortable,
|
||||||
useBookmarkContext,
|
useBookmarkContext,
|
||||||
@ -22,12 +21,12 @@ function Bookmarks() {
|
|||||||
icon={Icons.BOOKMARK}
|
icon={Icons.BOOKMARK}
|
||||||
>
|
>
|
||||||
<MediaGrid>
|
<MediaGrid>
|
||||||
{bookmarks.map((v) => (
|
{/* {bookmarks.map((v) => (
|
||||||
<WatchedMediaCard
|
<WatchedMediaCard
|
||||||
key={[v.mediaId, v.providerId].join("|")}
|
key={[v.mediaId, v.providerId].join("|")}
|
||||||
media={v}
|
media={v}
|
||||||
/>
|
/>
|
||||||
))}
|
))} */}
|
||||||
</MediaGrid>
|
</MediaGrid>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
);
|
);
|
||||||
@ -51,13 +50,13 @@ function Watched() {
|
|||||||
icon={Icons.CLOCK}
|
icon={Icons.CLOCK}
|
||||||
>
|
>
|
||||||
<MediaGrid>
|
<MediaGrid>
|
||||||
{watchedItems.map((v) => (
|
{/* {watchedItems.map((v) => (
|
||||||
<WatchedMediaCard
|
<WatchedMediaCard
|
||||||
key={[v.mediaId, v.providerId].join("|")}
|
key={[v.mediaId, v.providerId].join("|")}
|
||||||
media={v}
|
media={v}
|
||||||
series
|
series
|
||||||
/>
|
/>
|
||||||
))}
|
))} */}
|
||||||
</MediaGrid>
|
</MediaGrid>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useDebounce } from "@/hooks/useDebounce";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
import { MWQuery } from "@/providers";
|
import { MWQuery } from "@/backend/metadata/types";
|
||||||
import { HomeView } from "./HomeView";
|
import { HomeView } from "./HomeView";
|
||||||
import { SearchLoadingView } from "./SearchLoadingView";
|
import { SearchLoadingView } from "./SearchLoadingView";
|
||||||
import { SearchResultsView } from "./SearchResultsView";
|
import { SearchResultsView } from "./SearchResultsView";
|
||||||
|
@ -6,8 +6,8 @@ import { SectionHeading } from "@/components/layout/SectionHeading";
|
|||||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { MWQuery } from "@/providers";
|
import { searchForMedia } from "@/backend/metadata/search";
|
||||||
import { MWSearchResult, searchForMedia } from "@/backend/metadata/search";
|
import { MWMediaMeta, MWQuery } from "@/backend/metadata/types";
|
||||||
import { SearchLoadingView } from "./SearchLoadingView";
|
import { SearchLoadingView } from "./SearchLoadingView";
|
||||||
|
|
||||||
function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
||||||
@ -46,7 +46,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
|||||||
export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
|
export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [results, setResults] = useState<MWSearchResult[]>([]);
|
const [results, setResults] = useState<MWMediaMeta[]>([]);
|
||||||
const [runSearchQuery, loading, error] = useLoading((query: MWQuery) =>
|
const [runSearchQuery, loading, error] = useLoading((query: MWQuery) =>
|
||||||
searchForMedia(query)
|
searchForMedia(query)
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user