From b3db58012f896996a2336cb85dedd53e8fef8902 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 19 Feb 2023 18:03:54 +0100 Subject: [PATCH] linting Co-authored-by: Jip Frijlink --- .eslintrc.js | 1 + src/backend/embeds/playm4u.ts | 29 ++- src/backend/embeds/streamm4u.ts | 97 ++++---- src/backend/helpers/embed.ts | 2 +- src/backend/helpers/scrape.ts | 18 +- src/backend/index.ts | 4 +- src/backend/providers/flixhq.ts | 5 +- src/backend/providers/m4ufree.ts | 223 ++++++++++-------- src/backend/providers/superstream/index.ts | 3 +- src/components/Overlay.tsx | 14 ++ src/components/SearchBar.tsx | 2 +- src/components/buttons/EditButton.tsx | 6 +- src/components/layout/ErrorBoundary.tsx | 7 +- src/components/layout/Modal.tsx | 25 ++ src/components/media/MediaCard.tsx | 32 +-- src/index.tsx | 7 +- src/state/watched/migrations/v2.ts | 13 +- src/utils/titleMatch.ts | 8 +- .../actions/CaptionsSelectionAction.tsx | 4 +- .../components/actions/PageTitleAction.tsx | 4 +- .../actions/SeriesSelectionAction.tsx | 2 +- .../actions/SourceSelectionAction.tsx | 2 +- .../hooks/useCurrentSeriesEpisodeInfo.ts | 6 +- .../popouts/CaptionSelectionPopout.tsx | 2 +- .../popouts/EpisodeSelectionPopout.tsx | 72 +++--- .../popouts/SourceSelectionPopout.tsx | 117 +++++---- src/video/state/types.ts | 1 - src/views/media/MediaErrorView.tsx | 11 +- src/views/media/MediaView.tsx | 19 +- src/views/notfound/NotFoundView.tsx | 2 +- src/views/other/v2Migration.tsx | 98 ++++---- src/views/search/SearchLoadingView.tsx | 17 +- src/views/search/SearchView.tsx | 2 +- vite.config.ts | 11 +- 34 files changed, 489 insertions(+), 377 deletions(-) create mode 100644 src/components/Overlay.tsx create mode 100644 src/components/layout/Modal.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 1556850a..40cda0da 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -50,6 +50,7 @@ module.exports = { "no-await-in-loop": "off", "no-nested-ternary": "off", "prefer-destructuring": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "react/jsx-filename-extension": [ "error", { extensions: [".js", ".tsx", ".jsx"] } diff --git a/src/backend/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts index 45d5e2f3..8328d337 100644 --- a/src/backend/embeds/playm4u.ts +++ b/src/backend/embeds/playm4u.ts @@ -1,20 +1,19 @@ import { MWEmbedType } from "@/backend/helpers/embed"; -import { MWMediaType } from "../metadata/types"; import { registerEmbedScraper } from "@/backend/helpers/register"; import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; registerEmbedScraper({ - id: "playm4u", - displayName: "playm4u", - for: MWEmbedType.PLAYM4U, - rank: 0, - async getStream(ctx) { - // throw new Error("Oh well 2") - return { - streamUrl: '', - quality: MWStreamQuality.Q1080P, - captions: [], - type: MWStreamType.MP4, - }; - }, -}) \ No newline at end of file + id: "playm4u", + displayName: "playm4u", + for: MWEmbedType.PLAYM4U, + rank: 0, + async getStream() { + // throw new Error("Oh well 2") + return { + streamUrl: "", + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts index f9b34c4a..d0eba66a 100644 --- a/src/backend/embeds/streamm4u.ts +++ b/src/backend/embeds/streamm4u.ts @@ -1,60 +1,65 @@ import { MWEmbedType } from "@/backend/helpers/embed"; -import { MWMediaType } from "../metadata/types"; import { registerEmbedScraper } from "@/backend/helpers/register"; -import { MWStreamQuality, MWStreamType, MWStream } from "@/backend/helpers/streams"; +import { + MWStreamQuality, + MWStreamType, + MWStream, +} from "@/backend/helpers/streams"; import { proxiedFetch } from "@/backend/helpers/fetch"; -const HOST = 'streamm4u.club'; +const HOST = "streamm4u.club"; const URL_BASE = `https://${HOST}`; const URL_API = `${URL_BASE}/api`; const URL_API_SOURCE = `${URL_API}/source`; +async function scrape(embed: string) { + const sources: MWStream[] = []; + + const embedID = embed.split("/").pop(); + + console.log(`${URL_API_SOURCE}/${embedID}`); + const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { + method: "POST", + body: `r=&d=${HOST}`, + }); + + if (json.success) { + const streams = json.data; + + for (const stream of streams) { + sources.push({ + streamUrl: stream.file as string, + quality: stream.label as MWStreamQuality, + type: stream.type as MWStreamType, + captions: [], + }); + } + } + + return sources; +} + // TODO check out 403 / 404 on successfully returned video stream URLs registerEmbedScraper({ - id: "streamm4u", - displayName: "streamm4u", - for: MWEmbedType.STREAMM4U, - rank: 100, - async getStream({ progress, url }) { + id: "streamm4u", + displayName: "streamm4u", + for: MWEmbedType.STREAMM4U, + rank: 100, + async getStream({ progress, url }) { + // const scrapingThreads = []; + // const streams = []; - const scrapingThreads = []; - let streams = []; + const sources = (await scrape(url)).sort( + (a, b) => + Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", "")) + ); + // const preferredSourceIndex = 0; + const preferredSource = sources[0]; - const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))); - let preferredSourceIndex = 0; - let preferredSource = sources[0]; + if (!preferredSource) throw new Error("No source found"); - if (!preferredSource) throw new Error("No source found") + progress(100); - progress(100) - - return preferredSource - }, -}) - -async function scrape(embed: string) { - const sources: MWStream[] = []; - - const embedID = embed.split('/').pop(); - - console.log(`${URL_API_SOURCE}/${embedID}`) - const json = await proxiedFetch(`${URL_API_SOURCE}/${embedID}`, { - method: 'POST', - body: `r=&d=${HOST}` - }); - - if (json.success) { - const streams = json.data; - - for (const stream of streams) { - sources.push({ - streamUrl: stream.file as string, - quality: stream.label as MWStreamQuality, - type: stream.type as MWStreamType, - captions: [] - }); - } - } - - return sources; -} \ No newline at end of file + return preferredSource; + }, +}); diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 4dc6ee95..64d039b7 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -3,7 +3,7 @@ import { MWStream } from "./streams"; export enum MWEmbedType { M4UFREE = "m4ufree", STREAMM4U = "streamm4u", - PLAYM4U = "playm4u" + PLAYM4U = "playm4u", } export type MWEmbed = { diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts index 7805fa4c..cb160305 100644 --- a/src/backend/helpers/scrape.ts +++ b/src/backend/helpers/scrape.ts @@ -25,15 +25,15 @@ type MWProviderRunContextBase = { }; type MWProviderRunContextTypeSpecific = | { - type: MWMediaType.MOVIE | MWMediaType.ANIME; - episode: undefined; - season: undefined; - } + type: MWMediaType.MOVIE | MWMediaType.ANIME; + episode: undefined; + season: undefined; + } | { - type: MWMediaType.SERIES; - episode: string; - season: string; - }; + type: MWMediaType.SERIES; + episode: string; + season: string; + }; export type MWProviderRunContext = MWProviderRunContextBase & MWProviderRunContextTypeSpecific; @@ -50,7 +50,7 @@ async function findBestEmbedStream( embedNum += 1; if (!embed.type) continue; const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Type for embed not found: " + embed.type); + if (!scraper) throw new Error(`Type for embed not found: ${embed.type}`); const eventId = [providerId, scraper.id, embedNum].join("|"); diff --git a/src/backend/index.ts b/src/backend/index.ts index 7261a45d..7a13a445 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -8,7 +8,7 @@ import "./providers/netfilm"; import "./providers/m4ufree"; // embeds -import "./embeds/streamm4u" -import "./embeds/playm4u" +import "./embeds/streamm4u"; +import "./embeds/playm4u"; initializeScraperStore(); diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index c8a89400..fdab1292 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -21,7 +21,10 @@ registerProvider({ } ); const foundItem = searchResults.results.find((v: any) => { - return compareTitle(v.title, media.meta.title) && v.releaseDate === media.meta.year; + return ( + compareTitle(v.title, media.meta.title) && + v.releaseDate === media.meta.year + ); }); if (!foundItem) throw new Error("No watchable item found"); const flixId = foundItem.id; diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts index 76e558db..f4e79f5b 100644 --- a/src/backend/providers/m4ufree.ts +++ b/src/backend/providers/m4ufree.ts @@ -1,11 +1,9 @@ -import { compareTitle } from "@/utils/titleMatch"; -import { MWEmbedType } from "../helpers/embed"; +import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { proxiedFetch } from "../helpers/fetch"; import { registerProvider } from "../helpers/register"; import { MWMediaType } from "../metadata/types"; -import { MWEmbed } from "@/backend/helpers/embed"; -const HOST = 'm4ufree.com'; +const HOST = "m4ufree.com"; const URL_BASE = `https://${HOST}`; const URL_SEARCH = `${URL_BASE}/search`; const URL_AJAX = `${URL_BASE}/ajax`; @@ -22,7 +20,7 @@ const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/; const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/; function toDom(html: string) { - return new DOMParser().parseFromString(html, "text/html") + return new DOMParser().parseFromString(html, "text/html"); } registerProvider({ @@ -32,9 +30,14 @@ registerProvider({ disabled: true, // Disables because the redirector URLs it returns will throw 404 / 403 depending on if you view it in the browser or fetch it respectively. It just does not work. type: [MWMediaType.MOVIE, MWMediaType.SERIES], - async scrape({ media, progress, type, episode: episodeId, season: seasonId }) { - const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1 - const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined + async scrape({ media, type, episode: episodeId, season: seasonId }) { + const season = + media.meta.seasons?.find((s) => s.id === seasonId)?.number || 1; + const episode = + media.meta.type === MWMediaType.SERIES + ? media.meta.seasonData.episodes.find((ep) => ep.id === episodeId) + ?.number || 1 + : undefined; const embeds: MWEmbed[] = []; @@ -43,39 +46,49 @@ registerProvider({ responseType: "text" as any, } */ - let responseText = await proxiedFetch(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`); + const responseText = await proxiedFetch( + `${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html` + ); let dom = toDom(responseText); - const searchResults = [...dom.querySelectorAll('.item')].map(element => { - const tooltipText = element.querySelector('.tiptitle p')?.innerHTML; - if (!tooltipText) return; + const searchResults = [...dom.querySelectorAll(".item")] + .map((element) => { + const tooltipText = element.querySelector(".tiptitle p")?.innerHTML; + if (!tooltipText) return; - let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); + let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText); - if (!regexResult || !regexResult[1] || !regexResult[2]) { - return; - } + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } - const title = regexResult[1]; - const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year - const a = element.querySelector('a'); - if (!a) return; - const href = a.href; + const title = regexResult[1]; + const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year + const a = element.querySelector("a"); + if (!a) return; + const href = a.href; - regexResult = REGEX_TYPE.exec(href); + regexResult = REGEX_TYPE.exec(href); - if (!regexResult || !regexResult[1]) { - return; - } + if (!regexResult || !regexResult[1]) { + return; + } - let scraperDeterminedType = regexResult[1]; + let scraperDeterminedType = regexResult[1]; - scraperDeterminedType = scraperDeterminedType === 'tvshow' ? 'show' : 'movie'; // * Map to Trakt type + scraperDeterminedType = + scraperDeterminedType === "tvshow" ? "show" : "movie"; // * Map to Trakt type - return { type: scraperDeterminedType, title, year, href }; - }).filter(item => item); + return { type: scraperDeterminedType, title, year, href }; + }) + .filter((item) => item); - const mediaInResults = searchResults.find(item => item && item.title === media.meta.title && item.year.toString() === media.meta.year); + const mediaInResults = searchResults.find( + (item) => + item && + item.title === media.meta.title && + item.year.toString() === media.meta.year + ); if (!mediaInResults) { // * Nothing found @@ -84,109 +97,124 @@ registerProvider({ }; } - let cookies: string | null = ''; - const responseTextFromMedia = await proxiedFetch(mediaInResults.href, { - onResponse(context) { - cookies = context.response.headers.get('X-Set-Cookie') - }, - }); + let cookies: string | null = ""; + const responseTextFromMedia = await proxiedFetch( + mediaInResults.href, + { + onResponse(context) { + cookies = context.response.headers.get("X-Set-Cookie"); + }, + } + ); dom = toDom(responseTextFromMedia); let regexResult = REGEX_COOKIES.exec(cookies); if (!regexResult || !regexResult[1] || !regexResult[2]) { // * DO SOMETHING? - throw new Error("No regexResults, yikesssssss kinda gross idk") + throw new Error("No regexResults, yikesssssss kinda gross idk"); } const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`; - const token = dom.querySelector('meta[name="csrf-token"]')?.getAttribute("content"); + const token = dom + .querySelector('meta[name="csrf-token"]') + ?.getAttribute("content"); if (!token) return { embeds }; if (type === MWMediaType.SERIES) { // * Get the season/episode data - const episodes = [...dom.querySelectorAll('.episode')].map(element => { - regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); + const episodes = [...dom.querySelectorAll(".episode")] + .map((element) => { + regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML); - if (!regexResult || !regexResult[1] || !regexResult[2]) { - return; - } + if (!regexResult || !regexResult[1] || !regexResult[2]) { + return; + } - const episode = Number(regexResult[1]); - const season = Number(regexResult[2]); + const newEpisode = Number(regexResult[1]); + const newSeason = Number(regexResult[2]); - return { - id: element.getAttribute('idepisode'), - episode: episode, - season: season - }; - }).filter(item => item); + return { + id: element.getAttribute("idepisode"), + episode: newEpisode, + season: newSeason, + }; + }) + .filter((item) => item); - const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season); - if (!ep) return { embeds } + const ep = episodes.find( + (newEp) => newEp && newEp.episode === episode && newEp.season === season + ); + if (!ep) return { embeds }; const form = `idepisode=${ep.id}&_token=${token}`; - let response = await proxiedFetch(URL_AJAX_TV, { - method: 'POST', + const response = await proxiedFetch(URL_AJAX_TV, { + method: "POST", headers: { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': "en-US,en;q=0.9", - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest', - 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', - 'Sec-CH-UA-Mobile': '?0', - 'Sec-CH-UA-Platform': '"Linux"', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Dest': 'empty', - 'X-Cookie': cookieHeader, - 'X-Origin': URL_BASE, - 'X-Referer': mediaInResults.href + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Sec-CH-UA": + '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + "Sec-CH-UA-Mobile": "?0", + "Sec-CH-UA-Platform": '"Linux"', + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "X-Cookie": cookieHeader, + "X-Origin": URL_BASE, + "X-Referer": mediaInResults.href, }, - body: form + body: form, }); dom = toDom(response); } - const servers = [...dom.querySelectorAll('.singlemv')].map(element => element.getAttribute('data')); + const servers = [...dom.querySelectorAll(".singlemv")].map((element) => + element.getAttribute("data") + ); for (const server of servers) { const form = `m4u=${server}&_token=${token}`; const response = await proxiedFetch(URL_AJAX, { - method: 'POST', + method: "POST", headers: { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': "en-US,en;q=0.9", - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - 'X-Requested-With': 'XMLHttpRequest', - 'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', - 'Sec-CH-UA-Mobile': '?0', - 'Sec-CH-UA-Platform': '"Linux"', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Dest': 'empty', - 'X-Cookie': cookieHeader, - 'X-Origin': URL_BASE, - 'X-Referer': mediaInResults.href + Accept: "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Sec-CH-UA": + '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"', + "Sec-CH-UA-Mobile": "?0", + "Sec-CH-UA-Platform": '"Linux"', + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "X-Cookie": cookieHeader, + "X-Origin": URL_BASE, + "X-Referer": mediaInResults.href, }, - body: form + body: form, }); - const dom = toDom(response); + const serverDom = toDom(response); - const link = dom.querySelector('iframe')?.src; + const link = serverDom.querySelector("iframe")?.src; const getEmbedType = (url: string) => { - if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U - if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U + if (url.startsWith("https://streamm4u.club")) + return MWEmbedType.STREAMM4U; + if (url.startsWith("https://play.playm4u.xyz")) + return MWEmbedType.PLAYM4U; return null; - } + }; if (!link) continue; @@ -194,15 +222,14 @@ registerProvider({ if (embedType) { embeds.push({ url: link, - type: embedType - }) - }; + type: embedType, + }); + } } console.log(embeds); return { embeds, - } - - } + }; + }, }); diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 0f20fb2d..8abed467 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -131,7 +131,8 @@ registerProvider({ const superstreamEntry = searchRes.find( (res: any) => - compareTitle(res.title, media.meta.title) && res.year === Number(media.meta.year) + compareTitle(res.title, media.meta.title) && + res.year === Number(media.meta.year) ); if (!superstreamEntry) throw new Error("No entry found on SuperStream"); diff --git a/src/components/Overlay.tsx b/src/components/Overlay.tsx new file mode 100644 index 00000000..243caedd --- /dev/null +++ b/src/components/Overlay.tsx @@ -0,0 +1,14 @@ +import { Helmet } from "react-helmet"; + +export function Overlay(props: { children: React.ReactNode }) { + return ( + <> + + + +
+ {props.children} +
+ + ); +} diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index ae332e8c..2b937549 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -67,7 +67,7 @@ export function SearchBarInput(props: SearchBarProps) { id: MWMediaType.SERIES, name: t("searchBar.series"), icon: Icons.CLAPPER_BOARD, - } + }, ]} onClick={() => setDropdownOpen((old) => !old)} > diff --git a/src/components/buttons/EditButton.tsx b/src/components/buttons/EditButton.tsx index 0b91e5ed..bcdd3cfd 100644 --- a/src/components/buttons/EditButton.tsx +++ b/src/components/buttons/EditButton.tsx @@ -10,7 +10,7 @@ export interface EditButtonProps { } export function EditButton(props: EditButtonProps) { - const { t } = useTranslation() + const { t } = useTranslation(); const [parent] = useAutoAnimate(); const onClick = useCallback(() => { @@ -24,7 +24,9 @@ export function EditButton(props: EditButtonProps) { > {props.editing ? ( - {t("media.stopEditing")} + + {t("media.stopEditing")} + ) : ( )} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index a5bf4399..bde7c11d 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -36,12 +36,13 @@ interface ErrorMessageProps { } export function ErrorMessage(props: ErrorMessageProps) { - const { t } = useTranslation() + const { t } = useTranslation(); return (
diff --git a/src/components/layout/Modal.tsx b/src/components/layout/Modal.tsx new file mode 100644 index 00000000..fb787259 --- /dev/null +++ b/src/components/layout/Modal.tsx @@ -0,0 +1,25 @@ +import { Overlay } from "@/components/Overlay"; +import { ReactNode } from "react"; +import { createPortal } from "react-dom"; + +interface Props { + show: boolean; + children?: ReactNode; +} + +export function ModalFrame(props: { children?: ReactNode }) { + return {props.children}; +} + +export function Modal(props: Props) { + if (!props.show) return null; + return createPortal({props.children}, document.body); +} + +export function ModalCard(props: { children?: ReactNode }) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index a67dba21..f4305eca 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -35,12 +35,14 @@ function MediaCardContent({ return (
{t("seasons.seasonAndEpisode", { season: series.season, - episode: series.episode + episode: series.episode, })}

@@ -62,12 +64,14 @@ function MediaCardContent({ {percentage !== undefined ? ( <>
@@ -83,8 +87,9 @@ function MediaCardContent({ ) : null}
diff --git a/src/index.tsx b/src/index.tsx index 3a8d7594..a6f88858 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -44,10 +44,9 @@ const LazyLoadedApp = React.lazy(async () => { function TheRouter(props: { children: ReactNode }) { const normalRouter = conf().NORMAL_ROUTER; - - if (normalRouter) - return - return + + if (normalRouter) return {props.children}; + return {props.children}; } ReactDOM.render( diff --git a/src/state/watched/migrations/v2.ts b/src/state/watched/migrations/v2.ts index 1d145968..0e9c52e8 100644 --- a/src/state/watched/migrations/v2.ts +++ b/src/state/watched/migrations/v2.ts @@ -49,11 +49,13 @@ async function getMetas( searchQuery: `${item.title} ${year}`, type: item.mediaType, }); - const relevantItem = data.find((res) => - yearsAreClose(Number(res.year), year) && compareTitle(res.title, item.title) + const relevantItem = data.find( + (res) => + yearsAreClose(Number(res.year), year) && + compareTitle(res.title, item.title) ); if (!relevantItem) { - console.error("No item found for migration: " + item.title); + console.error(`No item found for migration: ${item.title}`); return; } return { @@ -188,7 +190,10 @@ export async function migrateV2Videos(old: OldData) { }, progress: oldWatched.progress, percentage: oldWatched.percentage, - watchedAt: now + Number(oldWatched.seasonId) * 1000 + Number(oldWatched.episodeId), // There was no watchedAt in V2 + watchedAt: + now + + Number(oldWatched.seasonId) * 1000 + + Number(oldWatched.episodeId), // There was no watchedAt in V2 // JANK ALERT: Put watchedAt in the future to show last episode as most recently }; diff --git a/src/utils/titleMatch.ts b/src/utils/titleMatch.ts index cb69c790..dfdf3883 100644 --- a/src/utils/titleMatch.ts +++ b/src/utils/titleMatch.ts @@ -1,7 +1,11 @@ function normalizeTitle(title: string): string { - return title.trim().toLowerCase().replace(/[\'\"\:]/g, "").replace(/[^a-zA-Z0-9]+/g, "_"); + return title + .trim() + .toLowerCase() + .replace(/['":]/g, "") + .replace(/[^a-zA-Z0-9]+/g, "_"); } export function compareTitle(a: string, b: string): boolean { - return normalizeTitle(a) === normalizeTitle(b); + return normalizeTitle(a) === normalizeTitle(b); } diff --git a/src/video/components/actions/CaptionsSelectionAction.tsx b/src/video/components/actions/CaptionsSelectionAction.tsx index 96a13fdc..d6cc4328 100644 --- a/src/video/components/actions/CaptionsSelectionAction.tsx +++ b/src/video/components/actions/CaptionsSelectionAction.tsx @@ -11,7 +11,7 @@ interface Props { } export function CaptionsSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const { isMobile } = useIsMobile(); @@ -22,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) { controls.openPopout("captions")} icon={Icons.CAPTIONS} diff --git a/src/video/components/actions/PageTitleAction.tsx b/src/video/components/actions/PageTitleAction.tsx index 171a06f7..21a2bf23 100644 --- a/src/video/components/actions/PageTitleAction.tsx +++ b/src/video/components/actions/PageTitleAction.tsx @@ -9,7 +9,9 @@ export function PageTitleAction() { if (!meta) return null; - const title = isSeries ? `${meta.meta.title} - ${humanizedEpisodeId}` : meta.meta.title; + const title = isSeries + ? `${meta.meta.title} - ${humanizedEpisodeId}` + : meta.meta.title; return ( diff --git a/src/video/components/actions/SeriesSelectionAction.tsx b/src/video/components/actions/SeriesSelectionAction.tsx index abf2082e..2a6b2b35 100644 --- a/src/video/components/actions/SeriesSelectionAction.tsx +++ b/src/video/components/actions/SeriesSelectionAction.tsx @@ -13,7 +13,7 @@ interface Props { } export function SeriesSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); const videoInterface = useInterface(descriptor); diff --git a/src/video/components/actions/SourceSelectionAction.tsx b/src/video/components/actions/SourceSelectionAction.tsx index 3058a6d2..66784da8 100644 --- a/src/video/components/actions/SourceSelectionAction.tsx +++ b/src/video/components/actions/SourceSelectionAction.tsx @@ -11,7 +11,7 @@ interface Props { } export function SourceSelectionAction(props: Props) { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const videoInterface = useInterface(descriptor); const controls = useControls(descriptor); diff --git a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts index 7275cc07..88ab2e31 100644 --- a/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts +++ b/src/video/components/hooks/useCurrentSeriesEpisodeInfo.ts @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; export function useCurrentSeriesEpisodeInfo(descriptor: string) { const meta = useMeta(descriptor); - const {t} = useTranslation() + const { t } = useTranslation(); const currentSeasonInfo = useMemo(() => { return meta?.seasons?.find( @@ -24,10 +24,10 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) { ); if (!isSeries) return { isSeries: false }; - + const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", { season: currentSeasonInfo?.number, - episode: currentEpisodeInfo?.number + episode: currentEpisodeInfo?.number, }); return { diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index 995b86ac..e5ecdaeb 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -15,7 +15,7 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string { } export function CaptionSelectionPopout() { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); diff --git a/src/video/components/popouts/EpisodeSelectionPopout.tsx b/src/video/components/popouts/EpisodeSelectionPopout.tsx index de54b457..1f167731 100644 --- a/src/video/components/popouts/EpisodeSelectionPopout.tsx +++ b/src/video/components/popouts/EpisodeSelectionPopout.tsx @@ -11,14 +11,14 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { useWatchedContext } from "@/state/watched"; -import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useTranslation } from "react-i18next"; +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; export function EpisodeSelectionPopout() { const params = useParams<{ media: string; }>(); - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const meta = useMeta(descriptor); @@ -61,7 +61,7 @@ export function EpisodeSelectionPopout() { // race condition, jank solution but it works. setTimeout(() => { controls.setCurrentEpisode(seasonId, episodeId); - }, 100) + }, 100); }, [controls] ); @@ -141,15 +141,15 @@ export function EpisodeSelectionPopout() { > {currentSeasonInfo ? meta?.seasons?.map?.((season) => ( - setSeason(season.id)} - isOnDarkBackground - > - {season.title} - - )) + setSeason(season.id)} + isOnDarkBackground + > + {season.title} + + )) : "No season"} @@ -166,7 +166,7 @@ export function EpisodeSelectionPopout() { />

{t("videoPLayer.popouts.errors.loadingWentWrong", { - seasonTitle: currentSeasonInfo?.title?.toLowerCase() + seasonTitle: currentSeasonInfo?.title?.toLowerCase(), })}

@@ -175,29 +175,29 @@ export function EpisodeSelectionPopout() {
{currentSeasonEpisodes && currentSeasonInfo ? currentSeasonEpisodes.map((e) => ( - { - if (e.id === meta?.episode?.episodeId) - controls.closePopout(); - else setCurrent(currentSeasonInfo.id, e.id); - }} - percentageCompleted={ - watched.items.find( - (item) => - item.item?.series?.seasonId === - currentSeasonInfo.id && - item.item?.series?.episodeId === e.id - )?.percentage - } - > - {t("videoPlayer.popouts.episode", { - index: e.number, - title: e.title - })} - - )) + { + if (e.id === meta?.episode?.episodeId) + controls.closePopout(); + else setCurrent(currentSeasonInfo.id, e.id); + }} + percentageCompleted={ + watched.items.find( + (item) => + item.item?.series?.seasonId === + currentSeasonInfo.id && + item.item?.series?.episodeId === e.id + )?.percentage + } + > + {t("videoPlayer.popouts.episode", { + index: e.number, + title: e.title, + })} + + )) : "No episodes"}
)} diff --git a/src/video/components/popouts/SourceSelectionPopout.tsx b/src/video/components/popouts/SourceSelectionPopout.tsx index 81f783b5..70e78a1b 100644 --- a/src/video/components/popouts/SourceSelectionPopout.tsx +++ b/src/video/components/popouts/SourceSelectionPopout.tsx @@ -7,12 +7,15 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { MWStream } from "@/backend/helpers/streams"; -import { getEmbedScraperByType, getProviders } from "@/backend/helpers/register"; +import { + getEmbedScraperByType, + getProviders, +} from "@/backend/helpers/register"; import { runEmbedScraper, runProvider } from "@/backend/helpers/run"; import { MWProviderScrapeResult } from "@/backend/helpers/provider"; -import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; import { useTranslation } from "react-i18next"; import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; +import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; interface EmbedEntryProps { name: string; @@ -24,35 +27,39 @@ interface EmbedEntryProps { export function EmbedEntry(props: EmbedEntryProps) { const [scrapeEmbed, loading, error] = useLoading(async () => { const scraper = getEmbedScraperByType(props.type); - if (!scraper) throw new Error("Embed scraper not found") + if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { - progress: () => { }, // no progress tracking for inline scraping + progress: () => {}, // no progress tracking for inline scraping url: props.url, - }) + }); props.onSelect(stream); }); - return ( { - scrapeEmbed(); - }} - > - {props.name} - ) + return ( + { + scrapeEmbed(); + }} + > + {props.name} + + ); } export function SourceSelectionPopout() { - const { t } = useTranslation() + const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const meta = useMeta(descriptor); const providers = useMemo( () => - meta ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) : [], + meta + ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) + : [], [meta] ); @@ -71,7 +78,7 @@ export function SourceSelectionPopout() { if (!meta) throw new Error("need meta"); return runProvider(theProvider, { media: meta.meta, - progress: () => { }, + progress: () => {}, type: meta.meta.meta.type, episode: meta.episode?.episodeId as any, season: meta.episode?.seasonId as any, @@ -110,13 +117,13 @@ export function SourceSelectionPopout() { const realStream = v.stream; if (!realStream) { const embed = v?.embeds[0]; - if (!embed) throw new Error("Embed scraper not found") + if (!embed) throw new Error("Embed scraper not found"); const scraper = getEmbedScraperByType(embed.type); - if (!scraper) throw new Error("Embed scraper not found") + if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { - progress: () => { }, // no progress tracking for inline scraping + progress: () => {}, // no progress tracking for inline scraping url: embed.url, - }) + }); selectSource(stream); return; } @@ -142,28 +149,30 @@ export function SourceSelectionPopout() { const embeds = scrapeResult?.embeds || []; // Count embed types to determine if it should show a number behind the name - const embedsPerType: Record = {} + const embedsPerType: Record = + {}; for (const embed of embeds) { if (!embed.type) continue; if (!embedsPerType[embed.type]) embedsPerType[embed.type] = []; embedsPerType[embed.type].push({ ...embed, - displayName: embed.type - }) + displayName: embed.type, + }); } - const embedsRes = Object.entries(embedsPerType).flatMap(([type, entries]) => { - if (entries.length > 1) return entries.map((embed, i) => ({ - ...embed, - displayName: `${embed.type} ${i + 1}` - })) + const embedsRes = Object.entries(embedsPerType).flatMap(([_, entries]) => { + if (entries.length > 1) + return entries.map((embed, i) => ({ + ...embed, + displayName: `${embed.type} ${i + 1}`, + })); return entries; - }) + }); - console.log(embedsRes) + console.log(embedsRes); return embedsRes; - }, [scrapeResult?.embeds]) + }, [scrapeResult?.embeds]); return ( <> @@ -234,27 +243,31 @@ export function SourceSelectionPopout() { Native source ) : null} - {(visibleEmbeds?.length || 0) > 0 ? visibleEmbeds?.map((v) => ( - { - selectSource(stream); - }} - /> - )) : (
-
- 0 ? ( + visibleEmbeds?.map((v) => ( + { + selectSource(stream); + }} /> -

- {t("videoPlayer.popouts.noEmbeds")} -

+ )) + ) : ( +
+
+ +

+ {t("videoPlayer.popouts.noEmbeds")} +

+
-
)} + )} )} diff --git a/src/video/state/types.ts b/src/video/state/types.ts index 5686af1d..6b04a55a 100644 --- a/src/video/state/types.ts +++ b/src/video/state/types.ts @@ -4,7 +4,6 @@ import { MWStreamType, } from "@/backend/helpers/streams"; import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaMeta } from "@/backend/metadata/types"; import Hls from "hls.js"; import { VideoPlayerStateProvider } from "./providers/providerTypes"; diff --git a/src/views/media/MediaErrorView.tsx b/src/views/media/MediaErrorView.tsx index 140a3dbe..5dc634f4 100644 --- a/src/views/media/MediaErrorView.tsx +++ b/src/views/media/MediaErrorView.tsx @@ -1,14 +1,11 @@ -import { MWMediaMeta } from "@/backend/metadata/types"; import { ErrorMessage } from "@/components/layout/ErrorBoundary"; -import { Link } from "@/components/text/Link"; import { useGoBack } from "@/hooks/useGoBack"; -import { conf } from "@/setup/config"; import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader"; import { Helmet } from "react-helmet"; -import { Trans, useTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; export function MediaFetchErrorView() { - const { t } = useTranslation() + const { t } = useTranslation(); const goBack = useGoBack(); return ( @@ -20,9 +17,7 @@ export function MediaFetchErrorView() {
-

- {t("media.errors.mediaFailed")} -

+

{t("media.errors.mediaFailed")}

); diff --git a/src/views/media/MediaView.tsx b/src/views/media/MediaView.tsx index 242c73ea..7ab7ebee 100644 --- a/src/views/media/MediaView.tsx +++ b/src/views/media/MediaView.tsx @@ -19,13 +19,13 @@ import { ProgressListenerController } from "@/video/components/controllers/Progr import { VideoPlayerMeta } from "@/video/state/types"; import { SeriesController } from "@/video/components/controllers/SeriesController"; import { useWatchedItem } from "@/state/watched"; +import { useTranslation } from "react-i18next"; import { MediaFetchErrorView } from "./MediaErrorView"; import { MediaScrapeLog } from "./MediaScrapeLog"; import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView"; -import { useTranslation } from "react-i18next"; function MediaViewLoading(props: { onGoBack(): void }) { - const { t } = useTranslation() + const { t } = useTranslation(); return (
@@ -37,7 +37,9 @@ function MediaViewLoading(props: { onGoBack(): void }) {
-

{t("videoPlayer.findingBestVideo")}

+

+ {t("videoPlayer.findingBestVideo")} +

); @@ -51,7 +53,7 @@ interface MediaViewScrapingProps { } function MediaViewScraping(props: MediaViewScrapingProps) { const { eventLog, stream, pending } = useScrape(props.meta, props.selected); - const { t } = useTranslation() + const { t } = useTranslation(); useEffect(() => { if (stream) { @@ -78,14 +80,13 @@ function MediaViewScraping(props: MediaViewScrapingProps) { ) : ( <> -

- {t("videoPlayer.noVideos")} -

+

{t("videoPlayer.noVideos")}

)}
diff --git a/src/views/notfound/NotFoundView.tsx b/src/views/notfound/NotFoundView.tsx index a1dfcab0..7061e039 100644 --- a/src/views/notfound/NotFoundView.tsx +++ b/src/views/notfound/NotFoundView.tsx @@ -13,7 +13,7 @@ export function NotFoundWrapper(props: { children?: ReactNode; video?: boolean; }) { - const { t } = useTranslation() + const { t } = useTranslation(); const goBack = useGoBack(); return ( diff --git a/src/views/other/v2Migration.tsx b/src/views/other/v2Migration.tsx index f97c27f7..483b6f77 100644 --- a/src/views/other/v2Migration.tsx +++ b/src/views/other/v2Migration.tsx @@ -2,60 +2,66 @@ import { useEffect, useState } from "react"; import pako from "pako"; function fromBinary(str: string): Uint8Array { - let result = new Uint8Array(str.length); - [...str].forEach((char, i) => { - result[i] = char.charCodeAt(0); - }); - return result; + const result = new Uint8Array(str.length); + [...str].forEach((char, i) => { + result[i] = char.charCodeAt(0); + }); + return result; } - export function V2MigrationView() { - const [done, setDone] = useState(false); - useEffect(() => { - const params = new URLSearchParams(window.location.search ?? ""); - if (!params.has("m-time") || !params.has("m-data")) { - // migration params missing, just redirect - setDone(true); - return; - } + const [done, setDone] = useState(false); + useEffect(() => { + const params = new URLSearchParams(window.location.search ?? ""); + if (!params.has("m-time") || !params.has("m-data")) { + // migration params missing, just redirect + setDone(true); + return; + } - const data = JSON.parse(pako.inflate(fromBinary(atob(params.get("m-data") as string)), { to: "string" })); - const timeOfMigration = new Date(params.get("m-time") as string); + const data = JSON.parse( + pako.inflate(fromBinary(atob(params.get("m-data") as string)), { + to: "string", + }) + ); + const timeOfMigration = new Date(params.get("m-time") as string); - const savedTime = localStorage.getItem("mw-migration-date"); - if (savedTime) { - if (new Date(savedTime) >= timeOfMigration) { - // has already migrated this or something newer, skip - setDone(true); - return; - } - } - - // restore migration data - if (data.bookmarks) - localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks)) - if (data.videoProgress) - localStorage.setItem("video-progress", JSON.stringify(data.videoProgress)) - localStorage.setItem("mw-migration-date", timeOfMigration.toISOString()) - - // finished + const savedTime = localStorage.getItem("mw-migration-date"); + if (savedTime) { + if (new Date(savedTime) >= timeOfMigration) { + // has already migrated this or something newer, skip setDone(true); - }, []) - - // redirect when done - useEffect(() => { - if (!done) return; - const newUrl = new URL(window.location.href); + return; + } + } - const newParams = [] as string[]; - newUrl.searchParams.forEach((_, key)=>newParams.push(key)); - newParams.forEach(v => newUrl.searchParams.delete(v)) + // restore migration data + if (data.bookmarks) + localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks)); + if (data.videoProgress) + localStorage.setItem( + "video-progress", + JSON.stringify(data.videoProgress) + ); + localStorage.setItem("mw-migration-date", timeOfMigration.toISOString()); - newUrl.hash = ""; + // finished + setDone(true); + }, []); - window.location.href = newUrl.toString(); - }, [done]) + // redirect when done + useEffect(() => { + if (!done) return; + const newUrl = new URL(window.location.href); - return null; + const newParams = [] as string[]; + newUrl.searchParams.forEach((_, key) => newParams.push(key)); + newParams.forEach((v) => newUrl.searchParams.delete(v)); + + newUrl.hash = ""; + + window.location.href = newUrl.toString(); + }, [done]); + + return null; } diff --git a/src/views/search/SearchLoadingView.tsx b/src/views/search/SearchLoadingView.tsx index 1ae0d89c..307ed428 100644 --- a/src/views/search/SearchLoadingView.tsx +++ b/src/views/search/SearchLoadingView.tsx @@ -1,17 +1,18 @@ import { useTranslation } from "react-i18next"; import { Loading } from "@/components/layout/Loading"; -import { MWQuery } from "@/backend/metadata/types"; import { useSearchQuery } from "@/hooks/useSearchQuery"; export function SearchLoadingView() { const { t } = useTranslation(); - const [query] = useSearchQuery() + const [query] = useSearchQuery(); return ( - <> - - + ); } diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx index b65892f8..4201d954 100644 --- a/src/views/search/SearchView.tsx +++ b/src/views/search/SearchView.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import Sticky from "react-stickynode"; import { useTranslation } from "react-i18next"; import { Navigation } from "@/components/layout/Navigation"; diff --git a/vite.config.ts b/vite.config.ts index 72f5bf13..3946b798 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import loadVersion from "vite-plugin-package-version"; -import checker from 'vite-plugin-checker' +import checker from "vite-plugin-checker"; import path from "path"; export default defineConfig({ @@ -10,7 +10,14 @@ export default defineConfig({ loadVersion(), checker({ typescript: true, // check typescript build errors in dev server - }) + eslint: { + // check lint errors in dev server + lintCommand: "eslint --ext .tsx,.ts src", + dev: { + logLevel: ["error"], + }, + }, + }), ], resolve: { alias: {