diff --git a/src/providers/all.ts b/src/providers/all.ts index 6ac4f00..854cef4 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -8,12 +8,14 @@ import { mixdropScraper } from '@/providers/embeds/mixdrop'; import { mp4uploadScraper } from '@/providers/embeds/mp4upload'; import { streambucketScraper } from '@/providers/embeds/streambucket'; import { streamsbScraper } from '@/providers/embeds/streamsb'; +import { turbovidScraper } from '@/providers/embeds/turbovid'; import { upcloudScraper } from '@/providers/embeds/upcloud'; import { upstreamScraper } from '@/providers/embeds/upstream'; import { vidsrcembedScraper } from '@/providers/embeds/vidsrc'; import { vTubeScraper } from '@/providers/embeds/vtube'; import { astraScraper, novaScraper } from '@/providers/embeds/whvx'; import { autoembedScraper } from '@/providers/sources/autoembed'; +import { catflixScraper } from '@/providers/sources/catflix'; import { flixhqScraper } from '@/providers/sources/flixhq/index'; import { goMoviesScraper } from '@/providers/sources/gomovies/index'; import { insertunitScraper } from '@/providers/sources/insertunit'; @@ -67,6 +69,7 @@ import { warezcdnScraper } from './sources/warezcdn'; export function gatherAllSources(): Array { // all sources are gathered here return [ + catflixScraper, flixhqScraper, remotestreamScraper, kissAsianScraper, @@ -134,6 +137,7 @@ export function gatherAllEmbeds(): Array { autoembedBengaliScraper, autoembedTamilScraper, autoembedTeluguScraper, + turbovidScraper, novaScraper, astraScraper, ]; diff --git a/src/providers/embeds/turbovid.ts b/src/providers/embeds/turbovid.ts new file mode 100644 index 0000000..7f7e3fc --- /dev/null +++ b/src/providers/embeds/turbovid.ts @@ -0,0 +1,79 @@ +import { makeEmbed } from '@/providers/base'; + +// Thanks to Paradox_77 for helping with the decryption +function hexToChar(hex: string): string { + return String.fromCharCode(parseInt(hex, 16)); +} + +function decrypt(data: string, key: string): string { + const formatedData = data.match(/../g)?.map(hexToChar).join('') || ''; + return formatedData + .split('') + .map((char, i) => String.fromCharCode(char.charCodeAt(0) ^ key.charCodeAt(i % key.length))) + .join(''); +} + +export const turbovidScraper = makeEmbed({ + id: 'turbovid', + name: 'Turbovid', + rank: 122, + async scrape(ctx) { + const baseUrl = new URL(ctx.url).origin; + const embedPage = await ctx.proxiedFetcher(ctx.url); + + ctx.progress(30); + + // the whitespace is for future-proofing the regex a bit + const apkey = embedPage.match(/const\s+apkey\s*=\s*"(.*?)";/)?.[1]; + const xxid = embedPage.match(/const\s+xxid\s*=\s*"(.*?)";/)?.[1]; + + if (!apkey || !xxid) throw new Error('Failed to get required values'); + + // json isn't parsed by proxiedFetcher due to content-type being text/html + const juiceKey = JSON.parse( + await ctx.proxiedFetcher('/api/cucked/juice_key', { + baseUrl, + headers: { + referer: ctx.url, + }, + }), + ).juice; + + if (!juiceKey) throw new Error('Failed to fetch the key'); + + ctx.progress(60); + + const data = JSON.parse( + await ctx.proxiedFetcher('/api/cucked/the_juice/', { + baseUrl, + query: { + [apkey]: xxid, + }, + headers: { + referer: ctx.url, + }, + }), + ).data; + + if (!data) throw new Error('Failed to fetch required data'); + + ctx.progress(90); + + const playlist = decrypt(data, juiceKey); + + return { + stream: [ + { + type: 'hls', + id: 'primary', + playlist, + headers: { + referer: baseUrl, + }, + flags: [], + captions: [], + }, + ], + }; + }, +}); diff --git a/src/providers/sources/catflix.ts b/src/providers/sources/catflix.ts new file mode 100644 index 0000000..03c9456 --- /dev/null +++ b/src/providers/sources/catflix.ts @@ -0,0 +1,74 @@ +import { load } from 'cheerio'; + +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { compareMedia } from '@/utils/compare'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const baseUrl = 'https://catflix.su'; + +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const searchPage = await ctx.proxiedFetcher('/', { + baseUrl, + query: { + s: ctx.media.title, + }, + }); + + ctx.progress(40); + + const $search = load(searchPage); + const searchResults: { title: string; year?: number | undefined; url: string }[] = []; + + $search('li').each((_, element) => { + const title = $search(element).find('h2').first().text().trim(); + // the year is always present, but I sitll decided to make it nullable since the impl isn't as future-proof + const year = Number($search(element).find('.text-xs > span').eq(1).text().trim()) || undefined; + const url = $search(element).find('a').attr('href'); + + if (!title || !url) return; + + searchResults.push({ title, year, url }); + }); + + let watchPageUrl = searchResults.find((x) => x && compareMedia(ctx.media, x.title, x.year))?.url; + if (!watchPageUrl) throw new NotFoundError('No watchable item found'); + + ctx.progress(60); + + if (ctx.media.type === 'show') { + const match = watchPageUrl.match(/\/series\/([^/]+)\/?/); + if (!match) throw new Error('Failed to parse watch page url'); + watchPageUrl = watchPageUrl.replace( + `/series/${match[1]}`, + `/episode/${match[1]}-${ctx.media.season.number}x${ctx.media.episode.number}`, + ); + } + + const watchPage = load(await ctx.proxiedFetcher(watchPageUrl)); + + ctx.progress(80); + + const url = watchPage('iframe').first().attr('src'); // I couldn't think of a better way + if (!url) throw new Error('Failed to find embed url'); + + ctx.progress(90); + + return { + embeds: [ + { + embedId: 'turbovid', + url, + }, + ], + }; +} + +export const catflixScraper = makeSourcerer({ + id: 'catflix', + name: 'Catflix', + rank: 122, + flags: [], + scrapeMovie: comboScraper, + scrapeShow: comboScraper, +}); diff --git a/src/providers/sources/nepu/index.ts b/src/providers/sources/nepu/index.ts index 0e19721..f3be5a1 100644 --- a/src/providers/sources/nepu/index.ts +++ b/src/providers/sources/nepu/index.ts @@ -1,5 +1,6 @@ import { load } from 'cheerio'; +import { flags } from '@/entrypoint/utils/targets'; import { SourcererOutput, makeSourcerer } from '@/providers/base'; import { compareTitle } from '@/utils/compare'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; @@ -7,8 +8,8 @@ import { NotFoundError } from '@/utils/errors'; import { SearchResults } from './types'; -const nepuBase = 'https://nepu.to'; -const nepuReferer = `${nepuBase}/`; +const nepuBase = 'https://nepu.io'; +const nepuReferer = 'https://nepu.to'; const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => { const searchResultRequest = await ctx.proxiedFetcher('/ajax/posts', { @@ -63,11 +64,11 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => captions: [], playlist: streamUrl[1], type: 'hls', - flags: [], headers: { - Origin: nepuBase, - Referer: nepuReferer, + Origin: nepuReferer, + Referer: `${nepuReferer}/`, }, + flags: [], }, ], } as SourcererOutput; @@ -77,8 +78,8 @@ export const nepuScraper = makeSourcerer({ id: 'nepu', name: 'Nepu', rank: 80, - flags: [], disabled: true, + flags: [], scrapeMovie: universalScraper, scrapeShow: universalScraper, });