mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 07:51:51 +01:00
bunch of todos
This commit is contained in:
parent
8e522e18d4
commit
52b063b10a
@ -15,6 +15,7 @@
|
|||||||
"json5": "^2.2.0",
|
"json5": "^2.2.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
|
"ofetch": "^1.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-i18next": "^12.1.1",
|
"react-i18next": "^12.1.1",
|
||||||
|
@ -4,15 +4,13 @@
|
|||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"loading": "Fetching your favourite shows...",
|
"loading": "Fetching your favourite shows...",
|
||||||
"providersFailed": "{{fails}}/{{total}} providers failed!",
|
|
||||||
"allResults": "That's all we have!",
|
"allResults": "That's all we have!",
|
||||||
"noResults": "We couldn't find anything!",
|
"noResults": "We couldn't find anything!",
|
||||||
"allFailed": "All providers have failed!",
|
"allFailed": "Failed to find media, try again!",
|
||||||
"headingTitle": "Search results",
|
"headingTitle": "Search results",
|
||||||
"headingLink": "Back to home",
|
"headingLink": "Back to home",
|
||||||
"bookmarks": "Bookmarks",
|
"bookmarks": "Bookmarks",
|
||||||
"continueWatching": "Continue Watching",
|
"continueWatching": "Continue Watching",
|
||||||
"tagline": "Because watching legally is boring",
|
|
||||||
"title": "What do you want to watch?",
|
"title": "What do you want to watch?",
|
||||||
"placeholder": "What do you want to watch?"
|
"placeholder": "What do you want to watch?"
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,6 @@ import { MWStream } from "./streams";
|
|||||||
|
|
||||||
export enum MWEmbedType {
|
export enum MWEmbedType {
|
||||||
OPENLOAD = "openload",
|
OPENLOAD = "openload",
|
||||||
ANOTHER = "another",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MWEmbed = {
|
export type MWEmbed = {
|
||||||
@ -17,6 +16,7 @@ export type MWEmbedContext = {
|
|||||||
|
|
||||||
export type MWEmbedScraper = {
|
export type MWEmbedScraper = {
|
||||||
id: string;
|
id: string;
|
||||||
|
displayName: string;
|
||||||
for: MWEmbedType;
|
for: MWEmbedType;
|
||||||
rank: number;
|
rank: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
35
src/backend/helpers/fetch.ts
Normal file
35
src/backend/helpers/fetch.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { conf } from "@/setup/config";
|
||||||
|
import { ofetch } from "ofetch";
|
||||||
|
|
||||||
|
type P<T> = Parameters<typeof ofetch<T>>;
|
||||||
|
type R<T> = ReturnType<typeof ofetch<T>>;
|
||||||
|
|
||||||
|
const baseFetch = ofetch.create({
|
||||||
|
retry: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function makeUrl(url: string, data: Record<string, string>) {
|
||||||
|
let parsedUrl: string = url;
|
||||||
|
Object.entries(data).forEach(([k, v]) => {
|
||||||
|
parsedUrl = parsedUrl.replace(`{${k}}`, encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
return parsedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mwFetch<T>(url: string, ops: P<T>[1]): R<T> {
|
||||||
|
return baseFetch<T>(url, ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proxiedFetch<T>(url: string, ops: P<T>[1]): R<T> {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
Object.entries(ops?.params ?? {}).forEach(([k, v]) => {
|
||||||
|
parsedUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
return baseFetch<T>(conf().BASE_PROXY_URL, {
|
||||||
|
...ops,
|
||||||
|
baseURL: undefined,
|
||||||
|
params: {
|
||||||
|
destination: parsedUrl.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -8,13 +8,26 @@ export type MWProviderScrapeResult = {
|
|||||||
embeds: MWEmbed[];
|
embeds: MWEmbed[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MWProviderContext = {
|
type MWProviderBase = {
|
||||||
progress(percentage: number): void;
|
progress(percentage: number): void;
|
||||||
media: DetailedMeta;
|
media: DetailedMeta;
|
||||||
};
|
};
|
||||||
|
type MWProviderTypeSpecific =
|
||||||
|
| {
|
||||||
|
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||||
|
episode?: undefined;
|
||||||
|
season?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MWMediaType.SERIES;
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
};
|
||||||
|
export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase;
|
||||||
|
|
||||||
export type MWProvider = {
|
export type MWProvider = {
|
||||||
id: string;
|
id: string;
|
||||||
|
displayName: string;
|
||||||
rank: number;
|
rank: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
type: MWMediaType[];
|
type: MWMediaType[];
|
||||||
|
@ -1,38 +1,60 @@
|
|||||||
import { MWProviderScrapeResult } from "./provider";
|
import { MWProviderContext, MWProviderScrapeResult } from "./provider";
|
||||||
import { getEmbedScraperByType, getProviders } from "./register";
|
import { getEmbedScraperByType, getProviders } from "./register";
|
||||||
import { runEmbedScraper, runProvider } from "./run";
|
import { runEmbedScraper, runProvider } from "./run";
|
||||||
import { MWStream } from "./streams";
|
import { MWStream } from "./streams";
|
||||||
import { DetailedMeta } from "../metadata/getmeta";
|
import { DetailedMeta } from "../metadata/getmeta";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
|
||||||
interface MWProgressData {
|
interface MWProgressData {
|
||||||
type: "embed" | "provider";
|
type: "embed" | "provider";
|
||||||
id: string;
|
id: string;
|
||||||
|
eventId: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
errored: boolean;
|
errored: boolean;
|
||||||
}
|
}
|
||||||
interface MWNextData {
|
interface MWNextData {
|
||||||
id: string;
|
id: string;
|
||||||
|
eventId: string;
|
||||||
type: "embed" | "provider";
|
type: "embed" | "provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MWProviderRunContext {
|
type MWProviderRunContextBase = {
|
||||||
media: DetailedMeta;
|
media: DetailedMeta;
|
||||||
onProgress?: (data: MWProgressData) => void;
|
onProgress?: (data: MWProgressData) => void;
|
||||||
onNext?: (data: MWNextData) => void;
|
onNext?: (data: MWNextData) => void;
|
||||||
}
|
};
|
||||||
|
type MWProviderRunContextTypeSpecific =
|
||||||
|
| {
|
||||||
|
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||||
|
episode: undefined;
|
||||||
|
season: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MWMediaType.SERIES;
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MWProviderRunContext = MWProviderRunContextBase &
|
||||||
|
MWProviderRunContextTypeSpecific;
|
||||||
|
|
||||||
async function findBestEmbedStream(
|
async function findBestEmbedStream(
|
||||||
result: MWProviderScrapeResult,
|
result: MWProviderScrapeResult,
|
||||||
|
providerId: string,
|
||||||
ctx: MWProviderRunContext
|
ctx: MWProviderRunContext
|
||||||
): Promise<MWStream | null> {
|
): Promise<MWStream | null> {
|
||||||
if (result.stream) return result.stream;
|
if (result.stream) return result.stream;
|
||||||
|
|
||||||
|
let embedNum = 0;
|
||||||
for (const embed of result.embeds) {
|
for (const embed of result.embeds) {
|
||||||
|
embedNum += 1;
|
||||||
if (!embed.type) continue;
|
if (!embed.type) continue;
|
||||||
const scraper = getEmbedScraperByType(embed.type);
|
const scraper = getEmbedScraperByType(embed.type);
|
||||||
if (!scraper) throw new Error("Type for embed not found");
|
if (!scraper) throw new Error("Type for embed not found");
|
||||||
|
|
||||||
ctx.onNext?.({ id: scraper.id, type: "embed" });
|
const eventId = [providerId, scraper.id, embedNum].join("|");
|
||||||
|
|
||||||
|
ctx.onNext?.({ id: scraper.id, type: "embed", eventId });
|
||||||
|
|
||||||
let stream: MWStream;
|
let stream: MWStream;
|
||||||
try {
|
try {
|
||||||
@ -41,6 +63,7 @@ async function findBestEmbedStream(
|
|||||||
progress(num) {
|
progress(num) {
|
||||||
ctx.onProgress?.({
|
ctx.onProgress?.({
|
||||||
errored: false,
|
errored: false,
|
||||||
|
eventId,
|
||||||
id: scraper.id,
|
id: scraper.id,
|
||||||
percentage: num,
|
percentage: num,
|
||||||
type: "embed",
|
type: "embed",
|
||||||
@ -50,6 +73,7 @@ async function findBestEmbedStream(
|
|||||||
} catch {
|
} catch {
|
||||||
ctx.onProgress?.({
|
ctx.onProgress?.({
|
||||||
errored: true,
|
errored: true,
|
||||||
|
eventId,
|
||||||
id: scraper.id,
|
id: scraper.id,
|
||||||
percentage: 100,
|
percentage: 100,
|
||||||
type: "embed",
|
type: "embed",
|
||||||
@ -59,6 +83,7 @@ async function findBestEmbedStream(
|
|||||||
|
|
||||||
ctx.onProgress?.({
|
ctx.onProgress?.({
|
||||||
errored: false,
|
errored: false,
|
||||||
|
eventId,
|
||||||
id: scraper.id,
|
id: scraper.id,
|
||||||
percentage: 100,
|
percentage: 100,
|
||||||
type: "embed",
|
type: "embed",
|
||||||
@ -76,24 +101,48 @@ export async function findBestStream(
|
|||||||
const providers = getProviders();
|
const providers = getProviders();
|
||||||
|
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
ctx.onNext?.({ id: provider.id, type: "provider" });
|
const eventId = provider.id;
|
||||||
|
ctx.onNext?.({ id: provider.id, type: "provider", eventId });
|
||||||
let result: MWProviderScrapeResult;
|
let result: MWProviderScrapeResult;
|
||||||
try {
|
try {
|
||||||
result = await runProvider(provider, {
|
let context: MWProviderContext;
|
||||||
media: ctx.media,
|
if (ctx.type === MWMediaType.SERIES) {
|
||||||
progress(num) {
|
context = {
|
||||||
ctx.onProgress?.({
|
media: ctx.media,
|
||||||
percentage: num,
|
type: ctx.type,
|
||||||
errored: false,
|
episode: ctx.episode,
|
||||||
id: provider.id,
|
season: ctx.season,
|
||||||
type: "provider",
|
progress(num) {
|
||||||
});
|
ctx.onProgress?.({
|
||||||
},
|
percentage: num,
|
||||||
});
|
eventId,
|
||||||
|
errored: false,
|
||||||
|
id: provider.id,
|
||||||
|
type: "provider",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
context = {
|
||||||
|
media: ctx.media,
|
||||||
|
type: ctx.type,
|
||||||
|
progress(num) {
|
||||||
|
ctx.onProgress?.({
|
||||||
|
percentage: num,
|
||||||
|
eventId,
|
||||||
|
errored: false,
|
||||||
|
id: provider.id,
|
||||||
|
type: "provider",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
result = await runProvider(provider, context);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.onProgress?.({
|
ctx.onProgress?.({
|
||||||
percentage: 100,
|
percentage: 100,
|
||||||
errored: true,
|
errored: true,
|
||||||
|
eventId,
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
type: "provider",
|
type: "provider",
|
||||||
});
|
});
|
||||||
@ -103,11 +152,12 @@ export async function findBestStream(
|
|||||||
ctx.onProgress?.({
|
ctx.onProgress?.({
|
||||||
errored: false,
|
errored: false,
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
|
eventId,
|
||||||
percentage: 100,
|
percentage: 100,
|
||||||
type: "provider",
|
type: "provider",
|
||||||
});
|
});
|
||||||
|
|
||||||
const stream = await findBestEmbedStream(result, ctx);
|
const stream = await findBestEmbedStream(result, provider.id, ctx);
|
||||||
if (!stream) continue;
|
if (!stream) continue;
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,10 @@ import { initializeScraperStore } from "./helpers/register";
|
|||||||
|
|
||||||
// TODO backend system:
|
// TODO backend system:
|
||||||
// - caption support
|
// - caption support
|
||||||
// - hooks to run all providers one by one
|
|
||||||
// - move over old providers to new system
|
// - move over old providers to new system
|
||||||
// - implement jons providers/embedscrapers
|
// - implement jons providers/embedscrapers
|
||||||
// - show/episode support
|
|
||||||
|
|
||||||
// providers
|
// providers
|
||||||
// -- nothing here yet
|
|
||||||
import "./providers/gdriveplayer";
|
import "./providers/gdriveplayer";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { formatJWMeta, JWMediaResult } from "./justwatch";
|
import { FetchError } from "ofetch";
|
||||||
|
import { makeUrl, mwFetch } from "../helpers/fetch";
|
||||||
|
import {
|
||||||
|
formatJWMeta,
|
||||||
|
JWMediaResult,
|
||||||
|
JW_API_BASE,
|
||||||
|
mediaTypeToJW,
|
||||||
|
} from "./justwatch";
|
||||||
import { MWMediaMeta, MWMediaType } from "./types";
|
import { MWMediaMeta, MWMediaType } from "./types";
|
||||||
|
|
||||||
const JW_API_BASE = "https://apis.justwatch.com";
|
|
||||||
|
|
||||||
// http://localhost:5173/#/media/movie-439596/
|
|
||||||
|
|
||||||
type JWExternalIdType =
|
type JWExternalIdType =
|
||||||
| "eidr"
|
| "eidr"
|
||||||
| "imdb_latest"
|
| "imdb_latest"
|
||||||
@ -31,18 +34,23 @@ export interface DetailedMeta {
|
|||||||
export async function getMetaFromId(
|
export async function getMetaFromId(
|
||||||
type: MWMediaType,
|
type: MWMediaType,
|
||||||
id: string
|
id: string
|
||||||
): Promise<DetailedMeta> {
|
): Promise<DetailedMeta | null> {
|
||||||
let queryType = "";
|
const queryType = mediaTypeToJW(type);
|
||||||
if (type === MWMediaType.MOVIE) queryType = "movie";
|
|
||||||
else if (type === MWMediaType.SERIES) queryType = "show";
|
|
||||||
else if (type === MWMediaType.ANIME)
|
|
||||||
throw new Error("Anime search type is not supported");
|
|
||||||
|
|
||||||
const data = await fetch(
|
let data: JWDetailedMeta;
|
||||||
`${JW_API_BASE}/content/titles/${queryType}/${encodeURIComponent(
|
try {
|
||||||
id
|
const url = makeUrl("/content/titles/{type}/{id}/locale/en_US", {
|
||||||
)}/locale/en_US`
|
type: queryType,
|
||||||
).then((res) => res.json() as Promise<JWDetailedMeta>);
|
id,
|
||||||
|
});
|
||||||
|
data = await mwFetch<JWDetailedMeta>(url, { baseURL: JW_API_BASE });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof FetchError) {
|
||||||
|
// 400 and 404 are treated as not found
|
||||||
|
if (err.statusCode === 400 || err.statusCode === 404) return null;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const imdbId = data.external_ids.find(
|
const imdbId = data.external_ids.find(
|
||||||
(v) => v.provider === "imdb_latest"
|
(v) => v.provider === "imdb_latest"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { MWMediaType } from "./types";
|
import { MWMediaType } from "./types";
|
||||||
|
|
||||||
export const JW_API_BASE = "https://apis.justwatch.com";
|
export const JW_API_BASE = "https://apis.justwatch.com";
|
||||||
|
export const JW_IMAGE_BASE = "https://images.justwatch.com";
|
||||||
|
|
||||||
export type JWContentTypes = "movie" | "show";
|
export type JWContentTypes = "movie" | "show";
|
||||||
|
|
||||||
@ -32,10 +33,7 @@ export function formatJWMeta(media: JWMediaResult) {
|
|||||||
id: media.id.toString(),
|
id: media.id.toString(),
|
||||||
year: media.original_release_year.toString(),
|
year: media.original_release_year.toString(),
|
||||||
poster: media.poster
|
poster: media.poster
|
||||||
? `https://images.justwatch.com${media.poster.replace(
|
? `${JW_IMAGE_BASE}${media.poster.replace("{profile}", "s166")}`
|
||||||
"{profile}",
|
|
||||||
"s166"
|
|
||||||
)}`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
|
import { SimpleCache } from "@/utils/cache";
|
||||||
|
import { mwFetch } from "../helpers/fetch";
|
||||||
import {
|
import {
|
||||||
formatJWMeta,
|
formatJWMeta,
|
||||||
JWContentTypes,
|
JWContentTypes,
|
||||||
JWMediaResult,
|
JWMediaResult,
|
||||||
JW_API_BASE,
|
JW_API_BASE,
|
||||||
|
mediaTypeToJW,
|
||||||
} from "./justwatch";
|
} from "./justwatch";
|
||||||
import { MWMediaMeta, MWMediaType, MWQuery } from "./types";
|
import { MWMediaMeta, MWQuery } from "./types";
|
||||||
|
|
||||||
|
const cache = new SimpleCache<MWQuery, MWMediaMeta[]>();
|
||||||
|
cache.setCompare((a, b) => {
|
||||||
|
return a.type === b.type && a.searchQuery.trim() === b.searchQuery.trim();
|
||||||
|
});
|
||||||
|
cache.initialize();
|
||||||
|
|
||||||
type JWSearchQuery = {
|
type JWSearchQuery = {
|
||||||
content_types: JWContentTypes[];
|
content_types: JWContentTypes[];
|
||||||
@ -21,26 +30,29 @@ type JWPage<T> = {
|
|||||||
total_results: number;
|
total_results: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function searchForMedia({
|
export async function searchForMedia(query: MWQuery): Promise<MWMediaMeta[]> {
|
||||||
searchQuery,
|
if (cache.has(query)) return cache.get(query) as MWMediaMeta[];
|
||||||
type,
|
const { searchQuery, type } = query;
|
||||||
}: MWQuery): Promise<MWMediaMeta[]> {
|
|
||||||
|
const contentType = mediaTypeToJW(type);
|
||||||
const body: JWSearchQuery = {
|
const body: JWSearchQuery = {
|
||||||
content_types: [],
|
content_types: [contentType],
|
||||||
page: 1,
|
page: 1,
|
||||||
query: searchQuery,
|
query: searchQuery,
|
||||||
page_size: 40,
|
page_size: 40,
|
||||||
};
|
};
|
||||||
if (type === MWMediaType.MOVIE) body.content_types.push("movie");
|
|
||||||
else if (type === MWMediaType.SERIES) body.content_types.push("show");
|
|
||||||
else if (type === MWMediaType.ANIME)
|
|
||||||
throw new Error("Anime search type is not supported");
|
|
||||||
|
|
||||||
const data = await fetch(
|
const data = await mwFetch<JWPage<JWMediaResult>>(
|
||||||
`${JW_API_BASE}/content/titles/en_US/popular?body=${encodeURIComponent(
|
"/content/titles/en_US/popular",
|
||||||
JSON.stringify(body)
|
{
|
||||||
)}`
|
baseURL: JW_API_BASE,
|
||||||
).then((res) => res.json() as Promise<JWPage<JWMediaResult>>);
|
params: {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
|
const returnData = data.items.map<MWMediaMeta>((v) => formatJWMeta(v));
|
||||||
|
cache.set(query, returnData, 3600); // cache for an hour
|
||||||
|
return returnData;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { conf } from "@/setup/config";
|
import { unpack } from "unpacker";
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
|
||||||
import { registerProvider } from "@/backend/helpers/register";
|
import { registerProvider } from "@/backend/helpers/register";
|
||||||
import { MWMediaType } from "@/backend/metadata/types";
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { MWStreamQuality } from "@/backend/helpers/streams";
|
import { MWStreamQuality } from "@/backend/helpers/streams";
|
||||||
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { unpack } from "unpacker";
|
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const format = {
|
const format = {
|
||||||
stringify: (cipher: any) => {
|
stringify: (cipher: any) => {
|
||||||
@ -34,16 +34,20 @@ const format = {
|
|||||||
|
|
||||||
registerProvider({
|
registerProvider({
|
||||||
id: "gdriveplayer",
|
id: "gdriveplayer",
|
||||||
|
displayName: "gdriveplayer",
|
||||||
rank: 69,
|
rank: 69,
|
||||||
type: [MWMediaType.MOVIE],
|
type: [MWMediaType.MOVIE],
|
||||||
|
|
||||||
async scrape({ progress, media: { imdbId } }) {
|
async scrape({ progress, media: { imdbId } }) {
|
||||||
progress(10);
|
progress(10);
|
||||||
const streamRes = await fetch(
|
const streamRes = await proxiedFetch<string>(
|
||||||
`${
|
"https://database.gdriveplayer.us/player.php",
|
||||||
conf().CORS_PROXY_URL
|
{
|
||||||
}https://database.gdriveplayer.us/player.php?imdb=${imdbId}`
|
params: {
|
||||||
).then((d) => d.text());
|
imdb: imdbId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
progress(90);
|
progress(90);
|
||||||
const page = new DOMParser().parseFromString(streamRes, "text/html");
|
const page = new DOMParser().parseFromString(streamRes, "text/html");
|
||||||
|
|
||||||
@ -67,6 +71,7 @@ registerProvider({
|
|||||||
{ format }
|
{ format }
|
||||||
).toString(CryptoJS.enc.Utf8)
|
).toString(CryptoJS.enc.Utf8)
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const sources = JSON.parse(
|
const sources = JSON.parse(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
|
@ -5,6 +5,62 @@ import { Link } from "@/components/text/Link";
|
|||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
interface ErrorShowcaseProps {
|
||||||
|
error: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorShowcase(props: ErrorShowcaseProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
|
||||||
|
<p className="mb-1 break-words font-bold text-white">
|
||||||
|
{props.error.name} - {props.error.description}
|
||||||
|
</p>
|
||||||
|
<p className="break-words">{props.error.path}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorMessageProps {
|
||||||
|
error?: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorMessage(props: ErrorMessageProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div className="flex flex-col items-center justify-start text-center">
|
||||||
|
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||||
|
<Title>Whoops, it broke</Title>
|
||||||
|
{props.children ? (
|
||||||
|
props.children
|
||||||
|
) : (
|
||||||
|
<p className="my-6 max-w-lg">
|
||||||
|
The app encountered an error and wasn't able to recover, please
|
||||||
|
report it to the{" "}
|
||||||
|
<Link url={conf().DISCORD_LINK} newTab>
|
||||||
|
Discord server
|
||||||
|
</Link>{" "}
|
||||||
|
or on{" "}
|
||||||
|
<Link url={conf().GITHUB_LINK} newTab>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.error ? <ErrorShowcase error={props.error} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
error?: {
|
error?: {
|
||||||
@ -50,33 +106,6 @@ export class ErrorBoundary extends Component<
|
|||||||
render() {
|
render() {
|
||||||
if (!this.state.hasError) return this.props.children as any;
|
if (!this.state.hasError) return this.props.children as any;
|
||||||
|
|
||||||
return (
|
return <ErrorMessage error={this.state.error} />;
|
||||||
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
|
|
||||||
<div className="flex flex-col items-center justify-start text-center">
|
|
||||||
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
|
||||||
<Title>Whoops, it broke</Title>
|
|
||||||
<p className="my-6 max-w-lg">
|
|
||||||
The app encountered an error and wasn't able to recover, please
|
|
||||||
report it to the{" "}
|
|
||||||
<Link url={conf().DISCORD_LINK} newTab>
|
|
||||||
Discord server
|
|
||||||
</Link>{" "}
|
|
||||||
or on{" "}
|
|
||||||
<Link url={conf().GITHUB_LINK} newTab>
|
|
||||||
GitHub
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{this.state.error ? (
|
|
||||||
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
|
|
||||||
<p className="mb-1 break-words font-bold text-white">
|
|
||||||
{this.state.error.name} - {this.state.error.description}
|
|
||||||
</p>
|
|
||||||
<p className="break-words">{this.state.error.path}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ interface VideoPlayerHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||||
const showDivider = props.title || props.onClick;
|
const showDivider = props.title && props.onClick;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex flex-1 items-center">
|
<div className="flex flex-1 items-center">
|
||||||
|
12
src/hooks/useGoBack.ts
Normal file
12
src/hooks/useGoBack.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
export function useGoBack() {
|
||||||
|
const reactHistory = useHistory();
|
||||||
|
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
if (reactHistory.action !== "POP") reactHistory.goBack();
|
||||||
|
else reactHistory.push("/");
|
||||||
|
}, [reactHistory]);
|
||||||
|
return goBack;
|
||||||
|
}
|
@ -1,16 +1,30 @@
|
|||||||
import { findBestStream } from "@/backend/helpers/scrape";
|
import { findBestStream } from "@/backend/helpers/scrape";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface ScrapeEventLog {
|
export interface ScrapeEventLog {
|
||||||
type: "provider" | "embed";
|
type: "provider" | "embed";
|
||||||
errored: boolean;
|
errored: boolean;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
eventId: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScrape(meta: DetailedMeta) {
|
export type SelectedMediaData =
|
||||||
|
| {
|
||||||
|
type: MWMediaType.SERIES;
|
||||||
|
episode: number;
|
||||||
|
season: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||||
|
episode: undefined;
|
||||||
|
season: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useScrape(meta: DetailedMeta, selected: SelectedMediaData) {
|
||||||
const [eventLog, setEventLog] = useState<ScrapeEventLog[]>([]);
|
const [eventLog, setEventLog] = useState<ScrapeEventLog[]>([]);
|
||||||
const [stream, setStream] = useState<MWStream | null>(null);
|
const [stream, setStream] = useState<MWStream | null>(null);
|
||||||
const [pending, setPending] = useState(true);
|
const [pending, setPending] = useState(true);
|
||||||
@ -22,12 +36,14 @@ export function useScrape(meta: DetailedMeta) {
|
|||||||
(async () => {
|
(async () => {
|
||||||
const scrapedStream = await findBestStream({
|
const scrapedStream = await findBestStream({
|
||||||
media: meta,
|
media: meta,
|
||||||
|
...selected,
|
||||||
onNext(ctx) {
|
onNext(ctx) {
|
||||||
setEventLog((arr) => [
|
setEventLog((arr) => [
|
||||||
...arr,
|
...arr,
|
||||||
{
|
{
|
||||||
errored: false,
|
errored: false,
|
||||||
id: ctx.id,
|
id: ctx.id,
|
||||||
|
eventId: ctx.eventId,
|
||||||
type: ctx.type,
|
type: ctx.type,
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
},
|
},
|
||||||
@ -48,7 +64,7 @@ export function useScrape(meta: DetailedMeta) {
|
|||||||
setPending(false);
|
setPending(false);
|
||||||
setStream(scrapedStream);
|
setStream(scrapedStream);
|
||||||
})();
|
})();
|
||||||
}, [meta]);
|
}, [meta, selected]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream,
|
stream,
|
||||||
|
@ -33,6 +33,9 @@ if (key) {
|
|||||||
// - devices: ipadOS
|
// - devices: ipadOS
|
||||||
// - features: HLS, error handling, preload interactions
|
// - features: HLS, error handling, preload interactions
|
||||||
|
|
||||||
|
// TODO general todos:
|
||||||
|
// - localize everything
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
49
src/views/media/MediaErrorView.tsx
Normal file
49
src/views/media/MediaErrorView.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
|
import { Link } from "@/components/text/Link";
|
||||||
|
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||||
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
export function MediaFetchErrorView() {
|
||||||
|
const goBack = useGoBack();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex-1">
|
||||||
|
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||||
|
<VideoPlayerHeader onClick={goBack} />
|
||||||
|
</div>
|
||||||
|
<ErrorMessage>
|
||||||
|
<p className="my-6 max-w-lg">
|
||||||
|
We failed to request the media you asked for, check your internet
|
||||||
|
connection and try again.
|
||||||
|
</p>
|
||||||
|
</ErrorMessage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MediaPlaybackErrorView(props: { title?: string }) {
|
||||||
|
const goBack = useGoBack();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex-1">
|
||||||
|
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||||
|
<VideoPlayerHeader onClick={goBack} title={props.title} />
|
||||||
|
</div>
|
||||||
|
<ErrorMessage>
|
||||||
|
<p className="my-6 max-w-lg">
|
||||||
|
We encountered an error while playing the video you requested. If this
|
||||||
|
keeps happening please report the issue to the
|
||||||
|
<Link url={conf().DISCORD_LINK} newTab>
|
||||||
|
Discord server
|
||||||
|
</Link>{" "}
|
||||||
|
or on{" "}
|
||||||
|
<Link url={conf().GITHUB_LINK} newTab>
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</ErrorMessage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -71,7 +71,7 @@ export function MediaScrapeLog(props: MediaScrapeLogProps) {
|
|||||||
>
|
>
|
||||||
<MediaScrapePillSkeleton />
|
<MediaScrapePillSkeleton />
|
||||||
{props.events.map((v) => (
|
{props.events.map((v) => (
|
||||||
<MediaScrapePill event={v} key={v.id} />
|
<MediaScrapePill event={v} key={v.eventId} />
|
||||||
))}
|
))}
|
||||||
<MediaScrapePillSkeleton />
|
<MediaScrapePillSkeleton />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
import { DecoratedVideoPlayer } from "@/components/video/DecoratedVideoPlayer";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
import { MWStream } from "@/backend/helpers/streams";
|
||||||
import { useScrape } from "@/hooks/useScrape";
|
import { SelectedMediaData, useScrape } from "@/hooks/useScrape";
|
||||||
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { JWMediaToMediaType } from "@/backend/metadata/justwatch";
|
import { JWMediaToMediaType } from "@/backend/metadata/justwatch";
|
||||||
import { SourceControl } from "@/components/video/controls/SourceControl";
|
import { SourceControl } from "@/components/video/controls/SourceControl";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
|
import { MWMediaType } from "@/backend/metadata/types";
|
||||||
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||||
|
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
||||||
|
|
||||||
function MediaViewLoading(props: { onGoBack(): void }) {
|
function MediaViewLoading(props: { onGoBack(): void }) {
|
||||||
return (
|
return (
|
||||||
@ -28,9 +35,10 @@ interface MediaViewScrapingProps {
|
|||||||
onStream(stream: MWStream): void;
|
onStream(stream: MWStream): void;
|
||||||
onGoBack(): void;
|
onGoBack(): void;
|
||||||
meta: DetailedMeta;
|
meta: DetailedMeta;
|
||||||
|
selected: SelectedMediaData;
|
||||||
}
|
}
|
||||||
function MediaViewScraping(props: MediaViewScrapingProps) {
|
function MediaViewScraping(props: MediaViewScrapingProps) {
|
||||||
const { eventLog, stream } = useScrape(props.meta);
|
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
@ -38,8 +46,6 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
|||||||
}
|
}
|
||||||
}, [stream, props]);
|
}, [stream, props]);
|
||||||
|
|
||||||
// TODO error screen if no streams found
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen items-center justify-center">
|
<div className="relative flex h-screen items-center justify-center">
|
||||||
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
||||||
@ -48,44 +54,91 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
|||||||
title={props.meta.meta.title}
|
title={props.meta.meta.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center transition-opacity duration-200">
|
||||||
<Loading className="mb-4" />
|
{pending ? (
|
||||||
<p className="mb-8 text-denim-700">Finding the best video for you</p>
|
<>
|
||||||
<MediaScrapeLog events={eventLog} />
|
<Loading />
|
||||||
|
<p className="mb-8 text-denim-700">
|
||||||
|
Finding the best video for you
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
||||||
|
<p className="mb-8 text-denim-700">
|
||||||
|
Whoops, could't find any videos for you
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-center transition-opacity duration-200 ${
|
||||||
|
pending ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MediaScrapeLog events={eventLog} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaView() {
|
export function MediaView() {
|
||||||
const reactHistory = useHistory();
|
|
||||||
const params = useParams<{ media: string }>();
|
const params = useParams<{ media: string }>();
|
||||||
const goBack = useCallback(() => {
|
const goBack = useGoBack();
|
||||||
if (reactHistory.action !== "POP") reactHistory.goBack();
|
|
||||||
else reactHistory.push("/");
|
|
||||||
}, [reactHistory]);
|
|
||||||
|
|
||||||
const [meta, setMeta] = useState<DetailedMeta | null>(null);
|
const [meta, setMeta] = useState<DetailedMeta | null>(null);
|
||||||
|
const [selected, setSelected] = useState<SelectedMediaData | null>(null);
|
||||||
|
const [exec, loading, error] = useLoading(async (mediaParams: string) => {
|
||||||
|
let type: MWMediaType;
|
||||||
|
let id = "";
|
||||||
|
try {
|
||||||
|
const [t, i] = mediaParams.split("-", 2);
|
||||||
|
type = JWMediaToMediaType(t);
|
||||||
|
id = i;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getMetaFromId(type, id);
|
||||||
|
});
|
||||||
const [stream, setStream] = useState<MWStream | null>(null);
|
const [stream, setStream] = useState<MWStream | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO handle errors
|
exec(params.media).then((v) => {
|
||||||
(async () => {
|
setMeta(v ?? null);
|
||||||
const [t, id] = params.media.split("-", 2);
|
if (v)
|
||||||
const type = JWMediaToMediaType(t);
|
setSelected({
|
||||||
const fetchedMeta = await getMetaFromId(type, id);
|
type: v.meta.type,
|
||||||
setMeta(fetchedMeta);
|
episode: 0 as any,
|
||||||
})();
|
season: 0 as any,
|
||||||
}, [setMeta, params]);
|
});
|
||||||
|
else setSelected(null);
|
||||||
|
});
|
||||||
|
}, [exec, params.media]);
|
||||||
|
|
||||||
// TODO watched store
|
// TODO watched store
|
||||||
// TODO error page with video header
|
// TODO error page with video header
|
||||||
|
|
||||||
if (!meta) return <MediaViewLoading onGoBack={goBack} />;
|
if (loading) return <MediaViewLoading onGoBack={goBack} />;
|
||||||
|
if (error) return <MediaFetchErrorView />;
|
||||||
|
if (!meta || !selected)
|
||||||
|
return (
|
||||||
|
<NotFoundWrapper video>
|
||||||
|
<NotFoundMedia />
|
||||||
|
</NotFoundWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// scraping view will start scraping and return with onStream
|
||||||
if (!stream)
|
if (!stream)
|
||||||
return (
|
return (
|
||||||
<MediaViewScraping meta={meta} onGoBack={goBack} onStream={setStream} />
|
<MediaViewScraping
|
||||||
|
meta={meta}
|
||||||
|
selected={selected}
|
||||||
|
onGoBack={goBack}
|
||||||
|
onStream={setStream}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// show stream once we have a stream
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen">
|
<div className="h-screen w-screen">
|
||||||
<DecoratedVideoPlayer title={meta.meta.title} onGoBack={goBack} autoPlay>
|
<DecoratedVideoPlayer title={meta.meta.title} onGoBack={goBack} autoPlay>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { ReactElement } from "react";
|
|
||||||
|
|
||||||
export interface NotFoundChecksProps {
|
|
||||||
id: string;
|
|
||||||
children?: ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Component that only renders children if the passed in data is fully correct
|
|
||||||
*/
|
|
||||||
export function NotFoundChecks(
|
|
||||||
props: NotFoundChecksProps
|
|
||||||
): ReactElement | null {
|
|
||||||
// TODO do notfound check
|
|
||||||
|
|
||||||
return props.children || null;
|
|
||||||
}
|
|
@ -5,11 +5,24 @@ import { Icons } from "@/components/Icon";
|
|||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
import { ArrowLink } from "@/components/text/ArrowLink";
|
import { ArrowLink } from "@/components/text/ArrowLink";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
|
import { VideoPlayerHeader } from "@/components/video/parts/VideoPlayerHeader";
|
||||||
|
|
||||||
|
export function NotFoundWrapper(props: {
|
||||||
|
children?: ReactNode;
|
||||||
|
video?: boolean;
|
||||||
|
}) {
|
||||||
|
const goBack = useGoBack();
|
||||||
|
|
||||||
function NotFoundWrapper(props: { children?: ReactNode }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex-1">
|
<div className="h-screen flex-1">
|
||||||
<Navigation />
|
{props.video ? (
|
||||||
|
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||||
|
<VideoPlayerHeader onClick={goBack} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Navigation />
|
||||||
|
)}
|
||||||
<div className="flex h-full flex-col items-center justify-center p-5 text-center">
|
<div className="flex h-full flex-col items-center justify-center p-5 text-center">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +52,6 @@ export function SearchResultsView({ searchQuery }: { searchQuery: MWQuery }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO use cache
|
|
||||||
async function runSearch(query: MWQuery) {
|
async function runSearch(query: MWQuery) {
|
||||||
const searchResults = await runSearchQuery(query);
|
const searchResults = await runSearchQuery(query);
|
||||||
if (!searchResults) return;
|
if (!searchResults) return;
|
||||||
|
24
yarn.lock
24
yarn.lock
@ -974,6 +974,11 @@
|
|||||||
"depd@^1.1.2":
|
"depd@^1.1.2":
|
||||||
"version" "1.1.2"
|
"version" "1.1.2"
|
||||||
|
|
||||||
|
"destr@^1.2.1":
|
||||||
|
"integrity" "sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA=="
|
||||||
|
"resolved" "https://registry.npmjs.org/destr/-/destr-1.2.2.tgz"
|
||||||
|
"version" "1.2.2"
|
||||||
|
|
||||||
"detective@^5.2.1":
|
"detective@^5.2.1":
|
||||||
"integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw=="
|
"integrity" "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw=="
|
||||||
"resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz"
|
"resolved" "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz"
|
||||||
@ -2363,6 +2368,11 @@
|
|||||||
"negotiator@^0.6.3":
|
"negotiator@^0.6.3":
|
||||||
"version" "0.6.3"
|
"version" "0.6.3"
|
||||||
|
|
||||||
|
"node-fetch-native@^1.0.1":
|
||||||
|
"integrity" "sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg=="
|
||||||
|
"resolved" "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz"
|
||||||
|
"version" "1.0.1"
|
||||||
|
|
||||||
"node-fetch@2.6.7":
|
"node-fetch@2.6.7":
|
||||||
"integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="
|
"integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="
|
||||||
"resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
|
"resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
|
||||||
@ -2631,6 +2641,15 @@
|
|||||||
"define-properties" "^1.1.4"
|
"define-properties" "^1.1.4"
|
||||||
"es-abstract" "^1.20.4"
|
"es-abstract" "^1.20.4"
|
||||||
|
|
||||||
|
"ofetch@^1.0.0":
|
||||||
|
"integrity" "sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ=="
|
||||||
|
"resolved" "https://registry.npmjs.org/ofetch/-/ofetch-1.0.0.tgz"
|
||||||
|
"version" "1.0.0"
|
||||||
|
dependencies:
|
||||||
|
"destr" "^1.2.1"
|
||||||
|
"node-fetch-native" "^1.0.1"
|
||||||
|
"ufo" "^1.0.0"
|
||||||
|
|
||||||
"once@^1.3.0":
|
"once@^1.3.0":
|
||||||
"integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
|
"integrity" "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
|
||||||
"resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
"resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||||
@ -3430,6 +3449,11 @@
|
|||||||
"resolved" "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
|
"resolved" "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
|
||||||
"version" "4.9.4"
|
"version" "4.9.4"
|
||||||
|
|
||||||
|
"ufo@^1.0.0":
|
||||||
|
"integrity" "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA=="
|
||||||
|
"resolved" "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz"
|
||||||
|
"version" "1.0.1"
|
||||||
|
|
||||||
"unbox-primitive@^1.0.2":
|
"unbox-primitive@^1.0.2":
|
||||||
"integrity" "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="
|
"integrity" "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="
|
||||||
"resolved" "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
|
"resolved" "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user