From af6ede4a397f73dd93fcfe55867da1405a57713b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 23 Dec 2023 19:55:28 +0100 Subject: [PATCH] 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';