diff --git a/src/providers/all.ts b/src/providers/all.ts index e32790e..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'; @@ -40,6 +41,7 @@ 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'; @@ -62,6 +64,8 @@ export function gatherAllSources(): Array { hdRezkaScraper, primewireScraper, warezcdnScraper, + insertunitScraper, + soaperTvScraper, ]; } 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/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; +}