mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 20:01:53 +01:00
linting
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
c90d59ef93
commit
b3db58012f
@ -50,6 +50,7 @@ module.exports = {
|
|||||||
"no-await-in-loop": "off",
|
"no-await-in-loop": "off",
|
||||||
"no-nested-ternary": "off",
|
"no-nested-ternary": "off",
|
||||||
"prefer-destructuring": "off",
|
"prefer-destructuring": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
"error",
|
"error",
|
||||||
{ extensions: [".js", ".tsx", ".jsx"] }
|
{ extensions: [".js", ".tsx", ".jsx"] }
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { MWEmbedType } from "@/backend/helpers/embed";
|
import { MWEmbedType } from "@/backend/helpers/embed";
|
||||||
import { MWMediaType } from "../metadata/types";
|
|
||||||
import { registerEmbedScraper } from "@/backend/helpers/register";
|
import { registerEmbedScraper } from "@/backend/helpers/register";
|
||||||
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
|
||||||
registerEmbedScraper({
|
registerEmbedScraper({
|
||||||
id: "playm4u",
|
id: "playm4u",
|
||||||
displayName: "playm4u",
|
displayName: "playm4u",
|
||||||
for: MWEmbedType.PLAYM4U,
|
for: MWEmbedType.PLAYM4U,
|
||||||
rank: 0,
|
rank: 0,
|
||||||
async getStream(ctx) {
|
async getStream() {
|
||||||
// throw new Error("Oh well 2")
|
// throw new Error("Oh well 2")
|
||||||
return {
|
return {
|
||||||
streamUrl: '',
|
streamUrl: "",
|
||||||
quality: MWStreamQuality.Q1080P,
|
quality: MWStreamQuality.Q1080P,
|
||||||
captions: [],
|
captions: [],
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
@ -1,60 +1,65 @@
|
|||||||
import { MWEmbedType } from "@/backend/helpers/embed";
|
import { MWEmbedType } from "@/backend/helpers/embed";
|
||||||
import { MWMediaType } from "../metadata/types";
|
|
||||||
import { registerEmbedScraper } from "@/backend/helpers/register";
|
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";
|
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
|
|
||||||
const HOST = 'streamm4u.club';
|
const HOST = "streamm4u.club";
|
||||||
const URL_BASE = `https://${HOST}`;
|
const URL_BASE = `https://${HOST}`;
|
||||||
const URL_API = `${URL_BASE}/api`;
|
const URL_API = `${URL_BASE}/api`;
|
||||||
const URL_API_SOURCE = `${URL_API}/source`;
|
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<any>(`${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
|
// TODO check out 403 / 404 on successfully returned video stream URLs
|
||||||
registerEmbedScraper({
|
registerEmbedScraper({
|
||||||
id: "streamm4u",
|
id: "streamm4u",
|
||||||
displayName: "streamm4u",
|
displayName: "streamm4u",
|
||||||
for: MWEmbedType.STREAMM4U,
|
for: MWEmbedType.STREAMM4U,
|
||||||
rank: 100,
|
rank: 100,
|
||||||
async getStream({ progress, url }) {
|
async getStream({ progress, url }) {
|
||||||
|
// const scrapingThreads = [];
|
||||||
|
// const streams = [];
|
||||||
|
|
||||||
const scrapingThreads = [];
|
const sources = (await scrape(url)).sort(
|
||||||
let streams = [];
|
(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", "")));
|
if (!preferredSource) throw new Error("No source found");
|
||||||
let preferredSourceIndex = 0;
|
|
||||||
let preferredSource = sources[0];
|
|
||||||
|
|
||||||
if (!preferredSource) throw new Error("No source found")
|
progress(100);
|
||||||
|
|
||||||
progress(100)
|
return preferredSource;
|
||||||
|
},
|
||||||
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<any>(`${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;
|
|
||||||
}
|
|
||||||
|
@ -3,7 +3,7 @@ import { MWStream } from "./streams";
|
|||||||
export enum MWEmbedType {
|
export enum MWEmbedType {
|
||||||
M4UFREE = "m4ufree",
|
M4UFREE = "m4ufree",
|
||||||
STREAMM4U = "streamm4u",
|
STREAMM4U = "streamm4u",
|
||||||
PLAYM4U = "playm4u"
|
PLAYM4U = "playm4u",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MWEmbed = {
|
export type MWEmbed = {
|
||||||
|
@ -25,15 +25,15 @@ type MWProviderRunContextBase = {
|
|||||||
};
|
};
|
||||||
type MWProviderRunContextTypeSpecific =
|
type MWProviderRunContextTypeSpecific =
|
||||||
| {
|
| {
|
||||||
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||||
episode: undefined;
|
episode: undefined;
|
||||||
season: undefined;
|
season: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: MWMediaType.SERIES;
|
type: MWMediaType.SERIES;
|
||||||
episode: string;
|
episode: string;
|
||||||
season: string;
|
season: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MWProviderRunContext = MWProviderRunContextBase &
|
export type MWProviderRunContext = MWProviderRunContextBase &
|
||||||
MWProviderRunContextTypeSpecific;
|
MWProviderRunContextTypeSpecific;
|
||||||
@ -50,7 +50,7 @@ async function findBestEmbedStream(
|
|||||||
embedNum += 1;
|
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: " + embed.type);
|
if (!scraper) throw new Error(`Type for embed not found: ${embed.type}`);
|
||||||
|
|
||||||
const eventId = [providerId, scraper.id, embedNum].join("|");
|
const eventId = [providerId, scraper.id, embedNum].join("|");
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import "./providers/netfilm";
|
|||||||
import "./providers/m4ufree";
|
import "./providers/m4ufree";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
import "./embeds/streamm4u"
|
import "./embeds/streamm4u";
|
||||||
import "./embeds/playm4u"
|
import "./embeds/playm4u";
|
||||||
|
|
||||||
initializeScraperStore();
|
initializeScraperStore();
|
||||||
|
@ -21,7 +21,10 @@ registerProvider({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const foundItem = searchResults.results.find((v: any) => {
|
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");
|
if (!foundItem) throw new Error("No watchable item found");
|
||||||
const flixId = foundItem.id;
|
const flixId = foundItem.id;
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
||||||
import { MWEmbedType } from "../helpers/embed";
|
|
||||||
import { proxiedFetch } from "../helpers/fetch";
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
import { registerProvider } from "../helpers/register";
|
import { registerProvider } from "../helpers/register";
|
||||||
import { MWMediaType } from "../metadata/types";
|
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_BASE = `https://${HOST}`;
|
||||||
const URL_SEARCH = `${URL_BASE}/search`;
|
const URL_SEARCH = `${URL_BASE}/search`;
|
||||||
const URL_AJAX = `${URL_BASE}/ajax`;
|
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*)/;
|
const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/;
|
||||||
|
|
||||||
function toDom(html: string) {
|
function toDom(html: string) {
|
||||||
return new DOMParser().parseFromString(html, "text/html")
|
return new DOMParser().parseFromString(html, "text/html");
|
||||||
}
|
}
|
||||||
|
|
||||||
registerProvider({
|
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.
|
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],
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
|
|
||||||
async scrape({ media, progress, type, episode: episodeId, season: seasonId }) {
|
async scrape({ media, type, episode: episodeId, season: seasonId }) {
|
||||||
const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1
|
const season =
|
||||||
const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined
|
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[] = [];
|
const embeds: MWEmbed[] = [];
|
||||||
|
|
||||||
@ -43,39 +46,49 @@ registerProvider({
|
|||||||
responseType: "text" as any,
|
responseType: "text" as any,
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let responseText = await proxiedFetch<string>(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`);
|
const responseText = await proxiedFetch<string>(
|
||||||
|
`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`
|
||||||
|
);
|
||||||
let dom = toDom(responseText);
|
let dom = toDom(responseText);
|
||||||
|
|
||||||
const searchResults = [...dom.querySelectorAll('.item')].map(element => {
|
const searchResults = [...dom.querySelectorAll(".item")]
|
||||||
const tooltipText = element.querySelector('.tiptitle p')?.innerHTML;
|
.map((element) => {
|
||||||
if (!tooltipText) return;
|
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]) {
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = regexResult[1];
|
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 year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year
|
||||||
const a = element.querySelector('a');
|
const a = element.querySelector("a");
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
const href = a.href;
|
const href = a.href;
|
||||||
|
|
||||||
regexResult = REGEX_TYPE.exec(href);
|
regexResult = REGEX_TYPE.exec(href);
|
||||||
|
|
||||||
if (!regexResult || !regexResult[1]) {
|
if (!regexResult || !regexResult[1]) {
|
||||||
return;
|
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 };
|
return { type: scraperDeterminedType, title, year, href };
|
||||||
}).filter(item => item);
|
})
|
||||||
|
.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) {
|
if (!mediaInResults) {
|
||||||
// * Nothing found
|
// * Nothing found
|
||||||
@ -84,109 +97,124 @@ registerProvider({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let cookies: string | null = '';
|
let cookies: string | null = "";
|
||||||
const responseTextFromMedia = await proxiedFetch<string>(mediaInResults.href, {
|
const responseTextFromMedia = await proxiedFetch<string>(
|
||||||
onResponse(context) {
|
mediaInResults.href,
|
||||||
cookies = context.response.headers.get('X-Set-Cookie')
|
{
|
||||||
},
|
onResponse(context) {
|
||||||
});
|
cookies = context.response.headers.get("X-Set-Cookie");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
dom = toDom(responseTextFromMedia);
|
dom = toDom(responseTextFromMedia);
|
||||||
|
|
||||||
let regexResult = REGEX_COOKIES.exec(cookies);
|
let regexResult = REGEX_COOKIES.exec(cookies);
|
||||||
|
|
||||||
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
// * DO SOMETHING?
|
// * 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 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 (!token) return { embeds };
|
||||||
|
|
||||||
if (type === MWMediaType.SERIES) {
|
if (type === MWMediaType.SERIES) {
|
||||||
// * Get the season/episode data
|
// * Get the season/episode data
|
||||||
const episodes = [...dom.querySelectorAll('.episode')].map(element => {
|
const episodes = [...dom.querySelectorAll(".episode")]
|
||||||
regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML);
|
.map((element) => {
|
||||||
|
regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML);
|
||||||
|
|
||||||
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const episode = Number(regexResult[1]);
|
const newEpisode = Number(regexResult[1]);
|
||||||
const season = Number(regexResult[2]);
|
const newSeason = Number(regexResult[2]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: element.getAttribute('idepisode'),
|
id: element.getAttribute("idepisode"),
|
||||||
episode: episode,
|
episode: newEpisode,
|
||||||
season: season
|
season: newSeason,
|
||||||
};
|
};
|
||||||
}).filter(item => item);
|
})
|
||||||
|
.filter((item) => item);
|
||||||
|
|
||||||
const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season);
|
const ep = episodes.find(
|
||||||
if (!ep) return { embeds }
|
(newEp) => newEp && newEp.episode === episode && newEp.season === season
|
||||||
|
);
|
||||||
|
if (!ep) return { embeds };
|
||||||
|
|
||||||
const form = `idepisode=${ep.id}&_token=${token}`;
|
const form = `idepisode=${ep.id}&_token=${token}`;
|
||||||
|
|
||||||
let response = await proxiedFetch<string>(URL_AJAX_TV, {
|
const response = await proxiedFetch<string>(URL_AJAX_TV, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': '*/*',
|
Accept: "*/*",
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
'Accept-Language': "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
"Sec-CH-UA":
|
||||||
'Sec-CH-UA-Mobile': '?0',
|
'"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
||||||
'Sec-CH-UA-Platform': '"Linux"',
|
"Sec-CH-UA-Mobile": "?0",
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
"Sec-CH-UA-Platform": '"Linux"',
|
||||||
'Sec-Fetch-Mode': 'cors',
|
"Sec-Fetch-Site": "same-origin",
|
||||||
'Sec-Fetch-Dest': 'empty',
|
"Sec-Fetch-Mode": "cors",
|
||||||
'X-Cookie': cookieHeader,
|
"Sec-Fetch-Dest": "empty",
|
||||||
'X-Origin': URL_BASE,
|
"X-Cookie": cookieHeader,
|
||||||
'X-Referer': mediaInResults.href
|
"X-Origin": URL_BASE,
|
||||||
|
"X-Referer": mediaInResults.href,
|
||||||
},
|
},
|
||||||
body: form
|
body: form,
|
||||||
});
|
});
|
||||||
|
|
||||||
dom = toDom(response);
|
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) {
|
for (const server of servers) {
|
||||||
const form = `m4u=${server}&_token=${token}`;
|
const form = `m4u=${server}&_token=${token}`;
|
||||||
|
|
||||||
const response = await proxiedFetch<string>(URL_AJAX, {
|
const response = await proxiedFetch<string>(URL_AJAX, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': '*/*',
|
Accept: "*/*",
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
'Accept-Language': "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
"Sec-CH-UA":
|
||||||
'Sec-CH-UA-Mobile': '?0',
|
'"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
||||||
'Sec-CH-UA-Platform': '"Linux"',
|
"Sec-CH-UA-Mobile": "?0",
|
||||||
'Sec-Fetch-Site': 'same-origin',
|
"Sec-CH-UA-Platform": '"Linux"',
|
||||||
'Sec-Fetch-Mode': 'cors',
|
"Sec-Fetch-Site": "same-origin",
|
||||||
'Sec-Fetch-Dest': 'empty',
|
"Sec-Fetch-Mode": "cors",
|
||||||
'X-Cookie': cookieHeader,
|
"Sec-Fetch-Dest": "empty",
|
||||||
'X-Origin': URL_BASE,
|
"X-Cookie": cookieHeader,
|
||||||
'X-Referer': mediaInResults.href
|
"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) => {
|
const getEmbedType = (url: string) => {
|
||||||
if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U
|
if (url.startsWith("https://streamm4u.club"))
|
||||||
if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U
|
return MWEmbedType.STREAMM4U;
|
||||||
|
if (url.startsWith("https://play.playm4u.xyz"))
|
||||||
|
return MWEmbedType.PLAYM4U;
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (!link) continue;
|
if (!link) continue;
|
||||||
|
|
||||||
@ -194,15 +222,14 @@ registerProvider({
|
|||||||
if (embedType) {
|
if (embedType) {
|
||||||
embeds.push({
|
embeds.push({
|
||||||
url: link,
|
url: link,
|
||||||
type: embedType
|
type: embedType,
|
||||||
})
|
});
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(embeds);
|
console.log(embeds);
|
||||||
return {
|
return {
|
||||||
embeds,
|
embeds,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
@ -131,7 +131,8 @@ registerProvider({
|
|||||||
|
|
||||||
const superstreamEntry = searchRes.find(
|
const superstreamEntry = searchRes.find(
|
||||||
(res: any) =>
|
(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");
|
if (!superstreamEntry) throw new Error("No entry found on SuperStream");
|
||||||
|
14
src/components/Overlay.tsx
Normal file
14
src/components/Overlay.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
export function Overlay(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<body data-no-scroll />
|
||||||
|
</Helmet>
|
||||||
|
<div className="fixed inset-0 z-[99999] flex h-full w-full items-center justify-center bg-[rgba(8,6,18,0.85)]">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -67,7 +67,7 @@ export function SearchBarInput(props: SearchBarProps) {
|
|||||||
id: MWMediaType.SERIES,
|
id: MWMediaType.SERIES,
|
||||||
name: t("searchBar.series"),
|
name: t("searchBar.series"),
|
||||||
icon: Icons.CLAPPER_BOARD,
|
icon: Icons.CLAPPER_BOARD,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
onClick={() => setDropdownOpen((old) => !old)}
|
onClick={() => setDropdownOpen((old) => !old)}
|
||||||
>
|
>
|
||||||
|
@ -10,7 +10,7 @@ export interface EditButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EditButton(props: EditButtonProps) {
|
export function EditButton(props: EditButtonProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
const [parent] = useAutoAnimate<HTMLSpanElement>();
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
@ -24,7 +24,9 @@ export function EditButton(props: EditButtonProps) {
|
|||||||
>
|
>
|
||||||
<span ref={parent}>
|
<span ref={parent}>
|
||||||
{props.editing ? (
|
{props.editing ? (
|
||||||
<span className="mx-4 whitespace-nowrap">{t("media.stopEditing")}</span>
|
<span className="mx-4 whitespace-nowrap">
|
||||||
|
{t("media.stopEditing")}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Icon icon={Icons.EDIT} />
|
<Icon icon={Icons.EDIT} />
|
||||||
)}
|
)}
|
||||||
|
@ -36,12 +36,13 @@ interface ErrorMessageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorMessage(props: ErrorMessageProps) {
|
export function ErrorMessage(props: ErrorMessageProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${props.localSize ? "h-full" : "min-h-screen"
|
className={`${
|
||||||
} flex w-full flex-col items-center justify-center px-4 py-12`}
|
props.localSize ? "h-full" : "min-h-screen"
|
||||||
|
} flex w-full flex-col items-center justify-center px-4 py-12`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-start text-center">
|
<div className="flex flex-col items-center justify-start text-center">
|
||||||
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||||
|
25
src/components/layout/Modal.tsx
Normal file
25
src/components/layout/Modal.tsx
Normal file
@ -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 <Overlay>{props.children}</Overlay>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Modal(props: Props) {
|
||||||
|
if (!props.show) return null;
|
||||||
|
return createPortal(<ModalFrame>{props.children}</ModalFrame>, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalCard(props: { children?: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-4/5 max-w-[645px] overflow-hidden rounded-lg bg-denim-200 px-10 py-16">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -35,12 +35,14 @@ function MediaCardContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${canLink ? "hover:bg-opacity-100" : ""
|
className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${
|
||||||
}`}
|
canLink ? "hover:bg-opacity-100" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<article
|
<article
|
||||||
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${canLink ? "group-hover:scale-95" : ""
|
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${
|
||||||
}`}
|
canLink ? "group-hover:scale-95" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center transition-[border-radius] duration-100 group-hover:rounded-lg"
|
className="relative mb-4 aspect-[2/3] w-full overflow-hidden rounded-xl bg-denim-500 bg-cover bg-center transition-[border-radius] duration-100 group-hover:rounded-lg"
|
||||||
@ -53,7 +55,7 @@ function MediaCardContent({
|
|||||||
<p className="text-center text-xs font-bold text-slate-400 transition-colors group-hover:text-white">
|
<p className="text-center text-xs font-bold text-slate-400 transition-colors group-hover:text-white">
|
||||||
{t("seasons.seasonAndEpisode", {
|
{t("seasons.seasonAndEpisode", {
|
||||||
season: series.season,
|
season: series.season,
|
||||||
episode: series.episode
|
episode: series.episode,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -62,12 +64,14 @@ function MediaCardContent({
|
|||||||
{percentage !== undefined ? (
|
{percentage !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${canLink ? "group-hover:from-denim-100" : ""
|
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${
|
||||||
}`}
|
canLink ? "group-hover:from-denim-100" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${canLink ? "group-hover:from-denim-100" : ""
|
className={`absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-denim-300 to-transparent transition-colors ${
|
||||||
}`}
|
canLink ? "group-hover:from-denim-100" : ""
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-x-0 bottom-0 p-3">
|
<div className="absolute inset-x-0 bottom-0 p-3">
|
||||||
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
|
<div className="relative h-1 overflow-hidden rounded-full bg-denim-600">
|
||||||
@ -83,8 +87,9 @@ function MediaCardContent({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 flex items-center justify-center bg-denim-200 bg-opacity-80 transition-opacity duration-200 ${closable ? "opacity-100" : "pointer-events-none opacity-0"
|
className={`absolute inset-0 flex items-center justify-center bg-denim-200 bg-opacity-80 transition-opacity duration-200 ${
|
||||||
}`}
|
closable ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<IconPatch
|
<IconPatch
|
||||||
clickable
|
clickable
|
||||||
@ -99,10 +104,7 @@ function MediaCardContent({
|
|||||||
</h1>
|
</h1>
|
||||||
<DotList
|
<DotList
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
content={[
|
content={[t(`media.${media.type}`), media.year]}
|
||||||
t(`media.${media.type}`),
|
|
||||||
media.year,
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,10 +44,9 @@ const LazyLoadedApp = React.lazy(async () => {
|
|||||||
|
|
||||||
function TheRouter(props: { children: ReactNode }) {
|
function TheRouter(props: { children: ReactNode }) {
|
||||||
const normalRouter = conf().NORMAL_ROUTER;
|
const normalRouter = conf().NORMAL_ROUTER;
|
||||||
|
|
||||||
if (normalRouter)
|
if (normalRouter) return <BrowserRouter>{props.children}</BrowserRouter>;
|
||||||
return <BrowserRouter children={props.children} />
|
return <HashRouter>{props.children}</HashRouter>;
|
||||||
return <HashRouter children={props.children} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
@ -49,11 +49,13 @@ async function getMetas(
|
|||||||
searchQuery: `${item.title} ${year}`,
|
searchQuery: `${item.title} ${year}`,
|
||||||
type: item.mediaType,
|
type: item.mediaType,
|
||||||
});
|
});
|
||||||
const relevantItem = data.find((res) =>
|
const relevantItem = data.find(
|
||||||
yearsAreClose(Number(res.year), year) && compareTitle(res.title, item.title)
|
(res) =>
|
||||||
|
yearsAreClose(Number(res.year), year) &&
|
||||||
|
compareTitle(res.title, item.title)
|
||||||
);
|
);
|
||||||
if (!relevantItem) {
|
if (!relevantItem) {
|
||||||
console.error("No item found for migration: " + item.title);
|
console.error(`No item found for migration: ${item.title}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -188,7 +190,10 @@ export async function migrateV2Videos(old: OldData) {
|
|||||||
},
|
},
|
||||||
progress: oldWatched.progress,
|
progress: oldWatched.progress,
|
||||||
percentage: oldWatched.percentage,
|
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
|
// JANK ALERT: Put watchedAt in the future to show last episode as most recently
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
function normalizeTitle(title: string): string {
|
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 {
|
export function compareTitle(a: string, b: string): boolean {
|
||||||
return normalizeTitle(a) === normalizeTitle(b);
|
return normalizeTitle(a) === normalizeTitle(b);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CaptionsSelectionAction(props: Props) {
|
export function CaptionsSelectionAction(props: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
@ -22,7 +22,7 @@ export function CaptionsSelectionAction(props: Props) {
|
|||||||
<PopoutAnchor for="captions">
|
<PopoutAnchor for="captions">
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
className={props.className}
|
className={props.className}
|
||||||
text={isMobile ? t("videoPlayer.buttons.captions") as string : ""}
|
text={isMobile ? (t("videoPlayer.buttons.captions") as string) : ""}
|
||||||
wide={isMobile}
|
wide={isMobile}
|
||||||
onClick={() => controls.openPopout("captions")}
|
onClick={() => controls.openPopout("captions")}
|
||||||
icon={Icons.CAPTIONS}
|
icon={Icons.CAPTIONS}
|
||||||
|
@ -9,7 +9,9 @@ export function PageTitleAction() {
|
|||||||
|
|
||||||
if (!meta) return null;
|
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 (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -13,7 +13,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SeriesSelectionAction(props: Props) {
|
export function SeriesSelectionAction(props: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
|
@ -11,7 +11,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SourceSelectionAction(props: Props) {
|
export function SourceSelectionAction(props: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const videoInterface = useInterface(descriptor);
|
const videoInterface = useInterface(descriptor);
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
|
@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const {t} = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const currentSeasonInfo = useMemo(() => {
|
const currentSeasonInfo = useMemo(() => {
|
||||||
return meta?.seasons?.find(
|
return meta?.seasons?.find(
|
||||||
@ -24,10 +24,10 @@ export function useCurrentSeriesEpisodeInfo(descriptor: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isSeries) return { isSeries: false };
|
if (!isSeries) return { isSeries: false };
|
||||||
|
|
||||||
const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", {
|
const humanizedEpisodeId = t("videoPlayer.seasonAndEpisode", {
|
||||||
season: currentSeasonInfo?.number,
|
season: currentSeasonInfo?.number,
|
||||||
episode: currentEpisodeInfo?.number
|
episode: currentEpisodeInfo?.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -15,7 +15,7 @@ function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CaptionSelectionPopout() {
|
export function CaptionSelectionPopout() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
|
@ -11,14 +11,14 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useWatchedContext } from "@/state/watched";
|
import { useWatchedContext } from "@/state/watched";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
export function EpisodeSelectionPopout() {
|
export function EpisodeSelectionPopout() {
|
||||||
const params = useParams<{
|
const params = useParams<{
|
||||||
media: string;
|
media: string;
|
||||||
}>();
|
}>();
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
@ -61,7 +61,7 @@ export function EpisodeSelectionPopout() {
|
|||||||
// race condition, jank solution but it works.
|
// race condition, jank solution but it works.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
controls.setCurrentEpisode(seasonId, episodeId);
|
controls.setCurrentEpisode(seasonId, episodeId);
|
||||||
}, 100)
|
}, 100);
|
||||||
},
|
},
|
||||||
[controls]
|
[controls]
|
||||||
);
|
);
|
||||||
@ -141,15 +141,15 @@ export function EpisodeSelectionPopout() {
|
|||||||
>
|
>
|
||||||
{currentSeasonInfo
|
{currentSeasonInfo
|
||||||
? meta?.seasons?.map?.((season) => (
|
? meta?.seasons?.map?.((season) => (
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={season.id}
|
key={season.id}
|
||||||
active={meta?.episode?.seasonId === season.id}
|
active={meta?.episode?.seasonId === season.id}
|
||||||
onClick={() => setSeason(season.id)}
|
onClick={() => setSeason(season.id)}
|
||||||
isOnDarkBackground
|
isOnDarkBackground
|
||||||
>
|
>
|
||||||
{season.title}
|
{season.title}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
))
|
))
|
||||||
: "No season"}
|
: "No season"}
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
<PopoutSection className="relative h-full overflow-y-auto">
|
<PopoutSection className="relative h-full overflow-y-auto">
|
||||||
@ -166,7 +166,7 @@ export function EpisodeSelectionPopout() {
|
|||||||
/>
|
/>
|
||||||
<p className="mt-6 w-full text-center">
|
<p className="mt-6 w-full text-center">
|
||||||
{t("videoPLayer.popouts.errors.loadingWentWrong", {
|
{t("videoPLayer.popouts.errors.loadingWentWrong", {
|
||||||
seasonTitle: currentSeasonInfo?.title?.toLowerCase()
|
seasonTitle: currentSeasonInfo?.title?.toLowerCase(),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -175,29 +175,29 @@ export function EpisodeSelectionPopout() {
|
|||||||
<div>
|
<div>
|
||||||
{currentSeasonEpisodes && currentSeasonInfo
|
{currentSeasonEpisodes && currentSeasonInfo
|
||||||
? currentSeasonEpisodes.map((e) => (
|
? currentSeasonEpisodes.map((e) => (
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={e.id}
|
key={e.id}
|
||||||
active={e.id === meta?.episode?.episodeId}
|
active={e.id === meta?.episode?.episodeId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (e.id === meta?.episode?.episodeId)
|
if (e.id === meta?.episode?.episodeId)
|
||||||
controls.closePopout();
|
controls.closePopout();
|
||||||
else setCurrent(currentSeasonInfo.id, e.id);
|
else setCurrent(currentSeasonInfo.id, e.id);
|
||||||
}}
|
}}
|
||||||
percentageCompleted={
|
percentageCompleted={
|
||||||
watched.items.find(
|
watched.items.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.item?.series?.seasonId ===
|
item.item?.series?.seasonId ===
|
||||||
currentSeasonInfo.id &&
|
currentSeasonInfo.id &&
|
||||||
item.item?.series?.episodeId === e.id
|
item.item?.series?.episodeId === e.id
|
||||||
)?.percentage
|
)?.percentage
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t("videoPlayer.popouts.episode", {
|
{t("videoPlayer.popouts.episode", {
|
||||||
index: e.number,
|
index: e.number,
|
||||||
title: e.title
|
title: e.title,
|
||||||
})}
|
})}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
))
|
))
|
||||||
: "No episodes"}
|
: "No episodes"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -7,12 +7,15 @@ import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
|||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { MWStream } from "@/backend/helpers/streams";
|
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 { runEmbedScraper, runProvider } from "@/backend/helpers/run";
|
||||||
import { MWProviderScrapeResult } from "@/backend/helpers/provider";
|
import { MWProviderScrapeResult } from "@/backend/helpers/provider";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
|
||||||
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
interface EmbedEntryProps {
|
interface EmbedEntryProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -24,35 +27,39 @@ interface EmbedEntryProps {
|
|||||||
export function EmbedEntry(props: EmbedEntryProps) {
|
export function EmbedEntry(props: EmbedEntryProps) {
|
||||||
const [scrapeEmbed, loading, error] = useLoading(async () => {
|
const [scrapeEmbed, loading, error] = useLoading(async () => {
|
||||||
const scraper = getEmbedScraperByType(props.type);
|
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, {
|
const stream = await runEmbedScraper(scraper, {
|
||||||
progress: () => { }, // no progress tracking for inline scraping
|
progress: () => {}, // no progress tracking for inline scraping
|
||||||
url: props.url,
|
url: props.url,
|
||||||
})
|
});
|
||||||
props.onSelect(stream);
|
props.onSelect(stream);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (<PopoutListEntry
|
return (
|
||||||
isOnDarkBackground
|
<PopoutListEntry
|
||||||
loading={loading}
|
isOnDarkBackground
|
||||||
errored={!!error}
|
loading={loading}
|
||||||
onClick={() => {
|
errored={!!error}
|
||||||
scrapeEmbed();
|
onClick={() => {
|
||||||
}}
|
scrapeEmbed();
|
||||||
>
|
}}
|
||||||
{props.name}
|
>
|
||||||
</PopoutListEntry>)
|
{props.name}
|
||||||
|
</PopoutListEntry>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SourceSelectionPopout() {
|
export function SourceSelectionPopout() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const controls = useControls(descriptor);
|
const controls = useControls(descriptor);
|
||||||
const meta = useMeta(descriptor);
|
const meta = useMeta(descriptor);
|
||||||
const providers = useMemo(
|
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]
|
[meta]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -71,7 +78,7 @@ export function SourceSelectionPopout() {
|
|||||||
if (!meta) throw new Error("need meta");
|
if (!meta) throw new Error("need meta");
|
||||||
return runProvider(theProvider, {
|
return runProvider(theProvider, {
|
||||||
media: meta.meta,
|
media: meta.meta,
|
||||||
progress: () => { },
|
progress: () => {},
|
||||||
type: meta.meta.meta.type,
|
type: meta.meta.meta.type,
|
||||||
episode: meta.episode?.episodeId as any,
|
episode: meta.episode?.episodeId as any,
|
||||||
season: meta.episode?.seasonId as any,
|
season: meta.episode?.seasonId as any,
|
||||||
@ -110,13 +117,13 @@ export function SourceSelectionPopout() {
|
|||||||
const realStream = v.stream;
|
const realStream = v.stream;
|
||||||
if (!realStream) {
|
if (!realStream) {
|
||||||
const embed = v?.embeds[0];
|
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);
|
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, {
|
const stream = await runEmbedScraper(scraper, {
|
||||||
progress: () => { }, // no progress tracking for inline scraping
|
progress: () => {}, // no progress tracking for inline scraping
|
||||||
url: embed.url,
|
url: embed.url,
|
||||||
})
|
});
|
||||||
selectSource(stream);
|
selectSource(stream);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -142,28 +149,30 @@ export function SourceSelectionPopout() {
|
|||||||
const embeds = scrapeResult?.embeds || [];
|
const embeds = scrapeResult?.embeds || [];
|
||||||
|
|
||||||
// Count embed types to determine if it should show a number behind the name
|
// Count embed types to determine if it should show a number behind the name
|
||||||
const embedsPerType: Record<string, (MWEmbed & { displayName: string })[]> = {}
|
const embedsPerType: Record<string, (MWEmbed & { displayName: string })[]> =
|
||||||
|
{};
|
||||||
for (const embed of embeds) {
|
for (const embed of embeds) {
|
||||||
if (!embed.type) continue;
|
if (!embed.type) continue;
|
||||||
if (!embedsPerType[embed.type]) embedsPerType[embed.type] = [];
|
if (!embedsPerType[embed.type]) embedsPerType[embed.type] = [];
|
||||||
embedsPerType[embed.type].push({
|
embedsPerType[embed.type].push({
|
||||||
...embed,
|
...embed,
|
||||||
displayName: embed.type
|
displayName: embed.type,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedsRes = Object.entries(embedsPerType).flatMap(([type, entries]) => {
|
const embedsRes = Object.entries(embedsPerType).flatMap(([_, entries]) => {
|
||||||
if (entries.length > 1) return entries.map((embed, i) => ({
|
if (entries.length > 1)
|
||||||
...embed,
|
return entries.map((embed, i) => ({
|
||||||
displayName: `${embed.type} ${i + 1}`
|
...embed,
|
||||||
}))
|
displayName: `${embed.type} ${i + 1}`,
|
||||||
|
}));
|
||||||
return entries;
|
return entries;
|
||||||
})
|
});
|
||||||
|
|
||||||
console.log(embedsRes)
|
console.log(embedsRes);
|
||||||
|
|
||||||
return embedsRes;
|
return embedsRes;
|
||||||
}, [scrapeResult?.embeds])
|
}, [scrapeResult?.embeds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -234,27 +243,31 @@ export function SourceSelectionPopout() {
|
|||||||
Native source
|
Native source
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
) : null}
|
) : null}
|
||||||
{(visibleEmbeds?.length || 0) > 0 ? visibleEmbeds?.map((v) => (
|
{(visibleEmbeds?.length || 0) > 0 ? (
|
||||||
<EmbedEntry
|
visibleEmbeds?.map((v) => (
|
||||||
type={v.type}
|
<EmbedEntry
|
||||||
name={v.displayName ?? ""}
|
type={v.type}
|
||||||
key={v.url}
|
name={v.displayName ?? ""}
|
||||||
url={v.url}
|
key={v.url}
|
||||||
onSelect={(stream) => {
|
url={v.url}
|
||||||
selectSource(stream);
|
onSelect={(stream) => {
|
||||||
}}
|
selectSource(stream);
|
||||||
/>
|
}}
|
||||||
)) : (<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
|
||||||
<IconPatch
|
|
||||||
icon={Icons.EYE_SLASH}
|
|
||||||
className="text-xl text-bink-600"
|
|
||||||
/>
|
/>
|
||||||
<p className="mt-6 w-full text-center">
|
))
|
||||||
{t("videoPlayer.popouts.noEmbeds")}
|
) : (
|
||||||
</p>
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col flex-wrap items-center text-slate-400">
|
||||||
|
<IconPatch
|
||||||
|
icon={Icons.EYE_SLASH}
|
||||||
|
className="text-xl text-bink-600"
|
||||||
|
/>
|
||||||
|
<p className="mt-6 w-full text-center">
|
||||||
|
{t("videoPlayer.popouts.noEmbeds")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
MWStreamType,
|
MWStreamType,
|
||||||
} from "@/backend/helpers/streams";
|
} from "@/backend/helpers/streams";
|
||||||
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
import { DetailedMeta } from "@/backend/metadata/getmeta";
|
||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
import { VideoPlayerStateProvider } from "./providers/providerTypes";
|
||||||
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { MWMediaMeta } from "@/backend/metadata/types";
|
|
||||||
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
|
||||||
import { useGoBack } from "@/hooks/useGoBack";
|
import { useGoBack } from "@/hooks/useGoBack";
|
||||||
import { conf } from "@/setup/config";
|
|
||||||
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
import { VideoPlayerHeader } from "@/video/components/parts/VideoPlayerHeader";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function MediaFetchErrorView() {
|
export function MediaFetchErrorView() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -20,9 +17,7 @@ export function MediaFetchErrorView() {
|
|||||||
<VideoPlayerHeader onClick={goBack} />
|
<VideoPlayerHeader onClick={goBack} />
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage>
|
<ErrorMessage>
|
||||||
<p className="my-6 max-w-lg">
|
<p className="my-6 max-w-lg">{t("media.errors.mediaFailed")}</p>
|
||||||
{t("media.errors.mediaFailed")}
|
|
||||||
</p>
|
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -19,13 +19,13 @@ import { ProgressListenerController } from "@/video/components/controllers/Progr
|
|||||||
import { VideoPlayerMeta } from "@/video/state/types";
|
import { VideoPlayerMeta } from "@/video/state/types";
|
||||||
import { SeriesController } from "@/video/components/controllers/SeriesController";
|
import { SeriesController } from "@/video/components/controllers/SeriesController";
|
||||||
import { useWatchedItem } from "@/state/watched";
|
import { useWatchedItem } from "@/state/watched";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { MediaFetchErrorView } from "./MediaErrorView";
|
import { MediaFetchErrorView } from "./MediaErrorView";
|
||||||
import { MediaScrapeLog } from "./MediaScrapeLog";
|
import { MediaScrapeLog } from "./MediaScrapeLog";
|
||||||
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
import { NotFoundMedia, NotFoundWrapper } from "../notfound/NotFoundView";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
function MediaViewLoading(props: { onGoBack(): void }) {
|
function MediaViewLoading(props: { onGoBack(): void }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen items-center justify-center">
|
<div className="relative flex h-screen items-center justify-center">
|
||||||
@ -37,7 +37,9 @@ function MediaViewLoading(props: { onGoBack(): void }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Loading className="mb-4" />
|
<Loading className="mb-4" />
|
||||||
<p className="mb-8 text-denim-700">{t("videoPlayer.findingBestVideo")}</p>
|
<p className="mb-8 text-denim-700">
|
||||||
|
{t("videoPlayer.findingBestVideo")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -51,7 +53,7 @@ interface MediaViewScrapingProps {
|
|||||||
}
|
}
|
||||||
function MediaViewScraping(props: MediaViewScrapingProps) {
|
function MediaViewScraping(props: MediaViewScrapingProps) {
|
||||||
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
const { eventLog, stream, pending } = useScrape(props.meta, props.selected);
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stream) {
|
if (stream) {
|
||||||
@ -78,14 +80,13 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
<IconPatch icon={Icons.EYE_SLASH} className="mb-8 text-bink-700" />
|
||||||
<p className="mb-8 text-denim-700">
|
<p className="mb-8 text-denim-700">{t("videoPlayer.noVideos")}</p>
|
||||||
{t("videoPlayer.noVideos")}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center transition-opacity duration-200 ${pending ? "opacity-100" : "opacity-0"
|
className={`flex flex-col items-center transition-opacity duration-200 ${
|
||||||
}`}
|
pending ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<MediaScrapeLog events={eventLog} />
|
<MediaScrapeLog events={eventLog} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,7 @@ export function NotFoundWrapper(props: {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
video?: boolean;
|
video?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const goBack = useGoBack();
|
const goBack = useGoBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,60 +2,66 @@ import { useEffect, useState } from "react";
|
|||||||
import pako from "pako";
|
import pako from "pako";
|
||||||
|
|
||||||
function fromBinary(str: string): Uint8Array {
|
function fromBinary(str: string): Uint8Array {
|
||||||
let result = new Uint8Array(str.length);
|
const result = new Uint8Array(str.length);
|
||||||
[...str].forEach((char, i) => {
|
[...str].forEach((char, i) => {
|
||||||
result[i] = char.charCodeAt(0);
|
result[i] = char.charCodeAt(0);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function V2MigrationView() {
|
export function V2MigrationView() {
|
||||||
const [done, setDone] = useState(false);
|
const [done, setDone] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search ?? "");
|
const params = new URLSearchParams(window.location.search ?? "");
|
||||||
if (!params.has("m-time") || !params.has("m-data")) {
|
if (!params.has("m-time") || !params.has("m-data")) {
|
||||||
// migration params missing, just redirect
|
// migration params missing, just redirect
|
||||||
setDone(true);
|
setDone(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(pako.inflate(fromBinary(atob(params.get("m-data") as string)), { to: "string" }));
|
const data = JSON.parse(
|
||||||
const timeOfMigration = new Date(params.get("m-time") as string);
|
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");
|
const savedTime = localStorage.getItem("mw-migration-date");
|
||||||
if (savedTime) {
|
if (savedTime) {
|
||||||
if (new Date(savedTime) >= timeOfMigration) {
|
if (new Date(savedTime) >= timeOfMigration) {
|
||||||
// has already migrated this or something newer, skip
|
// 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
|
|
||||||
setDone(true);
|
setDone(true);
|
||||||
}, [])
|
return;
|
||||||
|
}
|
||||||
// redirect when done
|
}
|
||||||
useEffect(() => {
|
|
||||||
if (!done) return;
|
|
||||||
const newUrl = new URL(window.location.href);
|
|
||||||
|
|
||||||
const newParams = [] as string[];
|
// restore migration data
|
||||||
newUrl.searchParams.forEach((_, key)=>newParams.push(key));
|
if (data.bookmarks)
|
||||||
newParams.forEach(v => newUrl.searchParams.delete(v))
|
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();
|
// redirect when done
|
||||||
}, [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;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { MWQuery } from "@/backend/metadata/types";
|
|
||||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
|
|
||||||
export function SearchLoadingView() {
|
export function SearchLoadingView() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [query] = useSearchQuery()
|
const [query] = useSearchQuery();
|
||||||
return (
|
return (
|
||||||
<>
|
<Loading
|
||||||
<Loading
|
className="mt-40 mb-24 "
|
||||||
className="mt-40 mb-24 "
|
text={
|
||||||
text={t(`search.loading_${query.type}`) || t("search.loading") || "Fetching your favourite shows..."}
|
t(`search.loading_${query.type}`) ||
|
||||||
/>
|
t("search.loading") ||
|
||||||
</>
|
"Fetching your favourite shows..."
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import Sticky from "react-stickynode";
|
import Sticky from "react-stickynode";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import loadVersion from "vite-plugin-package-version";
|
import loadVersion from "vite-plugin-package-version";
|
||||||
import checker from 'vite-plugin-checker'
|
import checker from "vite-plugin-checker";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -10,7 +10,14 @@ export default defineConfig({
|
|||||||
loadVersion(),
|
loadVersion(),
|
||||||
checker({
|
checker({
|
||||||
typescript: true, // check typescript build errors in dev server
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
Loading…
Reference in New Issue
Block a user