diff --git a/src/providers/all.ts b/src/providers/all.ts index 847ebf1..d1e7885 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -1,5 +1,6 @@ import { Embed, Sourcerer } from '@/providers/base'; -import { febBoxScraper } from '@/providers/embeds/febBox'; +import { febboxHlsScraper } from '@/providers/embeds/febbox/hls'; +import { febboxMp4Scraper } from '@/providers/embeds/febbox/mp4'; import { mixdropScraper } from '@/providers/embeds/mixdrop'; import { mp4uploadScraper } from '@/providers/embeds/mp4upload'; import { streamsbScraper } from '@/providers/embeds/streamsb'; @@ -10,12 +11,11 @@ import { goMoviesScraper } from '@/providers/sources/gomovies/index'; import { kissAsianScraper } from '@/providers/sources/kissasian/index'; import { lookmovieScraper } from '@/providers/sources/lookmovie'; import { remotestreamScraper } from '@/providers/sources/remotestream'; -import { superStreamScraper } from '@/providers/sources/superstream/index'; +import { showboxScraper } from '@/providers/sources/showbox/index'; import { zoechipScraper } from '@/providers/sources/zoechip'; import { smashyStreamDScraper } from './embeds/smashystream/dued'; import { smashyStreamFScraper } from './embeds/smashystream/video1'; -import { showBoxScraper } from './sources/showbox'; import { smashyStreamScraper } from './sources/smashystream'; export function gatherAllSources(): Array { @@ -24,11 +24,10 @@ export function gatherAllSources(): Array { flixhqScraper, remotestreamScraper, kissAsianScraper, - superStreamScraper, + showboxScraper, goMoviesScraper, zoechipScraper, lookmovieScraper, - showBoxScraper, smashyStreamScraper, ]; } @@ -40,7 +39,8 @@ export function gatherAllEmbeds(): Array { mp4uploadScraper, streamsbScraper, upstreamScraper, - febBoxScraper, + febboxMp4Scraper, + febboxHlsScraper, mixdropScraper, smashyStreamFScraper, smashyStreamDScraper, diff --git a/src/providers/embeds/febBox.ts b/src/providers/embeds/febBox.ts deleted file mode 100644 index 7855745..0000000 --- a/src/providers/embeds/febBox.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { flags } from '@/main/targets'; -import { makeEmbed } from '@/providers/base'; -import { StreamFile } from '@/providers/streams'; -import { NotFoundError } from '@/utils/errors'; - -const febBoxBase = `https://www.febbox.com`; - -const allowedQualities = ['360', '480', '720', '1080']; - -export const febBoxScraper = makeEmbed({ - id: 'febbox', - name: 'FebBox', - rank: 160, - async scrape(ctx) { - const shareKey = ctx.url.split('/')[4]; - const streams = await ctx.proxiedFetcher<{ - data?: { - file_list?: { - fid?: string; - }[]; - }; - }>('/file/file_share_list', { - headers: { - 'accept-language': 'en', // without this header, the request is marked as a webscraper - }, - baseUrl: febBoxBase, - query: { - share_key: shareKey, - pwd: '', - }, - }); - - const fid = streams?.data?.file_list?.[0]?.fid; - if (!fid) throw new NotFoundError('no result found'); - - const formParams = new URLSearchParams(); - formParams.append('fid', fid); - formParams.append('share_key', shareKey); - - const player = await ctx.proxiedFetcher('/file/player', { - baseUrl: febBoxBase, - body: formParams, - method: 'POST', - headers: { - 'accept-language': 'en', // without this header, the request is marked as a webscraper - }, - }); - - const sourcesMatch = player?.match(/var sources = (\[[^\]]+\]);/); - const qualities = sourcesMatch ? JSON.parse(sourcesMatch[0].replace('var sources = ', '').replace(';', '')) : null; - - const embedQualities: Record = {}; - - qualities.forEach((quality: { file: string; label: string }) => { - const normalizedLabel = quality.label.toLowerCase().replace('p', ''); - if (allowedQualities.includes(normalizedLabel)) { - if (!quality.file) return; - embedQualities[normalizedLabel] = { - type: 'mp4', - url: quality.file, - }; - } - }); - - return { - stream: { - type: 'file', - captions: [], - flags: [flags.NO_CORS], - qualities: embedQualities, - }, - }; - }, -}); diff --git a/src/providers/embeds/febbox/common.ts b/src/providers/embeds/febbox/common.ts new file mode 100644 index 0000000..ef0288f --- /dev/null +++ b/src/providers/embeds/febbox/common.ts @@ -0,0 +1,8 @@ +export const febBoxBase = `https://www.febbox.com`; + +export interface FebboxFileList { + file_name: string; + ext: string; + fid: number; + oss_fid: number; +} diff --git a/src/providers/embeds/febbox/hls.ts b/src/providers/embeds/febbox/hls.ts new file mode 100644 index 0000000..64006ca --- /dev/null +++ b/src/providers/embeds/febbox/hls.ts @@ -0,0 +1,53 @@ +import { flags } from '@/main/targets'; +import { makeEmbed } from '@/providers/base'; +import { FebboxFileList, febBoxBase } from '@/providers/embeds/febbox/common'; +import { EmbedScrapeContext } from '@/utils/context'; + +// structure: https://www.febbox.com/share/ +export function extractShareKey(url: string): string { + const parsedUrl = new URL(url); + const shareKey = parsedUrl.pathname.split('/')[2]; + return shareKey; +} + +export async function getFileList(ctx: EmbedScrapeContext, shareKey: string): Promise { + const streams = await ctx.proxiedFetcher<{ + data?: { + file_list?: FebboxFileList[]; + }; + }>('/file/file_share_list', { + headers: { + 'accept-language': 'en', // without this header, the request is marked as a webscraper + }, + baseUrl: febBoxBase, + query: { + share_key: shareKey, + pwd: '', + }, + }); + + return streams.data?.file_list ?? []; +} + +export const febboxHlsScraper = makeEmbed({ + id: 'febbox-hls', + name: 'Febbox (HLS)', + rank: 160, + async scrape(ctx) { + const shareKey = extractShareKey(ctx.url); + const fileList = await getFileList(ctx, shareKey); + const firstMp4 = fileList.find((v) => v.ext === 'mp4'); + // TODO support TV, file list is gotten differently + // TODO support subtitles with getSubtitles + if (!firstMp4) throw new Error('No playable mp4 stream found'); + + return { + stream: { + type: 'hls', + flags: [flags.NO_CORS], + captions: [], + playlist: `https://www.febbox.com/hls/main/${firstMp4.oss_fid}.m3u8`, + }, + }; + }, +}); diff --git a/src/providers/embeds/febbox/mp4.ts b/src/providers/embeds/febbox/mp4.ts new file mode 100644 index 0000000..3405734 --- /dev/null +++ b/src/providers/embeds/febbox/mp4.ts @@ -0,0 +1,51 @@ +import { MediaTypes } from '@/main/media'; +import { flags } from '@/main/targets'; +import { makeEmbed } from '@/providers/base'; +import { getStreamQualities } from '@/providers/embeds/febbox/qualities'; +import { getSubtitles } from '@/providers/embeds/febbox/subtitles'; + +export const febboxMp4Scraper = makeEmbed({ + id: 'febbox-mp4', + name: 'Febbox (MP4)', + rank: 190, + async scrape(ctx) { + const [type, id, seasonId, episodeId] = ctx.url.slice(1).split('/'); + const season = seasonId ? parseInt(seasonId, 10) : undefined; + const episode = episodeId ? parseInt(episodeId, 10) : undefined; + let apiQuery: object | null = null; + + if (type === 'movie') { + apiQuery = { + uid: '', + module: 'Movie_downloadurl_v3', + mid: id, + oss: '1', + group: '', + }; + } else if (type === 'show') { + apiQuery = { + uid: '', + module: 'TV_downloadurl_v3', + tid: id, + season, + episode, + oss: '1', + group: '', + }; + } + + if (!apiQuery) throw Error('Incorrect type'); + + const { qualities, fid } = await getStreamQualities(ctx, apiQuery); + if (fid === undefined) throw new Error('No streamable file found'); + + return { + stream: { + captions: await getSubtitles(ctx, id, fid, type as MediaTypes, episode, season), + qualities, + type: 'file', + flags: [flags.NO_CORS], + }, + }; + }, +}); diff --git a/src/providers/sources/superstream/getStreamQualities.ts b/src/providers/embeds/febbox/qualities.ts similarity index 93% rename from src/providers/sources/superstream/getStreamQualities.ts rename to src/providers/embeds/febbox/qualities.ts index 5e82b4c..cb80a9d 100644 --- a/src/providers/sources/superstream/getStreamQualities.ts +++ b/src/providers/embeds/febbox/qualities.ts @@ -1,8 +1,7 @@ +import { sendRequest } from '@/providers/sources/showbox/sendRequest'; import { StreamFile } from '@/providers/streams'; import { ScrapeContext } from '@/utils/context'; -import { sendRequest } from './sendRequest'; - const allowedQualities = ['360', '480', '720', '1080']; export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) { diff --git a/src/providers/sources/superstream/subtitles.ts b/src/providers/embeds/febbox/subtitles.ts similarity index 92% rename from src/providers/sources/superstream/subtitles.ts rename to src/providers/embeds/febbox/subtitles.ts index 36be8de..a0394ec 100644 --- a/src/providers/sources/superstream/subtitles.ts +++ b/src/providers/embeds/febbox/subtitles.ts @@ -1,9 +1,8 @@ import { Caption, getCaptionTypeFromUrl, isValidLanguageCode } from '@/providers/captions'; -import { sendRequest } from '@/providers/sources/superstream/sendRequest'; +import { captionsDomains } from '@/providers/sources/showbox/common'; +import { sendRequest } from '@/providers/sources/showbox/sendRequest'; import { ScrapeContext } from '@/utils/context'; -import { captionsDomains } from './common'; - interface CaptionApiResponse { data: { list: { diff --git a/src/providers/sources/superstream/LICENSE b/src/providers/sources/showbox/LICENSE similarity index 100% rename from src/providers/sources/superstream/LICENSE rename to src/providers/sources/showbox/LICENSE diff --git a/src/providers/sources/superstream/common.ts b/src/providers/sources/showbox/common.ts similarity index 93% rename from src/providers/sources/superstream/common.ts rename to src/providers/sources/showbox/common.ts index 6ad1448..b2cf855 100644 --- a/src/providers/sources/superstream/common.ts +++ b/src/providers/sources/showbox/common.ts @@ -12,3 +12,5 @@ export const apiUrls = [ export const appKey = atob('bW92aWVib3g='); export const appId = atob('Y29tLnRkby5zaG93Ym94'); export const captionsDomains = [atob('bWJwaW1hZ2VzLmNodWF4aW4uY29t'), atob('aW1hZ2VzLnNoZWd1Lm5ldA==')]; + +export const showboxBase = 'https://www.showbox.media'; diff --git a/src/providers/sources/superstream/crypto.ts b/src/providers/sources/showbox/crypto.ts similarity index 100% rename from src/providers/sources/superstream/crypto.ts rename to src/providers/sources/showbox/crypto.ts diff --git a/src/providers/sources/showbox/index.ts b/src/providers/sources/showbox/index.ts index 0a2ddbb..2358993 100644 --- a/src/providers/sources/showbox/index.ts +++ b/src/providers/sources/showbox/index.ts @@ -1,64 +1,72 @@ -import { load } from 'cheerio'; - import { flags } from '@/main/targets'; -import { makeSourcerer } from '@/providers/base'; -import { febBoxScraper } from '@/providers/embeds/febBox'; -import { compareMedia } from '@/utils/compare'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { febboxHlsScraper } from '@/providers/embeds/febbox/hls'; +import { febboxMp4Scraper } from '@/providers/embeds/febbox/mp4'; +import { showboxBase } from '@/providers/sources/showbox/common'; +import { compareTitle } from '@/utils/compare'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; -const showboxBase = `https://www.showbox.media`; +import { sendRequest } from './sendRequest'; -export const showBoxScraper = makeSourcerer({ - id: 'show_box', - name: 'ShowBox', - rank: 20, - disabled: true, +async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + const searchQuery = { + module: 'Search4', + page: '1', + type: 'all', + keyword: ctx.media.title, + pagelimit: '20', + }; + + const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list; + ctx.progress(33); + + const showboxEntry = searchRes.find( + (res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear), + ); + + if (!showboxEntry) throw new NotFoundError('No entry found'); + const id = showboxEntry.id; + + const sharelinkResult = await ctx.proxiedFetcher<{ + data?: { link?: string }; + }>('/index/share_link', { + baseUrl: showboxBase, + query: { + id, + type: ctx.media.type === 'movie' ? '1' : '2', + }, + }); + if (!sharelinkResult?.data?.link) throw new NotFoundError('No embed url found'); + ctx.progress(80); + + const season = ctx.media.type === 'show' ? ctx.media.season.number : ''; + const episode = ctx.media.type === 'show' ? ctx.media.episode.number : ''; + + const embeds = [ + { + embedId: febboxMp4Scraper.id, + url: `/${ctx.media.type}/${id}/${season}/${episode}`, + }, + ]; + + if (sharelinkResult?.data?.link) { + embeds.push({ + embedId: febboxHlsScraper.id, + url: sharelinkResult.data.link, + }); + } + + return { + embeds, + }; +} + +export const showboxScraper = makeSourcerer({ + id: 'showbox', + name: 'Showbox', + rank: 300, flags: [flags.NO_CORS], - async scrapeMovie(ctx) { - const search = await ctx.proxiedFetcher('/search', { - baseUrl: showboxBase, - query: { - keyword: ctx.media.title, - }, - }); - - const searchPage = load(search); - const result = searchPage('.film-name > a') - .toArray() - .map((el) => { - const titleContainer = el.parent?.parent; - if (!titleContainer) return; - const year = searchPage(titleContainer).find('.fdi-item').first().text(); - - return { - title: el.attribs.title, - path: el.attribs.href, - year: !year.includes('SS') ? parseInt(year, 10) : undefined, - }; - }) - .find((v) => v && compareMedia(ctx.media, v.title, v.year ? v.year : undefined)); - - if (!result?.path) throw new NotFoundError('no result found'); - - const febboxResult = await ctx.proxiedFetcher<{ - data?: { link?: string }; - }>('/index/share_link', { - baseUrl: showboxBase, - query: { - id: result.path.split('/')[3], - type: '1', - }, - }); - - if (!febboxResult?.data?.link) throw new NotFoundError('no result found'); - - return { - embeds: [ - { - embedId: febBoxScraper.id, - url: febboxResult.data.link, - }, - ], - }; - }, + scrapeShow: comboScraper, + scrapeMovie: comboScraper, }); diff --git a/src/providers/sources/superstream/sendRequest.ts b/src/providers/sources/showbox/sendRequest.ts similarity index 100% rename from src/providers/sources/superstream/sendRequest.ts rename to src/providers/sources/showbox/sendRequest.ts diff --git a/src/providers/sources/superstream/index.ts b/src/providers/sources/superstream/index.ts deleted file mode 100644 index 173f849..0000000 --- a/src/providers/sources/superstream/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { flags } from '@/main/targets'; -import { makeSourcerer } from '@/providers/base'; -import { getSubtitles } from '@/providers/sources/superstream/subtitles'; -import { compareTitle } from '@/utils/compare'; -import { NotFoundError } from '@/utils/errors'; - -import { getStreamQualities } from './getStreamQualities'; -import { sendRequest } from './sendRequest'; - -export const superStreamScraper = makeSourcerer({ - id: 'superstream', - name: 'Superstream', - rank: 300, - flags: [flags.NO_CORS], - async scrapeShow(ctx) { - const searchQuery = { - module: 'Search4', - page: '1', - type: 'all', - keyword: ctx.media.title, - pagelimit: '20', - }; - - const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list; - ctx.progress(33); - - const superstreamEntry = searchRes.find( - (res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear), - ); - - if (!superstreamEntry) throw new NotFoundError('No entry found'); - const superstreamId = superstreamEntry.id; - - // Fetch requested episode - const apiQuery = { - uid: '', - module: 'TV_downloadurl_v3', - tid: superstreamId, - season: ctx.media.season.number, - episode: ctx.media.episode.number, - oss: '1', - group: '', - }; - - const { qualities, fid } = await getStreamQualities(ctx, apiQuery); - if (fid === undefined) throw new NotFoundError('No streamable file found'); - - return { - embeds: [], - stream: { - captions: await getSubtitles( - ctx, - superstreamId, - fid, - 'show', - ctx.media.episode.number, - ctx.media.season.number, - ), - qualities, - type: 'file', - flags: [flags.NO_CORS], - }, - }; - }, - async scrapeMovie(ctx) { - const searchQuery = { - module: 'Search4', - page: '1', - type: 'all', - keyword: ctx.media.title, - pagelimit: '20', - }; - - const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list; - ctx.progress(33); - - const superstreamEntry = searchRes.find( - (res: any) => compareTitle(res.title, ctx.media.title) && res.year === Number(ctx.media.releaseYear), - ); - - if (!superstreamEntry) throw new NotFoundError('No entry found'); - const superstreamId = superstreamEntry.id; - - // Fetch requested episode - const apiQuery = { - uid: '', - module: 'Movie_downloadurl_v3', - mid: superstreamId, - oss: '1', - group: '', - }; - - const { qualities, fid } = await getStreamQualities(ctx, apiQuery); - if (fid === undefined) throw new NotFoundError('No streamable file found'); - - return { - embeds: [], - stream: { - captions: await getSubtitles(ctx, superstreamId, fid, 'movie'), - qualities, - type: 'file', - flags: [flags.NO_CORS], - }, - }; - }, -});