Merge pull request #114 from movie-web/dev

Version 2.2.3
This commit is contained in:
Jorrin
2024-03-14 22:54:50 +01:00
committed by GitHub
16 changed files with 351 additions and 94 deletions

View File

@@ -2,6 +2,13 @@
title: 'Changelog' title: 'Changelog'
--- ---
# Version 2.2.3
- Fix VidSrcTo
- Add HDRezka provider
- Fix Goojara causing a crash
- Improve react-native URLSearchParams implementation
- Cover an edge case where the title contains 'the movie' or 'the show'
# Version 2.2.2 # Version 2.2.2
- Fix subtitles not appearing if the name of the subtitle is in its native tongue. - Fix subtitles not appearing if the name of the subtitle is in its native tongue.
- Remove references to the old domain - Remove references to the old domain

View File

@@ -39,6 +39,23 @@ const providers = makeProviders({
``` ```
## React native ## React native
To use the library in a react native app, you would also need a couple of polyfills to polyfill crypto and base64.
1. First install the polyfills:
```bash
npm install @react-native-anywhere/polyfill-base64 react-native-quick-crypto
```
2. Add the polyfills to your app:
```ts
// Import in your entry file
import '@react-native-anywhere/polyfill-base64';
```
And follow the [react-native-quick-crypto documentation](https://github.com/margelo/react-native-quick-crypto) to set up the crypto polyfill.
3. Then you can use the library like this:
```ts ```ts
import { makeProviders, makeStandardFetcher, targets } from '@movie-web/providers'; import { makeProviders, makeStandardFetcher, targets } from '@movie-web/providers';

View File

@@ -6474,9 +6474,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.4", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "2.2.2", "version": "2.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "2.2.2", "version": "2.2.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "2.2.2", "version": "2.2.3",
"description": "Package that contains all the providers of movie-web", "description": "Package that contains all the providers of movie-web",
"main": "./lib/index.umd.js", "main": "./lib/index.umd.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",

View File

@@ -27,6 +27,7 @@ import { vidCloudScraper } from './embeds/vidcloud';
import { vidplayScraper } from './embeds/vidplay'; import { vidplayScraper } from './embeds/vidplay';
import { wootlyScraper } from './embeds/wootly'; import { wootlyScraper } from './embeds/wootly';
import { goojaraScraper } from './sources/goojara'; import { goojaraScraper } from './sources/goojara';
import { hdRezkaScraper } from './sources/hdrezka';
import { nepuScraper } from './sources/nepu'; import { nepuScraper } from './sources/nepu';
import { ridooMoviesScraper } from './sources/ridomovies'; import { ridooMoviesScraper } from './sources/ridomovies';
import { smashyStreamScraper } from './sources/smashystream'; import { smashyStreamScraper } from './sources/smashystream';
@@ -48,6 +49,7 @@ export function gatherAllSources(): Array<Sourcerer> {
vidSrcToScraper, vidSrcToScraper,
nepuScraper, nepuScraper,
goojaraScraper, goojaraScraper,
hdRezkaScraper,
]; ];
} }

View File

@@ -30,6 +30,8 @@ export const doodScraper = makeEmbed({
}); });
const downloadURL = `${doodPage}${nanoid()}?token=${dataForLater}&expiry=${Date.now()}`; const downloadURL = `${doodPage}${nanoid()}?token=${dataForLater}&expiry=${Date.now()}`;
if (!downloadURL.startsWith('http')) throw new Error('Invalid URL');
return { return {
stream: [ stream: [
{ {

View File

@@ -1,5 +1,7 @@
import { unpack } from 'unpacker'; import { unpack } from 'unpacker';
import { flags } from '@/entrypoint/utils/targets';
import { SubtitleResult } from './types'; import { SubtitleResult } from './types';
import { makeEmbed } from '../../base'; import { makeEmbed } from '../../base';
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../../captions'; import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '../../captions';
@@ -49,7 +51,7 @@ export const fileMoonScraper = makeEmbed({
id: 'primary', id: 'primary',
type: 'hls', type: 'hls',
playlist: file[1], playlist: file[1],
flags: [], flags: [flags.CORS_ALLOWED],
captions, captions,
}, },
], ],

View File

@@ -1,7 +1,8 @@
import { flags } from '@/entrypoint/utils/targets';
import { makeEmbed } from '@/providers/base'; import { makeEmbed } from '@/providers/base';
import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions'; import { Caption, getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
import { getFileUrl, referer } from './common'; import { getFileUrl } from './common';
import { SubtitleResult, VidplaySourceResponse } from './types'; import { SubtitleResult, VidplaySourceResponse } from './types';
export const vidplayScraper = makeEmbed({ export const vidplayScraper = makeEmbed({
@@ -44,12 +45,8 @@ export const vidplayScraper = makeEmbed({
id: 'primary', id: 'primary',
type: 'hls', type: 'hls',
playlist: source, playlist: source,
flags: [], flags: [flags.CORS_ALLOWED],
captions, captions,
preferredHeaders: {
Referer: referer,
Origin: referer,
},
}, },
], ],
}; };

View File

@@ -0,0 +1,127 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
import { MovieData, VideoLinks } from './types';
import { extractTitleAndYear, generateRandomFavs, parseSubtitleLinks, parseVideoLinks } from './utils';
const rezkaBase = 'https://hdrzk.org';
const baseHeaders = {
'X-Hdrezka-Android-App': '1',
'X-Hdrezka-Android-App-Version': '2.2.0',
};
async function searchAndFindMediaId(ctx: ShowScrapeContext | MovieScrapeContext): Promise<MovieData | null> {
const itemRegexPattern = /<a href="([^"]+)"><span class="enty">([^<]+)<\/span> \(([^)]+)\)/g;
const idRegexPattern = /\/(\d+)-[^/]+\.html$/;
const searchData = await ctx.proxiedFetcher<string>(`/engine/ajax/search.php`, {
baseUrl: rezkaBase,
headers: baseHeaders,
query: { q: ctx.media.title },
});
const movieData: MovieData[] = [];
for (const match of searchData.matchAll(itemRegexPattern)) {
const url = match[1];
const titleAndYear = match[3];
const result = extractTitleAndYear(titleAndYear);
if (result !== null) {
const id = url.match(idRegexPattern)?.[1] || null;
movieData.push({ id: id ?? '', year: result.year ?? 0, type: ctx.media.type, url });
}
}
const filteredItems = movieData.filter((item) => item.type === ctx.media.type && item.year === ctx.media.releaseYear);
return filteredItems[0] || null;
}
async function getStream(
id: string,
translatorId: string,
ctx: ShowScrapeContext | MovieScrapeContext,
): Promise<VideoLinks> {
const searchParams = new URLSearchParams();
searchParams.append('id', id);
searchParams.append('translator_id', translatorId);
if (ctx.media.type === 'show') {
searchParams.append('season', ctx.media.season.number.toString());
searchParams.append('episode', ctx.media.episode.number.toString());
}
if (ctx.media.type === 'movie') {
searchParams.append('is_camprip', '0');
searchParams.append('is_ads', '0');
searchParams.append('is_director', '0');
}
searchParams.append('favs', generateRandomFavs());
searchParams.append('action', ctx.media.type === 'show' ? 'get_stream' : 'get_movie');
const response = await ctx.proxiedFetcher<string>('/ajax/get_cdn_series/', {
baseUrl: rezkaBase,
method: 'POST',
body: searchParams,
headers: baseHeaders,
});
// Response content-type is text/html, but it's actually JSON
return JSON.parse(response);
}
async function getTranslatorId(
url: string,
id: string,
ctx: ShowScrapeContext | MovieScrapeContext,
): Promise<string | null> {
const response = await ctx.proxiedFetcher<string>(url, {
headers: baseHeaders,
});
// Translator ID 238 represents the Original + subtitles player.
if (response.includes(`data-translator_id="238"`)) return '238';
const functionName = ctx.media.type === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
const regexPattern = new RegExp(`sof\\.tv\\.${functionName}\\(${id}, ([^,]+)`, 'i');
const match = response.match(regexPattern);
const translatorId = match ? match[1] : null;
return translatorId;
}
const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> => {
const result = await searchAndFindMediaId(ctx);
if (!result || !result.id) throw new NotFoundError('No result found');
const translatorId = await getTranslatorId(result.url, result.id, ctx);
if (!translatorId) throw new NotFoundError('No translator id found');
const { url: streamUrl, subtitle: streamSubtitle } = await getStream(result.id, translatorId, ctx);
const parsedVideos = parseVideoLinks(streamUrl);
const parsedSubtitles = parseSubtitleLinks(streamSubtitle);
return {
embeds: [],
stream: [
{
id: 'primary',
type: 'file',
flags: [flags.CORS_ALLOWED, flags.IP_LOCKED],
captions: parsedSubtitles,
qualities: parsedVideos,
},
],
};
};
export const hdRezkaScraper = makeSourcerer({
id: 'hdrezka',
name: 'HDRezka',
rank: 195,
flags: [flags.CORS_ALLOWED, flags.IP_LOCKED],
scrapeShow: universalScraper,
scrapeMovie: universalScraper,
});

View File

@@ -0,0 +1,20 @@
import { ScrapeMedia } from '@/index';
export type VideoLinks = {
success: boolean;
message: string;
premium_content: number;
url: string;
quality: string;
subtitle: boolean | string;
subtitle_lns: boolean;
subtitle_def: boolean;
thumbnails: string;
};
export interface MovieData {
id: string | null;
year: number;
type: ScrapeMedia['type'];
url: string;
}

View File

@@ -0,0 +1,76 @@
import { getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
import { FileBasedStream } from '@/providers/streams';
import { NotFoundError } from '@/utils/errors';
import { getValidQualityFromString } from '@/utils/quality';
function generateRandomFavs(): string {
const randomHex = () => Math.floor(Math.random() * 16).toString(16);
const generateSegment = (length: number) => Array.from({ length }, randomHex).join('');
return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(
12,
)}`;
}
function parseSubtitleLinks(inputString?: string | boolean): FileBasedStream['captions'] {
if (!inputString || typeof inputString === 'boolean') return [];
const linksArray = inputString.split(',');
const captions: FileBasedStream['captions'] = [];
linksArray.forEach((link) => {
const match = link.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/);
if (match) {
const type = getCaptionTypeFromUrl(match[2]);
const language = labelToLanguageCode(match[1]);
if (!type || !language) return;
captions.push({
id: match[2],
language,
hasCorsRestrictions: false,
type,
url: match[2],
});
}
});
return captions;
}
function parseVideoLinks(inputString?: string): FileBasedStream['qualities'] {
if (!inputString) throw new NotFoundError('No video links found');
const linksArray = inputString.split(',');
const result: FileBasedStream['qualities'] = {};
linksArray.forEach((link) => {
const match = link.match(/\[([^]+)](https?:\/\/[^\s,]+\.mp4)/);
if (match) {
const qualityText = match[1];
const mp4Url = match[2];
const numericQualityMatch = qualityText.match(/(\d+p)/);
const quality = numericQualityMatch ? numericQualityMatch[1] : 'Unknown';
console.log(quality, mp4Url);
const validQuality = getValidQualityFromString(quality);
result[validQuality] = { type: 'mp4', url: mp4Url };
}
});
return result;
}
function extractTitleAndYear(input: string) {
const regex = /^(.*?),.*?(\d{4})/;
const match = input.match(regex);
if (match) {
const title = match[1];
const year = match[2];
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
}
return null;
}
export { extractTitleAndYear, parseSubtitleLinks, parseVideoLinks, generateRandomFavs };

View File

@@ -1,5 +1,6 @@
import { load } from 'cheerio'; import { load } from 'cheerio';
import { flags } from '@/entrypoint/utils/targets';
import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base'; import { SourcererEmbed, SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
@@ -33,7 +34,7 @@ const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Pr
if (sources.status !== 200) throw new Error('No sources found'); if (sources.status !== 200) throw new Error('No sources found');
const embeds: SourcererEmbed[] = []; const embeds: SourcererEmbed[] = [];
const embedUrls = []; const embedArr = [];
for (const source of sources.result) { for (const source of sources.result) {
const sourceRes = await ctx.proxiedFetcher<SourceResult>(`/ajax/embed/source/${source.id}`, { const sourceRes = await ctx.proxiedFetcher<SourceResult>(`/ajax/embed/source/${source.id}`, {
baseUrl: vidSrcToBase, baseUrl: vidSrcToBase,
@@ -42,28 +43,23 @@ const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Pr
}, },
}); });
const decryptedUrl = decryptSourceUrl(sourceRes.result.url); const decryptedUrl = decryptSourceUrl(sourceRes.result.url);
embedUrls.push(decryptedUrl); embedArr.push({ source: source.title, url: decryptedUrl });
} }
// Originally Filemoon does not have subtitles. But we can use the ones from Vidplay. for (const embedObj of embedArr) {
const urlWithSubtitles = embedUrls.find((v) => v.includes('sub.info')); if (embedObj.source === 'Vidplay') {
let subtitleUrl: string | null = null; const fullUrl = new URL(embedObj.url);
if (urlWithSubtitles) subtitleUrl = new URL(urlWithSubtitles).searchParams.get('sub.info');
for (const source of sources.result) {
if (source.title === 'Vidplay') {
const embedUrl = embedUrls.find((v) => v.includes('vidplay'));
if (!embedUrl) continue;
embeds.push({ embeds.push({
embedId: 'vidplay', embedId: 'vidplay',
url: embedUrl, url: fullUrl.toString(),
}); });
} }
if (source.title === 'Filemoon') { if (embedObj.source === 'Filemoon') {
const embedUrl = embedUrls.find((v) => v.includes('filemoon')); const fullUrl = new URL(embedObj.url);
if (!embedUrl) continue; // Originally Filemoon does not have subtitles. But we can use the ones from Vidplay.
const fullUrl = new URL(embedUrl); const urlWithSubtitles = embedArr.find((v) => v.source === 'Vidplay' && v.url.includes('sub.info'))?.url;
const subtitleUrl = urlWithSubtitles ? new URL(urlWithSubtitles).searchParams.get('sub.info') : null;
if (subtitleUrl) fullUrl.searchParams.set('sub.info', subtitleUrl); if (subtitleUrl) fullUrl.searchParams.set('sub.info', subtitleUrl);
embeds.push({ embeds.push({
embedId: 'filemoon', embedId: 'filemoon',
@@ -82,6 +78,6 @@ export const vidSrcToScraper = makeSourcerer({
name: 'VidSrcTo', name: 'VidSrcTo',
scrapeMovie: universalScraper, scrapeMovie: universalScraper,
scrapeShow: universalScraper, scrapeShow: universalScraper,
flags: [], flags: [flags.CORS_ALLOWED],
rank: 300, rank: 300,
}); });

View File

@@ -1,4 +1,4 @@
import { FullScraperEvents } from '@/entrypoint/utils/events'; import { FullScraperEvents, UpdateEvent } from '@/entrypoint/utils/events';
import { ScrapeMedia } from '@/entrypoint/utils/media'; import { ScrapeMedia } from '@/entrypoint/utils/media';
import { FeatureMap, flagsAllowedInFeatures } from '@/entrypoint/utils/targets'; import { FeatureMap, flagsAllowedInFeatures } from '@/entrypoint/utils/targets';
import { UseableFetcher } from '@/fetchers/types'; import { UseableFetcher } from '@/fetchers/types';
@@ -38,13 +38,13 @@ export type ProviderRunnerOptions = {
}; };
export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOptions): Promise<RunOutput | null> { export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOptions): Promise<RunOutput | null> {
const sources = reorderOnIdList(ops.sourceOrder ?? [], list.sources).filter((v) => { const sources = reorderOnIdList(ops.sourceOrder ?? [], list.sources).filter((source) => {
if (ops.media.type === 'movie') return !!v.scrapeMovie; if (ops.media.type === 'movie') return !!source.scrapeMovie;
if (ops.media.type === 'show') return !!v.scrapeShow; if (ops.media.type === 'show') return !!source.scrapeShow;
return false; return false;
}); });
const embeds = reorderOnIdList(ops.embedOrder ?? [], list.embeds); const embeds = reorderOnIdList(ops.embedOrder ?? [], list.embeds);
const embedIds = embeds.map((v) => v.id); const embedIds = embeds.map((embed) => embed.id);
let lastId = ''; let lastId = '';
const contextBase: ScrapeContext = { const contextBase: ScrapeContext = {
@@ -63,47 +63,41 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
sourceIds: sources.map((v) => v.id), sourceIds: sources.map((v) => v.id),
}); });
for (const s of sources) { for (const source of sources) {
ops.events?.start?.(s.id); ops.events?.start?.(source.id);
lastId = s.id; lastId = source.id;
// run source scrapers // run source scrapers
let output: SourcererOutput | null = null; let output: SourcererOutput | null = null;
try { try {
if (ops.media.type === 'movie' && s.scrapeMovie) if (ops.media.type === 'movie' && source.scrapeMovie)
output = await s.scrapeMovie({ output = await source.scrapeMovie({
...contextBase, ...contextBase,
media: ops.media, media: ops.media,
}); });
else if (ops.media.type === 'show' && s.scrapeShow) else if (ops.media.type === 'show' && source.scrapeShow)
output = await s.scrapeShow({ output = await source.scrapeShow({
...contextBase, ...contextBase,
media: ops.media, media: ops.media,
}); });
if (output) { if (output) {
output.stream = (output.stream ?? []) output.stream = (output.stream ?? [])
.filter((stream) => isValidStream(stream)) .filter(isValidStream)
.filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags)); .filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags));
} }
if (!output) throw Error('No output'); if (!output || (!output.stream?.length && !output.embeds.length)) {
if ((!output.stream || output.stream.length === 0) && output.embeds.length === 0)
throw new NotFoundError('No streams found'); throw new NotFoundError('No streams found');
} catch (err) {
if (err instanceof NotFoundError) {
ops.events?.update?.({
id: s.id,
percentage: 100,
status: 'notfound',
reason: err.message,
});
continue;
} }
ops.events?.update?.({ } catch (error) {
id: s.id, const updateParams: UpdateEvent = {
id: source.id,
percentage: 100, percentage: 100,
status: 'failure', status: error instanceof NotFoundError ? 'notfound' : 'failure',
error: err, reason: error instanceof NotFoundError ? error.message : undefined,
}); error: error instanceof NotFoundError ? undefined : error,
};
ops.events?.update?.(updateParams);
continue; continue;
} }
if (!output) throw new Error('Invalid media type'); if (!output) throw new Error('Invalid media type');
@@ -111,7 +105,7 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
// return stream is there are any // return stream is there are any
if (output.stream?.[0]) { if (output.stream?.[0]) {
return { return {
sourceId: s.id, sourceId: source.id,
stream: output.stream[0], stream: output.stream[0],
}; };
} }
@@ -120,62 +114,56 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt
const sortedEmbeds = output.embeds const sortedEmbeds = output.embeds
.filter((embed) => { .filter((embed) => {
const e = list.embeds.find((v) => v.id === embed.embedId); const e = list.embeds.find((v) => v.id === embed.embedId);
if (!e || e.disabled) return false; return e && !e.disabled;
return true;
}) })
.sort((a, b) => embedIds.indexOf(a.embedId) - embedIds.indexOf(b.embedId)); .sort((a, b) => embedIds.indexOf(a.embedId) - embedIds.indexOf(b.embedId));
if (sortedEmbeds.length > 0) { if (sortedEmbeds.length > 0) {
ops.events?.discoverEmbeds?.({ ops.events?.discoverEmbeds?.({
embeds: sortedEmbeds.map((v, i) => ({ embeds: sortedEmbeds.map((embed, i) => ({
id: [s.id, i].join('-'), id: [source.id, i].join('-'),
embedScraperId: v.embedId, embedScraperId: embed.embedId,
})), })),
sourceId: s.id, sourceId: source.id,
}); });
} }
for (const ind in sortedEmbeds) { for (const [ind, embed] of sortedEmbeds.entries()) {
if (!Object.prototype.hasOwnProperty.call(sortedEmbeds, ind)) continue; const scraper = embeds.find((v) => v.id === embed.embedId);
const e = sortedEmbeds[ind];
const scraper = embeds.find((v) => v.id === e.embedId);
if (!scraper) throw new Error('Invalid embed returned'); if (!scraper) throw new Error('Invalid embed returned');
// run embed scraper // run embed scraper
const id = [s.id, ind].join('-'); const id = [source.id, ind].join('-');
ops.events?.start?.(id); ops.events?.start?.(id);
lastId = id; lastId = id;
let embedOutput: EmbedOutput; let embedOutput: EmbedOutput;
try { try {
embedOutput = await scraper.scrape({ embedOutput = await scraper.scrape({
...contextBase, ...contextBase,
url: e.url, url: embed.url,
}); });
embedOutput.stream = embedOutput.stream embedOutput.stream = embedOutput.stream
.filter((stream) => isValidStream(stream)) .filter(isValidStream)
.filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags)); .filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags));
if (embedOutput.stream.length === 0) throw new NotFoundError('No streams found'); if (embedOutput.stream.length === 0) {
} catch (err) { throw new NotFoundError('No streams found');
if (err instanceof NotFoundError) {
ops.events?.update?.({
id,
percentage: 100,
status: 'notfound',
reason: err.message,
});
continue;
} }
ops.events?.update?.({ } catch (error) {
id, const updateParams: UpdateEvent = {
id: source.id,
percentage: 100, percentage: 100,
status: 'failure', status: error instanceof NotFoundError ? 'notfound' : 'failure',
error: err, reason: error instanceof NotFoundError ? error.message : undefined,
}); error: error instanceof NotFoundError ? undefined : error,
};
ops.events?.update?.(updateParams);
continue; continue;
} }
return { return {
sourceId: s.id, sourceId: source.id,
embedId: scraper.id, embedId: scraper.id,
stream: embedOutput.stream[0], stream: embedOutput.stream[0],
}; };

View File

@@ -1,11 +1,14 @@
import { CommonMedia } from '@/entrypoint/utils/media'; import { CommonMedia } from '@/entrypoint/utils/media';
export function normalizeTitle(title: string): string { export function normalizeTitle(title: string): string {
return title let titleTrimmed = title.trim().toLowerCase();
.trim() if (titleTrimmed !== 'the movie' && titleTrimmed.endsWith('the movie')) {
.toLowerCase() titleTrimmed = titleTrimmed.replace('the movie', '');
.replace(/['":]/g, '') }
.replace(/[^a-zA-Z0-9]+/g, '_'); if (titleTrimmed !== 'the series' && titleTrimmed.endsWith('the series')) {
titleTrimmed = titleTrimmed.replace('the series', '');
}
return titleTrimmed.replace(/['":]/g, '').replace(/[^a-zA-Z0-9]+/g, '_');
} }
export function compareTitle(a: string, b: string): boolean { export function compareTitle(a: string, b: string): boolean {

20
src/utils/quality.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Qualities } from '@/providers/streams';
export function getValidQualityFromString(quality: string): Qualities {
switch (quality.toLowerCase().replace('p', '')) {
case '360':
return '360';
case '480':
return '480';
case '720':
return '720';
case '1080':
return '1080';
case '2160':
return '4k';
case '4k':
return '4k';
default:
return 'unknown';
}
}