diff --git a/.docs/content/1.get-started/4.changelog.md b/.docs/content/1.get-started/4.changelog.md index 0836b31..d792e30 100644 --- a/.docs/content/1.get-started/4.changelog.md +++ b/.docs/content/1.get-started/4.changelog.md @@ -2,6 +2,11 @@ title: 'Changelog' --- +# Version 2.3.0 +- Fixed RidoMovies search results +- Added Insertunit, SoaperTV, and WarezCDN providers +- Disabled Showbox and VidSrc + # Version 2.2.9 - Fixed VidSrcTo (both Vidplay and Filemoon embeds) - Added dropload, filelions and vtube embeds to Primewire diff --git a/package.json b/package.json index 69cfad1..c329891 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@movie-web/providers", - "version": "2.2.9", + "version": "2.3.0", "description": "Package that contains all the providers of movie-web", "type": "module", "main": "./lib/index.js", diff --git a/src/providers/all.ts b/src/providers/all.ts index 63be995..9cb3585 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -14,6 +14,7 @@ import { vidsrcembedScraper } from '@/providers/embeds/vidsrc'; import { vTubeScraper } from '@/providers/embeds/vtube'; import { flixhqScraper } from '@/providers/sources/flixhq/index'; import { goMoviesScraper } from '@/providers/sources/gomovies/index'; +import { insertunitScraper } from '@/providers/sources/insertunit'; import { kissAsianScraper } from '@/providers/sources/kissasian/index'; import { lookmovieScraper } from '@/providers/sources/lookmovie'; import { remotestreamScraper } from '@/providers/sources/remotestream'; @@ -31,6 +32,8 @@ import { streamvidScraper } from './embeds/streamvid'; import { vidCloudScraper } from './embeds/vidcloud'; import { vidplayScraper } from './embeds/vidplay'; import { voeScraper } from './embeds/voe'; +import { warezcdnembedHlsScraper } from './embeds/warezcdn/hls'; +import { warezcdnembedMp4Scraper } from './embeds/warezcdn/mp4'; import { wootlyScraper } from './embeds/wootly'; import { goojaraScraper } from './sources/goojara'; import { hdRezkaScraper } from './sources/hdrezka'; @@ -38,7 +41,9 @@ import { nepuScraper } from './sources/nepu'; import { primewireScraper } from './sources/primewire'; import { ridooMoviesScraper } from './sources/ridomovies'; import { smashyStreamScraper } from './sources/smashystream'; +import { soaperTvScraper } from './sources/soapertv'; import { vidSrcToScraper } from './sources/vidsrcto'; +import { warezcdnScraper } from './sources/warezcdn'; export function gatherAllSources(): Array { // all sources are gathered here @@ -58,6 +63,9 @@ export function gatherAllSources(): Array { goojaraScraper, hdRezkaScraper, primewireScraper, + warezcdnScraper, + insertunitScraper, + soaperTvScraper, ]; } @@ -88,5 +96,7 @@ export function gatherAllEmbeds(): Array { droploadScraper, filelionsScraper, vTubeScraper, + warezcdnembedHlsScraper, + warezcdnembedMp4Scraper, ]; } diff --git a/src/providers/embeds/vidsrc.ts b/src/providers/embeds/vidsrc.ts index 069c2f6..716c82c 100644 --- a/src/providers/embeds/vidsrc.ts +++ b/src/providers/embeds/vidsrc.ts @@ -1,5 +1,5 @@ -import { flags } from '@/entrypoint/utils/targets'; import { makeEmbed } from '@/providers/base'; +import { vidsrcRCPBase } from '@/providers/sources/vidsrc/common'; const hlsURLRegex = /file:"(.*?)"/; const setPassRegex = /var pass_path = "(.*set_pass\.php.*)";/; @@ -32,28 +32,35 @@ export const vidsrcembedScraper = makeEmbed({ if (!finalUrl.includes('.m3u8')) throw new Error('Unable to find HLS playlist'); let setPassLink = html.match(setPassRegex)?.[1]; - if (!setPassLink) throw new Error('Unable to find set_pass.php link'); - if (setPassLink.startsWith('//')) { - setPassLink = `https:${setPassLink}`; + // isn't neeeded, the stream works without it anyway + // shouldn't fail if the setpass link is not found + if (setPassLink) { + if (setPassLink.startsWith('//')) { + setPassLink = `https:${setPassLink}`; + } + + // VidSrc uses a password endpoint to temporarily whitelist the user's IP. This is called in an interval by the player. + // It currently has no effect on the player itself, the content plays fine without it. + // In the future we might have to introduce hooks for the frontend to call this endpoint. + await ctx.proxiedFetcher(setPassLink, { + headers: { + referer: ctx.url, + }, + }); } - // VidSrc uses a password endpoint to temporarily whitelist the user's IP. This is called in an interval by the player. - // It currently has no effect on the player itself, the content plays fine without it. - // In the future we might have to introduce hooks for the frontend to call this endpoint. - await ctx.proxiedFetcher(setPassLink, { - headers: { - referer: ctx.url, - }, - }); - return { stream: [ { id: 'primary', type: 'hls', playlist: finalUrl, - flags: [flags.CORS_ALLOWED], + headers: { + Referer: vidsrcRCPBase, + Origin: vidsrcRCPBase, + }, + flags: [], captions: [], }, ], diff --git a/src/providers/embeds/warezcdn/common.ts b/src/providers/embeds/warezcdn/common.ts new file mode 100644 index 0000000..2762ed8 --- /dev/null +++ b/src/providers/embeds/warezcdn/common.ts @@ -0,0 +1,58 @@ +import { warezcdnPlayerBase } from '@/providers/sources/warezcdn/common'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +function decrypt(input: string) { + let output = atob(input); + + // Remove leading and trailing whitespaces + output = output.trim(); + + // Reverse the string + output = output.split('').reverse().join(''); + + // Get the last 5 characters and reverse them + let last = output.slice(-5); + last = last.split('').reverse().join(''); + + // Remove the last 5 characters from the original string + output = output.slice(0, -5); + + // Return the original string concatenated with the reversed last 5 characters + return `${output}${last}`; +} + +export async function getDecryptedId(ctx: EmbedScrapeContext) { + const page = await ctx.proxiedFetcher(`/player.php`, { + baseUrl: warezcdnPlayerBase, + headers: { + Referer: `${warezcdnPlayerBase}/getEmbed.php?${new URLSearchParams({ + id: ctx.url, + sv: 'warezcdn', + })}`, + }, + query: { + id: ctx.url, + }, + }); + const allowanceKey = page.match(/let allowanceKey = "(.*?)";/)?.[1]; + if (!allowanceKey) throw new NotFoundError('Failed to get allowanceKey'); + + const streamData = await ctx.proxiedFetcher('/functions.php', { + baseUrl: warezcdnPlayerBase, + method: 'POST', + body: new URLSearchParams({ + getVideo: ctx.url, + key: allowanceKey, + }), + }); + const stream = JSON.parse(streamData); + + if (!stream.id) throw new NotFoundError("can't get stream id"); + + const decryptedId = decrypt(stream.id); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + return decryptedId; +} diff --git a/src/providers/embeds/warezcdn/hls.ts b/src/providers/embeds/warezcdn/hls.ts new file mode 100644 index 0000000..2e49852 --- /dev/null +++ b/src/providers/embeds/warezcdn/hls.ts @@ -0,0 +1,44 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { getDecryptedId } from './common'; + +// Method found by atpn +async function getVideowlUrlStream(ctx: EmbedScrapeContext, decryptedId: string) { + const sharePage = await ctx.proxiedFetcher('https://cloud.mail.ru/public/uaRH/2PYWcJRpH'); + const regex = /"videowl_view":\{"count":"(\d+)","url":"([^"]+)"\}/g; + const videowlUrl = regex.exec(sharePage)?.[2]; + + if (!videowlUrl) throw new NotFoundError('Failed to get videoOwlUrl'); + + return `${videowlUrl}/0p/${btoa(decryptedId)}.m3u8?${new URLSearchParams({ + double_encode: '1', + })}`; +} + +export const warezcdnembedHlsScraper = makeEmbed({ + id: 'warezcdnembedhls', // WarezCDN is both a source and an embed host + name: 'WarezCDN HLS', + rank: 83, + async scrape(ctx) { + const decryptedId = await getDecryptedId(ctx); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + const streamUrl = await getVideowlUrlStream(ctx, decryptedId); + + return { + stream: [ + { + id: 'primary', + type: 'hls', + flags: [flags.IP_LOCKED], + captions: [], + playlist: streamUrl, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/warezcdn/mp4.ts b/src/providers/embeds/warezcdn/mp4.ts new file mode 100644 index 0000000..ada781d --- /dev/null +++ b/src/providers/embeds/warezcdn/mp4.ts @@ -0,0 +1,58 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; +import { warezcdnWorkerProxy } from '@/providers/sources/warezcdn/common'; +import { EmbedScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { getDecryptedId } from './common'; + +const cdnListing = [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64]; + +async function checkUrls(ctx: EmbedScrapeContext, fileId: string) { + for (const id of cdnListing) { + const url = `https://cloclo${id}.cloud.mail.ru/weblink/view/${fileId}`; + const response = await ctx.proxiedFetcher.full(url, { + method: 'GET', + headers: { + Range: 'bytes=0-1', + }, + }); + if (response.statusCode === 206) return url; + } + return null; +} + +export const warezcdnembedMp4Scraper = makeEmbed({ + id: 'warezcdnembedmp4', // WarezCDN is both a source and an embed host + name: 'WarezCDN MP4', + rank: 82, + disabled: false, + async scrape(ctx) { + const decryptedId = await getDecryptedId(ctx); + + if (!decryptedId) throw new NotFoundError("can't get file id"); + + const streamUrl = await checkUrls(ctx, decryptedId); + + if (!streamUrl) throw new NotFoundError("can't get stream id"); + + return { + stream: [ + { + id: 'primary', + captions: [], + qualities: { + unknown: { + type: 'mp4', + url: `${warezcdnWorkerProxy}/?${new URLSearchParams({ + url: streamUrl, + })}`, + }, + }, + type: 'file', + flags: [flags.CORS_ALLOWED], + }, + ], + }; + }, +}); diff --git a/src/providers/sources/insertunit/captions.ts b/src/providers/sources/insertunit/captions.ts new file mode 100644 index 0000000..881c9c2 --- /dev/null +++ b/src/providers/sources/insertunit/captions.ts @@ -0,0 +1,30 @@ +import { Caption, removeDuplicatedLanguages } from '@/providers/captions'; + +import { Subtitle } from './types'; + +export async function getCaptions(data: Subtitle[]) { + let captions: Caption[] = []; + for (const subtitle of data) { + let language = ''; + + if (subtitle.name.includes('Рус')) { + language = 'ru'; + } else if (subtitle.name.includes('Укр')) { + language = 'uk'; + } else if (subtitle.name.includes('Eng')) { + language = 'en'; + } else { + continue; + } + + captions.push({ + id: subtitle.url, + url: subtitle.url, + language, + type: 'vtt', + hasCorsRestrictions: false, + }); + } + captions = removeDuplicatedLanguages(captions); + return captions; +} diff --git a/src/providers/sources/insertunit/index.ts b/src/providers/sources/insertunit/index.ts new file mode 100644 index 0000000..9a54866 --- /dev/null +++ b/src/providers/sources/insertunit/index.ts @@ -0,0 +1,103 @@ +import { flags } from '@/entrypoint/utils/targets'; +import { makeSourcerer } from '@/providers/base'; +import { Caption } from '@/providers/captions'; +import { NotFoundError } from '@/utils/errors'; + +import { getCaptions } from './captions'; +import { Season } from './types'; + +const insertUnitBase = 'https://api.insertunit.ws/'; + +export const insertunitScraper = makeSourcerer({ + id: 'insertunit', + name: 'Insertunit', + disabled: false, + rank: 60, + flags: [flags.CORS_ALLOWED], + async scrapeShow(ctx) { + const playerData = await ctx.fetcher(`/embed/imdb/${ctx.media.imdbId}`, { + baseUrl: insertUnitBase, + }); + ctx.progress(30); + + const seasonDataJSONregex = /seasons:(.*)/; + const seasonData = seasonDataJSONregex.exec(playerData); + + if (seasonData === null || seasonData[1] === null) { + throw new NotFoundError('No result found'); + } + ctx.progress(60); + + const seasonTable: Season[] = JSON.parse(seasonData[1]) as Season[]; + + const currentSeason = seasonTable.find( + (seasonElement) => seasonElement.season === ctx.media.season.number && !seasonElement.blocked, + ); + + const currentEpisode = currentSeason?.episodes.find((episodeElement) => + episodeElement.episode.includes(ctx.media.episode.number.toString()), + ); + + if (!currentEpisode?.hls) throw new NotFoundError('No result found'); + + let captions: Caption[] = []; + + if (currentEpisode.cc != null) { + captions = await getCaptions(currentEpisode.cc); + } + + ctx.progress(95); + + return { + embeds: [], + stream: [ + { + id: 'primary', + playlist: currentEpisode.hls, + type: 'hls', + flags: [flags.CORS_ALLOWED], + captions, + }, + ], + }; + }, + async scrapeMovie(ctx) { + const playerData = await ctx.fetcher(`/embed/imdb/${ctx.media.imdbId}`, { + baseUrl: insertUnitBase, + }); + ctx.progress(35); + + const streamRegex = /hls: "([^"]*)/; + const streamData = streamRegex.exec(playerData); + + if (streamData === null || streamData[1] === null) { + throw new NotFoundError('No result found'); + } + ctx.progress(75); + + const subtitleRegex = /cc: (.*)/; + const subtitleJSONData = subtitleRegex.exec(playerData); + + let captions: Caption[] = []; + + if (subtitleJSONData != null && subtitleJSONData[1] != null) { + const subtitleData = JSON.parse(subtitleJSONData[1]); + captions = await getCaptions(subtitleData); + } + + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'hls', + playlist: streamData[1], + flags: [flags.CORS_ALLOWED], + captions, + }, + ], + }; + }, +}); diff --git a/src/providers/sources/insertunit/types.ts b/src/providers/sources/insertunit/types.ts new file mode 100644 index 0000000..587ae36 --- /dev/null +++ b/src/providers/sources/insertunit/types.ts @@ -0,0 +1,30 @@ +export interface Subtitle { + url: string; + name: string; +} + +export interface Episode { + episode: string; + id: number; + videoKey: string; + hls: string; + audio: { + names: string[]; + order: number[]; + }; + cc: Subtitle[]; + duration: number; + title: string; + download: string; + sections: string[]; + poster: string; + preview: { + src: string; + }; +} + +export interface Season { + season: number; + blocked: boolean; + episodes: Episode[]; +} diff --git a/src/providers/sources/showbox/index.ts b/src/providers/sources/showbox/index.ts index c22979a..9901a52 100644 --- a/src/providers/sources/showbox/index.ts +++ b/src/providers/sources/showbox/index.ts @@ -42,6 +42,7 @@ export const showboxScraper = makeSourcerer({ id: 'showbox', name: 'Showbox', rank: 150, + disabled: true, flags: [flags.CORS_ALLOWED, flags.CF_BLOCKED], scrapeShow: comboScraper, scrapeMovie: comboScraper, diff --git a/src/providers/sources/soapertv/index.ts b/src/providers/sources/soapertv/index.ts new file mode 100644 index 0000000..feee555 --- /dev/null +++ b/src/providers/sources/soapertv/index.ts @@ -0,0 +1,120 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { Caption, labelToLanguageCode } from '@/providers/captions'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +import { InfoResponse } from './types'; +import { SourcererOutput, makeSourcerer } from '../../base'; + +const baseUrl = 'https://soaper.tv'; + +const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext): Promise => { + const searchResult = await ctx.proxiedFetcher('/search.html', { + baseUrl, + query: { + keyword: ctx.media.title, + }, + }); + const searchResult$ = load(searchResult); + let showLink = searchResult$('a') + .filter((_, el) => searchResult$(el).text() === ctx.media.title) + .attr('href'); + if (!showLink) throw new NotFoundError('Content not found'); + + if (ctx.media.type === 'show') { + const seasonNumber = ctx.media.season.number; + const episodeNumber = ctx.media.episode.number; + const showPage = await ctx.proxiedFetcher(showLink, { baseUrl }); + const showPage$ = load(showPage); + const seasonBlock = showPage$('h4') + .filter((_, el) => showPage$(el).text().trim().split(':')[0].trim() === `Season${seasonNumber}`) + .parent(); + const episodes = seasonBlock.find('a').toArray(); + showLink = showPage$( + episodes.find((el) => parseInt(showPage$(el).text().split('.')[0], 10) === episodeNumber), + ).attr('href'); + } + if (!showLink) throw new NotFoundError('Content not found'); + const contentPage = await ctx.proxiedFetcher(showLink, { baseUrl }); + const contentPage$ = load(contentPage); + + const pass = contentPage$('#hId').attr('value'); + const param = contentPage$('#divU').text(); + + if (!pass || !param) throw new NotFoundError('Content not found'); + + const formData = new URLSearchParams(); + formData.append('pass', pass); + formData.append('param', param); + formData.append('e2', '0'); + formData.append('server', '0'); + + const infoEndpoint = ctx.media.type === 'show' ? '/home/index/getEInfoAjax' : '/home/index/getMInfoAjax'; + const streamRes = await ctx.proxiedFetcher(infoEndpoint, { + baseUrl, + method: 'POST', + body: formData, + headers: { + referer: `${baseUrl}${showLink}`, + }, + }); + + const streamResJson: InfoResponse = JSON.parse(streamRes); + + const captions: Caption[] = []; + for (const sub of streamResJson.subs) { + // Some subtitles are named .srt, some are named :hi, or just + let language: string | null = ''; + if (sub.name.includes('.srt')) { + language = labelToLanguageCode(sub.name.split('.srt')[0]); + } else if (sub.name.includes(':')) { + language = sub.name.split(':')[0]; + } else { + language = sub.name; + } + if (!language) continue; + + captions.push({ + id: sub.path, + url: sub.path, + type: 'srt', + hasCorsRestrictions: false, + language, + }); + } + + return { + embeds: [], + stream: [ + { + id: 'primary', + playlist: streamResJson.val, + type: 'hls', + flags: [flags.IP_LOCKED], + captions, + }, + ...(streamResJson.val_bak + ? [ + { + id: 'backup', + playlist: streamResJson.val_bak, + type: 'hls' as const, + flags: [flags.IP_LOCKED], + captions, + }, + ] + : []), + ], + }; +}; + +export const soaperTvScraper = makeSourcerer({ + id: 'soapertv', + name: 'SoaperTV', + rank: 115, + flags: [flags.IP_LOCKED], + scrapeMovie: universalScraper, + scrapeShow: universalScraper, +}); diff --git a/src/providers/sources/soapertv/types.ts b/src/providers/sources/soapertv/types.ts new file mode 100644 index 0000000..70fb602 --- /dev/null +++ b/src/providers/sources/soapertv/types.ts @@ -0,0 +1,15 @@ +export interface Subtitle { + path: string; + name: string; +} + +export interface InfoResponse { + key: boolean; + val: string; + vtt: string; + val_bak: string; + pos: number; + type: string; + subs: Subtitle[]; + ip: string; +} diff --git a/src/providers/sources/vidsrc/common.ts b/src/providers/sources/vidsrc/common.ts index 4ccc93c..6f6fd71 100644 --- a/src/providers/sources/vidsrc/common.ts +++ b/src/providers/sources/vidsrc/common.ts @@ -1,2 +1,2 @@ export const vidsrcBase = 'https://vidsrc.me'; -export const vidsrcRCPBase = 'https://rcp.vidsrc.me'; +export const vidsrcRCPBase = 'https://vidsrc.stream'; diff --git a/src/providers/sources/vidsrc/index.ts b/src/providers/sources/vidsrc/index.ts index d6b15a8..d4df2f7 100644 --- a/src/providers/sources/vidsrc/index.ts +++ b/src/providers/sources/vidsrc/index.ts @@ -1,4 +1,3 @@ -import { flags } from '@/entrypoint/utils/targets'; import { makeSourcerer } from '@/providers/base'; import { scrapeMovie } from '@/providers/sources/vidsrc/scrape-movie'; import { scrapeShow } from '@/providers/sources/vidsrc/scrape-show'; @@ -7,7 +6,8 @@ export const vidsrcScraper = makeSourcerer({ id: 'vidsrc', name: 'VidSrc', rank: 90, - flags: [flags.CORS_ALLOWED], + disabled: true, + flags: [], scrapeMovie, scrapeShow, }); diff --git a/src/providers/sources/warezcdn/common.ts b/src/providers/sources/warezcdn/common.ts new file mode 100644 index 0000000..182b2b6 --- /dev/null +++ b/src/providers/sources/warezcdn/common.ts @@ -0,0 +1,24 @@ +import { ScrapeContext } from '@/utils/context'; + +export const warezcdnBase = 'https://embed.warezcdn.com'; +export const warezcdnApiBase = 'https://warezcdn.com/embed'; +export const warezcdnPlayerBase = 'https://warezcdn.com/player'; +export const warezcdnWorkerProxy = 'https://workerproxy.warezcdn.workers.dev'; + +export async function getExternalPlayerUrl(ctx: ScrapeContext, embedId: string, embedUrl: string) { + const params = { + id: embedUrl, + sv: embedId, + }; + const realUrl = await ctx.proxiedFetcher(`/getPlay.php`, { + baseUrl: warezcdnApiBase, + headers: { + Referer: `${warezcdnApiBase}/getEmbed.php?${new URLSearchParams(params)}`, + }, + query: params, + }); + + const realEmbedUrl = realUrl.match(/window\.location\.href="([^"]*)";/); + if (!realEmbedUrl) throw new Error('Could not find embed url'); + return realEmbedUrl[1]; +} diff --git a/src/providers/sources/warezcdn/index.ts b/src/providers/sources/warezcdn/index.ts new file mode 100644 index 0000000..f27b052 --- /dev/null +++ b/src/providers/sources/warezcdn/index.ts @@ -0,0 +1,114 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererEmbed, makeSourcerer } from '@/providers/base'; +import { mixdropScraper } from '@/providers/embeds/mixdrop'; +import { warezcdnembedHlsScraper } from '@/providers/embeds/warezcdn/hls'; +import { warezcdnembedMp4Scraper } from '@/providers/embeds/warezcdn/mp4'; +import { NotFoundError } from '@/utils/errors'; + +import { getExternalPlayerUrl, warezcdnBase } from './common'; +import { SerieAjaxResponse } from './types'; + +export const warezcdnScraper = makeSourcerer({ + id: 'warezcdn', + name: 'WarezCDN', + rank: 81, + flags: [flags.CORS_ALLOWED], + scrapeMovie: async (ctx) => { + if (!ctx.media.imdbId) throw new NotFoundError('This source requires IMDB id.'); + + const serversPage = await ctx.proxiedFetcher(`/filme/${ctx.media.imdbId}`, { + baseUrl: warezcdnBase, + }); + const $ = load(serversPage); + + const embedsHost = $('.hostList.active [data-load-embed]').get(); + + const embeds: SourcererEmbed[] = []; + + embedsHost.forEach(async (element) => { + const embedHost = $(element).attr('data-load-embed-host')!; + const embedUrl = $(element).attr('data-load-embed')!; + + if (embedHost === 'mixdrop') { + const realEmbedUrl = await getExternalPlayerUrl(ctx, 'mixdrop', embedUrl); + if (!realEmbedUrl) throw new Error('Could not find embed url'); + embeds.push({ + embedId: mixdropScraper.id, + url: realEmbedUrl, + }); + } else if (embedHost === 'warezcdn') { + embeds.push( + { + embedId: warezcdnembedHlsScraper.id, + url: embedUrl, + }, + { + embedId: warezcdnembedMp4Scraper.id, + url: embedUrl, + }, + ); + } + }); + + return { + embeds, + }; + }, + scrapeShow: async (ctx) => { + if (!ctx.media.imdbId) throw new NotFoundError('This source requires IMDB id.'); + + const url = `${warezcdnBase}/serie/${ctx.media.imdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + + const serversPage = await ctx.proxiedFetcher(url); + + const episodeId = serversPage.match(/\$\('\[data-load-episode-content="(\d+)"\]'\)/)?.[1]; + + if (!episodeId) throw new NotFoundError('Failed to find episode id'); + + const streamsData = await ctx.proxiedFetcher(`/serieAjax.php`, { + method: 'POST', + baseUrl: warezcdnBase, + body: new URLSearchParams({ + getAudios: episodeId, + }), + headers: { + Origin: warezcdnBase, + Referer: url, + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + + const streams: SerieAjaxResponse = JSON.parse(streamsData); + const list = streams.list['0']; + const embeds: SourcererEmbed[] = []; + + // 3 means ok + if (list.mixdropStatus === '3') { + const realEmbedUrl = await getExternalPlayerUrl(ctx, 'mixdrop', list.id); + if (!realEmbedUrl) throw new Error('Could not find embed url'); + embeds.push({ + embedId: mixdropScraper.id, + url: realEmbedUrl, + }); + } + + if (list.warezcdnStatus === '3') { + embeds.push( + { + embedId: warezcdnembedHlsScraper.id, + url: list.id, + }, + { + embedId: warezcdnembedMp4Scraper.id, + url: list.id, + }, + ); + } + + return { + embeds, + }; + }, +}); diff --git a/src/providers/sources/warezcdn/types.ts b/src/providers/sources/warezcdn/types.ts new file mode 100644 index 0000000..38711ff --- /dev/null +++ b/src/providers/sources/warezcdn/types.ts @@ -0,0 +1,16 @@ +interface Data { + id: string; + audio: string; + mixdropStatus: string; + fembedStatus: string; + streamtapeStatus: string; + warezcdnStatus: string; +} + +type List = { + [key: string]: Data; +}; + +export interface SerieAjaxResponse { + list: List; +} diff --git a/src/runners/individualRunner.ts b/src/runners/individualRunner.ts index 6c7e452..b309180 100644 --- a/src/runners/individualRunner.ts +++ b/src/runners/individualRunner.ts @@ -71,7 +71,7 @@ export async function scrapeInvidualSource( // only check for playable streams if there are streams, and if there are no embeds if (output.stream && output.stream.length > 0 && output.embeds.length === 0) { - const playableStreams = await validatePlayableStreams(output.stream, ops); + const playableStreams = await validatePlayableStreams(output.stream, ops, sourceScraper.id); if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); output.stream = playableStreams; } @@ -112,7 +112,7 @@ export async function scrapeIndividualEmbed( .filter((stream) => flagsAllowedInFeatures(ops.features, stream.flags)); if (output.stream.length === 0) throw new NotFoundError('No streams found'); - const playableStreams = await validatePlayableStreams(output.stream, ops); + const playableStreams = await validatePlayableStreams(output.stream, ops, embedScraper.id); if (playableStreams.length === 0) throw new NotFoundError('No playable streams found'); output.stream = playableStreams; diff --git a/src/runners/runner.ts b/src/runners/runner.ts index c3bc9eb..c5f5de3 100644 --- a/src/runners/runner.ts +++ b/src/runners/runner.ts @@ -104,7 +104,7 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt // return stream is there are any if (output.stream?.[0]) { - const playableStream = await validatePlayableStream(output.stream[0], ops); + const playableStream = await validatePlayableStream(output.stream[0], ops, source.id); if (!playableStream) throw new NotFoundError('No streams found'); return { sourceId: source.id, @@ -151,7 +151,7 @@ export async function runAllProviders(list: ProviderList, ops: ProviderRunnerOpt if (embedOutput.stream.length === 0) { throw new NotFoundError('No streams found'); } - const playableStream = await validatePlayableStream(embedOutput.stream[0], ops); + const playableStream = await validatePlayableStream(embedOutput.stream[0], ops, embed.embedId); if (!playableStream) throw new NotFoundError('No streams found'); embedOutput.stream = [playableStream]; } catch (error) { diff --git a/src/utils/valid.ts b/src/utils/valid.ts index fb9ef0f..e4ea664 100644 --- a/src/utils/valid.ts +++ b/src/utils/valid.ts @@ -1,7 +1,10 @@ +import { warezcdnembedMp4Scraper } from '@/providers/embeds/warezcdn/mp4'; import { Stream } from '@/providers/streams'; import { IndividualEmbedRunnerOptions } from '@/runners/individualRunner'; import { ProviderRunnerOptions } from '@/runners/runner'; +const SKIP_VALIDATION_CHECK_IDS = [warezcdnembedMp4Scraper.id]; + export function isValidStream(stream: Stream | undefined): boolean { if (!stream) return false; if (stream.type === 'hls') { @@ -21,7 +24,10 @@ export function isValidStream(stream: Stream | undefined): boolean { export async function validatePlayableStream( stream: Stream, ops: ProviderRunnerOptions | IndividualEmbedRunnerOptions, + sourcererId: string, ): Promise { + if (SKIP_VALIDATION_CHECK_IDS.includes(sourcererId)) return stream; + if (stream.type === 'hls') { const result = await ops.proxiedFetcher.full(stream.playlist, { method: 'GET', @@ -63,8 +69,11 @@ export async function validatePlayableStream( export async function validatePlayableStreams( streams: Stream[], ops: ProviderRunnerOptions | IndividualEmbedRunnerOptions, + sourcererId: string, ): Promise { - return (await Promise.all(streams.map((stream) => validatePlayableStream(stream, ops)))).filter( + if (SKIP_VALIDATION_CHECK_IDS.includes(sourcererId)) return streams; + + return (await Promise.all(streams.map((stream) => validatePlayableStream(stream, ops, sourcererId)))).filter( (v) => v !== null, ) as Stream[]; }