From 4be2da76ba4158fd01bbfa6c0f0ef88c2ecc5785 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 23 Dec 2023 18:57:28 +0100 Subject: [PATCH 1/3] Split superstream into showbox and febbox. add febbox HLS scraper --- src/providers/all.ts | 12 +- src/providers/embeds/febBox.ts | 74 ----------- src/providers/embeds/febbox/common.ts | 8 ++ src/providers/embeds/febbox/hls.ts | 53 ++++++++ src/providers/embeds/febbox/mp4.ts | 51 +++++++ .../febbox/qualities.ts} | 3 +- .../febbox}/subtitles.ts | 5 +- .../sources/{superstream => showbox}/LICENSE | 0 .../{superstream => showbox}/common.ts | 2 + .../{superstream => showbox}/crypto.ts | 0 src/providers/sources/showbox/index.ts | 124 ++++++++++-------- .../{superstream => showbox}/sendRequest.ts | 0 src/providers/sources/superstream/index.ts | 106 --------------- 13 files changed, 189 insertions(+), 249 deletions(-) delete mode 100644 src/providers/embeds/febBox.ts create mode 100644 src/providers/embeds/febbox/common.ts create mode 100644 src/providers/embeds/febbox/hls.ts create mode 100644 src/providers/embeds/febbox/mp4.ts rename src/providers/{sources/superstream/getStreamQualities.ts => embeds/febbox/qualities.ts} (93%) rename src/providers/{sources/superstream => embeds/febbox}/subtitles.ts (92%) rename src/providers/sources/{superstream => showbox}/LICENSE (100%) rename src/providers/sources/{superstream => showbox}/common.ts (93%) rename src/providers/sources/{superstream => showbox}/crypto.ts (100%) rename src/providers/sources/{superstream => showbox}/sendRequest.ts (100%) delete mode 100644 src/providers/sources/superstream/index.ts 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], - }, - }; - }, -}); From af6ede4a397f73dd93fcfe55867da1405a57713b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 23 Dec 2023 19:55:28 +0100 Subject: [PATCH 2/3] add show support to febbox and add captions --- src/providers/embeds/febbox/common.ts | 16 ++++++ src/providers/embeds/febbox/fileList.ts | 69 ++++++++++++++++++++++++ src/providers/embeds/febbox/hls.ts | 54 +++++++++---------- src/providers/embeds/febbox/mp4.ts | 1 + src/providers/embeds/febbox/qualities.ts | 3 +- src/providers/sources/showbox/index.ts | 43 +++++---------- src/providers/streams.ts | 2 +- 7 files changed, 124 insertions(+), 64 deletions(-) create mode 100644 src/providers/embeds/febbox/fileList.ts diff --git a/src/providers/embeds/febbox/common.ts b/src/providers/embeds/febbox/common.ts index ef0288f..2003120 100644 --- a/src/providers/embeds/febbox/common.ts +++ b/src/providers/embeds/febbox/common.ts @@ -1,3 +1,5 @@ +import { MediaTypes } from '@/main/media'; + export const febBoxBase = `https://www.febbox.com`; export interface FebboxFileList { @@ -5,4 +7,18 @@ export interface FebboxFileList { ext: string; fid: number; oss_fid: number; + is_dir: 0 | 1; +} + +export function parseInput(url: string) { + const [type, id, seasonId, episodeId] = url.slice(1).split('/'); + const season = seasonId ? parseInt(seasonId, 10) : undefined; + const episode = episodeId ? parseInt(episodeId, 10) : undefined; + + return { + type: type as MediaTypes, + id, + season, + episode, + }; } diff --git a/src/providers/embeds/febbox/fileList.ts b/src/providers/embeds/febbox/fileList.ts new file mode 100644 index 0000000..b0c03fb --- /dev/null +++ b/src/providers/embeds/febbox/fileList.ts @@ -0,0 +1,69 @@ +import { MediaTypes } from '@/main/media'; +import { FebboxFileList, febBoxBase } from '@/providers/embeds/febbox/common'; +import { EmbedScrapeContext } from '@/utils/context'; + +export async function getFileList( + ctx: EmbedScrapeContext, + shareKey: string, + parentId?: number, +): Promise { + const query: Record = { + share_key: shareKey, + pwd: '', + }; + if (parentId) { + query.parent_id = parentId.toString(); + query.page = '1'; + } + + 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, + }); + + return streams.data?.file_list ?? []; +} + +function isValidStream(file: FebboxFileList): boolean { + return file.ext === 'mp4' || file.ext === 'mkv'; +} + +export async function getStreams( + ctx: EmbedScrapeContext, + shareKey: string, + type: MediaTypes, + season?: number, + episode?: number, +): Promise { + const streams = await getFileList(ctx, shareKey); + + if (type === 'show') { + const seasonFolder = streams.find((v) => { + if (!v.is_dir) return false; + return v.file_name.toLowerCase() === `season ${season}`; + }); + if (!seasonFolder) return []; + + const episodes = await getFileList(ctx, shareKey, seasonFolder.fid); + const s = season?.toString() ?? '0'; + const e = episode?.toString() ?? '0'; + const episodeRegex = new RegExp(`[Ss]0*${s}[Ee]0*${e}`); + return episodes + .filter((file) => { + if (file.is_dir) return false; + const match = file.file_name.match(episodeRegex); + if (!match) return false; + return true; + }) + .filter(isValidStream); + } + + return streams.filter((v) => !v.is_dir).filter(isValidStream); +} diff --git a/src/providers/embeds/febbox/hls.ts b/src/providers/embeds/febbox/hls.ts index 64006ca..70eb323 100644 --- a/src/providers/embeds/febbox/hls.ts +++ b/src/providers/embeds/febbox/hls.ts @@ -1,7 +1,10 @@ +import { MediaTypes } from '@/main/media'; import { flags } from '@/main/targets'; import { makeEmbed } from '@/providers/base'; -import { FebboxFileList, febBoxBase } from '@/providers/embeds/febbox/common'; -import { EmbedScrapeContext } from '@/utils/context'; +import { parseInput } from '@/providers/embeds/febbox/common'; +import { getStreams } from '@/providers/embeds/febbox/fileList'; +import { getSubtitles } from '@/providers/embeds/febbox/subtitles'; +import { showboxBase } from '@/providers/sources/showbox/common'; // structure: https://www.febbox.com/share/ export function extractShareKey(url: string): string { @@ -9,44 +12,35 @@ export function extractShareKey(url: string): string { 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'); + const { type, id, season, episode } = parseInput(ctx.url); + const sharelinkResult = await ctx.proxiedFetcher<{ + data?: { link?: string }; + }>('/index/share_link', { + baseUrl: showboxBase, + query: { + id, + type: type === 'movie' ? '1' : '2', + }, + }); + if (!sharelinkResult?.data?.link) throw new Error('No embed url found'); + ctx.progress(30); + const shareKey = extractShareKey(sharelinkResult.data.link); + const fileList = await getStreams(ctx, shareKey, type, season, episode); + const firstStream = fileList[0]; + if (!firstStream) throw new Error('No playable mp4 stream found'); + ctx.progress(70); return { stream: { type: 'hls', flags: [flags.NO_CORS], - captions: [], - playlist: `https://www.febbox.com/hls/main/${firstMp4.oss_fid}.m3u8`, + captions: await getSubtitles(ctx, id, firstStream.fid, type as MediaTypes, season, episode), + playlist: `https://www.febbox.com/hls/main/${firstStream.oss_fid}.m3u8`, }, }; }, diff --git a/src/providers/embeds/febbox/mp4.ts b/src/providers/embeds/febbox/mp4.ts index 3405734..30e48f0 100644 --- a/src/providers/embeds/febbox/mp4.ts +++ b/src/providers/embeds/febbox/mp4.ts @@ -38,6 +38,7 @@ export const febboxMp4Scraper = makeEmbed({ const { qualities, fid } = await getStreamQualities(ctx, apiQuery); if (fid === undefined) throw new Error('No streamable file found'); + ctx.progress(70); return { stream: { diff --git a/src/providers/embeds/febbox/qualities.ts b/src/providers/embeds/febbox/qualities.ts index cb80a9d..54f8866 100644 --- a/src/providers/embeds/febbox/qualities.ts +++ b/src/providers/embeds/febbox/qualities.ts @@ -2,11 +2,10 @@ import { sendRequest } from '@/providers/sources/showbox/sendRequest'; import { StreamFile } from '@/providers/streams'; import { ScrapeContext } from '@/utils/context'; -const allowedQualities = ['360', '480', '720', '1080']; +const allowedQualities = ['360', '480', '720', '1080', '4k']; export async function getStreamQualities(ctx: ScrapeContext, apiQuery: object) { const mediaRes: { list: { path: string; quality: string; fid?: number }[] } = (await sendRequest(ctx, apiQuery)).data; - ctx.progress(66); const qualityMap = mediaRes.list .filter((file) => allowedQualities.includes(file.quality.replace('p', ''))) diff --git a/src/providers/sources/showbox/index.ts b/src/providers/sources/showbox/index.ts index 2358993..94164ee 100644 --- a/src/providers/sources/showbox/index.ts +++ b/src/providers/sources/showbox/index.ts @@ -2,7 +2,6 @@ import { flags } from '@/main/targets'; 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'; @@ -19,46 +18,28 @@ async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promis }; const searchRes = (await sendRequest(ctx, searchQuery, true)).data.list; - ctx.progress(33); + ctx.progress(50); 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, + embeds: [ + { + embedId: febboxHlsScraper.id, + url: `/${ctx.media.type}/${id}/${season}/${episode}`, + }, + { + embedId: febboxMp4Scraper.id, + url: `/${ctx.media.type}/${id}/${season}/${episode}`, + }, + ], }; } diff --git a/src/providers/streams.ts b/src/providers/streams.ts index 34863dd..1ba4c9a 100644 --- a/src/providers/streams.ts +++ b/src/providers/streams.ts @@ -7,7 +7,7 @@ export type StreamFile = { headers?: Record; }; -export type Qualities = 'unknown' | '360' | '480' | '720' | '1080'; +export type Qualities = 'unknown' | '360' | '480' | '720' | '1080' | '4k'; export type FileBasedStream = { type: 'file'; From b89602b87ee0215cff8217de87dbebfd0faccf91 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 23 Dec 2023 23:41:36 +0100 Subject: [PATCH 3/3] rename to parseInputUrl --- src/providers/embeds/febbox/common.ts | 2 +- src/providers/embeds/febbox/hls.ts | 4 ++-- src/providers/embeds/febbox/mp4.ts | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/providers/embeds/febbox/common.ts b/src/providers/embeds/febbox/common.ts index 2003120..4348c25 100644 --- a/src/providers/embeds/febbox/common.ts +++ b/src/providers/embeds/febbox/common.ts @@ -10,7 +10,7 @@ export interface FebboxFileList { is_dir: 0 | 1; } -export function parseInput(url: string) { +export function parseInputUrl(url: string) { const [type, id, seasonId, episodeId] = url.slice(1).split('/'); const season = seasonId ? parseInt(seasonId, 10) : undefined; const episode = episodeId ? parseInt(episodeId, 10) : undefined; diff --git a/src/providers/embeds/febbox/hls.ts b/src/providers/embeds/febbox/hls.ts index 70eb323..58478ca 100644 --- a/src/providers/embeds/febbox/hls.ts +++ b/src/providers/embeds/febbox/hls.ts @@ -1,7 +1,7 @@ import { MediaTypes } from '@/main/media'; import { flags } from '@/main/targets'; import { makeEmbed } from '@/providers/base'; -import { parseInput } from '@/providers/embeds/febbox/common'; +import { parseInputUrl } from '@/providers/embeds/febbox/common'; import { getStreams } from '@/providers/embeds/febbox/fileList'; import { getSubtitles } from '@/providers/embeds/febbox/subtitles'; import { showboxBase } from '@/providers/sources/showbox/common'; @@ -17,7 +17,7 @@ export const febboxHlsScraper = makeEmbed({ name: 'Febbox (HLS)', rank: 160, async scrape(ctx) { - const { type, id, season, episode } = parseInput(ctx.url); + const { type, id, season, episode } = parseInputUrl(ctx.url); const sharelinkResult = await ctx.proxiedFetcher<{ data?: { link?: string }; }>('/index/share_link', { diff --git a/src/providers/embeds/febbox/mp4.ts b/src/providers/embeds/febbox/mp4.ts index 30e48f0..086f191 100644 --- a/src/providers/embeds/febbox/mp4.ts +++ b/src/providers/embeds/febbox/mp4.ts @@ -1,6 +1,6 @@ -import { MediaTypes } from '@/main/media'; import { flags } from '@/main/targets'; import { makeEmbed } from '@/providers/base'; +import { parseInputUrl } from '@/providers/embeds/febbox/common'; import { getStreamQualities } from '@/providers/embeds/febbox/qualities'; import { getSubtitles } from '@/providers/embeds/febbox/subtitles'; @@ -9,9 +9,7 @@ export const febboxMp4Scraper = makeEmbed({ 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; + const { type, id, season, episode } = parseInputUrl(ctx.url); let apiQuery: object | null = null; if (type === 'movie') { @@ -42,7 +40,7 @@ export const febboxMp4Scraper = makeEmbed({ return { stream: { - captions: await getSubtitles(ctx, id, fid, type as MediaTypes, episode, season), + captions: await getSubtitles(ctx, id, fid, type, episode, season), qualities, type: 'file', flags: [flags.NO_CORS],