From c8b0c42e59a6cf2c092df0a57a7e6c8f20469133 Mon Sep 17 00:00:00 2001 From: Chris <134489068+jalywn@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:50:17 +0000 Subject: [PATCH] modified: src/providers/all.ts deleted: src/providers/sources/bludclart.ts new file: src/providers/sources/florence/bludclart.ts new file: src/providers/sources/florence/rose.ts --- src/providers/all.ts | 6 +- src/providers/sources/bludclart.ts | 418 -------------------- src/providers/sources/florence/bludclart.ts | 342 ++++++++++++++++ src/providers/sources/florence/rose.ts | 342 ++++++++++++++++ 4 files changed, 687 insertions(+), 421 deletions(-) delete mode 100644 src/providers/sources/bludclart.ts create mode 100644 src/providers/sources/florence/bludclart.ts create mode 100644 src/providers/sources/florence/rose.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index e73cfe2..e3f96c8 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -6,7 +6,8 @@ import { serverMirrorEmbed } from '@/providers/embeds/server-mirrors'; import { turbovidScraper } from '@/providers/embeds/turbovid'; import { upcloudScraper } from '@/providers/embeds/upcloud'; import { autoembedScraper } from '@/providers/sources/autoembed'; -import { bludclartScraper } from '@/providers/sources/bludclart'; +import { bludclartScraper } from '@/providers/sources/florence/bludclart'; +import { roseScraper } from '@/providers/sources/florence/rose'; import { dopeboxEmbeds, dopeboxScraper } from '@/providers/sources/dopebox/index'; import { ee3Scraper } from '@/providers/sources/ee3'; import { fsharetvScraper } from '@/providers/sources/fsharetv'; @@ -98,11 +99,11 @@ import { wecimaScraper } from './sources/wecima'; import { zunimeScraper } from './sources/zunime'; export function gatherAllSources(): Array { - // all sources are gathered here return [ fsOnlineScraper, dopeboxScraper, bludclartScraper, + roseScraper, cuevana3Scraper, ridooMoviesScraper, hdRezkaScraper, @@ -144,7 +145,6 @@ export function gatherAllSources(): Array { } export function gatherAllEmbeds(): Array { - // all embeds are gathered here return [ ...fsOnlineEmbeds, ...dopeboxEmbeds, diff --git a/src/providers/sources/bludclart.ts b/src/providers/sources/bludclart.ts deleted file mode 100644 index b7d22b2..0000000 --- a/src/providers/sources/bludclart.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { load } from 'cheerio'; - -import { flags } from '@/entrypoint/utils/targets'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; -import { NotFoundError } from '@/utils/errors'; - -const PROXY_URLS = [ - 'https://simple-proxy.asteral-ss2.workers.dev', - - 'https://simple-proxy.asteral-ss2.workers.dev', - /**'https://pcors.shipwr3ck.workers.dev', - 'https://pstream-proxy.katelyn-boreham.workers.dev', - 'https://simple-proxy-pstream.mohamdaimn.workers.dev', - 'https://simple-proxy.adzel.workers.dev', - 'https://simple-proxy.agenteg1000.workers.dev', - 'https://simple-proxy.bmk77778888.workers.dev', - 'https://simple-proxy.cacayi.workers.dev', - 'https://simple-proxy.chayokefck.workers.dev', - 'https://simple-proxy.f6zc5kyfjn.workers.dev', - 'https://simple-proxy.febopo2221.workers.dev', - 'https://simple-proxy.garble.workers.dev', - 'https://simple-proxy.hydrovolter.workers.dev', - 'https://simple-proxy.issaoui-said-info.workers.dev', - 'https://simple-proxy.jacobalt.workers.dev', - 'https://simple-proxy.justinbdaludado.workers.dev', - 'https://simple-proxy.kllxcai-2e8.workers.dev', - 'https://simple-proxy.lloydsymonds.workers.dev', - 'https://simple-proxy.quantality.workers.dev', - 'https://simple-proxy.ratshrabies.workers.dev', - 'https://simple-proxy.remuxbay.workers.dev', - 'https://simple-proxy.six666666666666666666-6-6-6-6-6-6-6-6-6-6.workers.dev', - 'https://simple-proxy.spencerjamesdeal.workers.dev', - 'https://simple-proxy.starseekerdude.workers.dev', - 'https://simple-proxy.subhashgottumukkala17.workers.dev', - 'https://simple-proxy.suvigya-goyal.workers.dev', - 'https://simple-proxy.tusharkhandare007.workers.dev', - 'https://simple-proxy.valdezmarcjoshua.workers.dev', - 'https://simple-proxy.ytonlyamit.workers.dev', - 'https://simple-proxy2.tonyvu4913.workers.dev', - 'https://simple-proxyyy.thinner-life-void.workers.dev', - 'https://bruh.jerry5890.workers.dev', - 'https://c719dda0-simple-proxy.sylaxx95.workers.dev',**/ -]; - -const SHOWBOX_FREE_DOMAINS = [ - 'https://www.showbox.media', - 'https://www.showbox.lat', - // showbox.mn is paid -]; - -const FEBBOX_BASE = 'https://www.febbox.com'; -const MEDIA_PROXY_API = 'https://media-proxy.oct-cdn.co/api/fetchMp4'; - -const REQUEST_HEADERS = { - 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', - accept: 'application/json, text/plain, */*', - 'accept-language': 'en-US,en;q=0.5', - 'accept-encoding': 'gzip, deflate, br, zstd', - origin: 'https://watch.bludclart.com', - referer: 'https://watch.bludclart.com/', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'cross-site', -}; - -function proxyUrl(url: string): string { - const randomProxy = PROXY_URLS[Math.floor(Math.random() * PROXY_URLS.length)]; - return `${randomProxy}/?destination=${encodeURIComponent(url)}`; -} - -async function customFetcher(ctx: ShowScrapeContext | MovieScrapeContext, url: string, options?: any): Promise { - const shuffledProxies = [...PROXY_URLS].sort(() => 0.5 - Math.random()); - - for (const proxy of shuffledProxies) { - try { - const proxiedUrl = `${proxy}/?destination=${encodeURIComponent(url)}`; - return await ctx.fetcher(proxiedUrl, options); - } catch (error) {} - } - - throw new Error('All proxies failed'); -} - -async function getShareKey( - ctx: ShowScrapeContext | MovieScrapeContext, - showboxId: string, - mediaType: string, - baseUrl: string, -): Promise { - const typeCode = mediaType === 'tv' ? '2' : '1'; - const shareLinkUrl = `${baseUrl}/index/share_link?id=${showboxId}&type=${typeCode}`; - - try { - const response = await customFetcher(ctx, shareLinkUrl, { - headers: REQUEST_HEADERS, - }); - - const data = response; - - if (typeof data === 'object' && data !== null) { - if ('data' in data && typeof data.data === 'object' && data.data !== null && 'link' in data.data) { - return (data.data as any).link.split('/').pop(); - } - - if ('link' in data) { - return (data as any).link.split('/').pop(); - } - if ('key' in data) { - return (data as any).key; - } - if ('data' in data) { - const val = (data as any).data; - if (typeof val === 'string' && val.includes('febbox.com')) { - return val.split('/').pop() || null; - } - return String(val); - } - } - - return String(data); - } catch (error) { - return null; - } -} - -async function getShowboxId( - ctx: ShowScrapeContext | MovieScrapeContext, - query: string, - _mediaType: string, -): Promise<{ id: string | null; baseUrl: string }> { - const queryLower = query.toLowerCase(); - - for (const baseUrl of SHOWBOX_FREE_DOMAINS) { - const searchUrl = `${baseUrl}/search?keyword=${encodeURIComponent(query)}`; - - try { - const response = await customFetcher(ctx, searchUrl, { - headers: REQUEST_HEADERS, - }); - - const $ = load(response); - const results = $('.film-poster-ahref'); - - if (results.length === 0) continue; - - let exactMatch: string | null = null; - let startsWithMatch: string | null = null; - let firstResult: string | null = null; - - if (results.length > 0) { - firstResult = `${baseUrl}${$(results[0]).attr('href')}`; - } - - for (const result of results.toArray()) { - const title = $(result).attr('title')?.trim()?.toLowerCase(); - const href = $(result).attr('href'); - - if (title === queryLower) { - exactMatch = `${baseUrl}${href}`; - break; - } - if (!startsWithMatch && title?.startsWith(queryLower)) { - startsWithMatch = `${baseUrl}${href}`; - } - } - - const targetUrl = exactMatch || startsWithMatch || firstResult; - - if (targetUrl) { - const detailResponse = await customFetcher(ctx, targetUrl, { - headers: REQUEST_HEADERS, - }); - - const detail$ = load(detailResponse); - const headingLink = detail$('h2.heading-name a'); - - if (headingLink.length > 0) { - const href = headingLink.attr('href'); - const showId = href?.split('/').pop(); - if (showId) { - return { id: showId, baseUrl }; - } - } - - const match = targetUrl.match(/-(\d+)$/); - if (match) { - return { id: match[1], baseUrl }; - } - } - } catch (error) {} - } - - return { id: null, baseUrl: SHOWBOX_FREE_DOMAINS[0] }; -} - -async function getFebboxFileList( - ctx: ShowScrapeContext | MovieScrapeContext, - shareKey: string, - parentId: number = 0, -): Promise { - const pidStr = parentId.toString(); - const listUrl = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${pidStr}`; - - const response = await customFetcher(ctx, listUrl, { - headers: REQUEST_HEADERS, - }); - - return response; -} - -async function findFileFid( - ctx: ShowScrapeContext | MovieScrapeContext, - shareKey: string, - mediaType: string, - season?: number, - episode?: number, -): Promise { - const rootData = await getFebboxFileList(ctx, shareKey); - if (!rootData || !rootData.data || !rootData.data.file_list) { - return null; - } - - const files = rootData.data.file_list; - - if (mediaType === 'movie') { - const videoFiles = files.filter((f: any) => f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())); - if (videoFiles.length === 0) { - return null; - } - - videoFiles.sort( - (a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10), - ); - return videoFiles[0].fid; - } - - if (mediaType === 'tv' && season && episode) { - const seasonFolderName = `Season ${season}`; - const seasonFolderNamePad = `Season ${season.toString().padStart(2, '0')}`; - - let seasonFid: number | null = null; - for (const f of files) { - if (f.is_dir === 1) { - const fname = f.file_name.trim(); - if ( - fname.toLowerCase() === seasonFolderName.toLowerCase() || - fname.toLowerCase() === seasonFolderNamePad.toLowerCase() - ) { - seasonFid = f.fid; - break; - } - } - } - - if (!seasonFid) { - for (const f of files) { - if (f.is_dir === 1) { - const fname = f.file_name.trim(); - const seasonRegex = new RegExp(`S0?${season}\\b`, 'i'); - const seasonOnlyRegex = new RegExp(`\\b${season}\\b.*season`, 'i'); - if (seasonRegex.test(fname) && !seasonOnlyRegex.test(fname)) { - seasonFid = f.fid; - break; - } - } - } - } - - if (!seasonFid) { - return null; - } - - const seasonData = await getFebboxFileList(ctx, shareKey, seasonFid); - if (!seasonData || !seasonData.data || !seasonData.data.file_list) { - return null; - } - - const episodeFiles = seasonData.data.file_list; - - const episodePatterns = [ - new RegExp(`S0?${season}E0?${episode}\\b`, 'i'), - new RegExp(`${season}x0?${episode}\\b`, 'i'), - new RegExp(`E0?${episode}\\b`, 'i'), - new RegExp(`\\b${episode}\\b`, 'i'), - ]; - - for (const f of episodeFiles) { - if (f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())) { - const fname = f.file_name; - for (const pattern of episodePatterns) { - if (pattern.test(fname)) { - return f.fid; - } - } - } - } - } - - return null; -} - -async function getStreamUrl( - ctx: ShowScrapeContext | MovieScrapeContext, - fid: number, - shareKey: string, -): Promise { - const payload = { - fid, - share_key: shareKey, - user_token: null, - }; - - try { - const response = await customFetcher(ctx, MEDIA_PROXY_API, { - method: 'POST', - headers: { - ...REQUEST_HEADERS, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (response && response.sources && response.sources.length > 0) { - let sources = response.sources as { download_url: string; quality: string }[]; - - sources = sources.filter((s) => s.download_url.includes('.m3u8') && s.download_url.includes('hls.shegu.net')); - - if (sources.length === 0) return null; - - const autoQuality = sources.find((s) => s.quality.toUpperCase() === 'AUTO'); - if (autoQuality) return autoQuality.download_url; - - const qualityRank = (q: string): number => { - const quality = q.toUpperCase(); - if (quality.includes('1080')) return 100; - if (quality.includes('720')) return 80; - if (quality === 'ORG') return 60; - if (quality.includes('480')) return 40; - if (quality.includes('360')) return 20; - return 0; - }; - - sources.sort((a: any, b: any) => qualityRank(b.quality) - qualityRank(a.quality)); - return sources[0].download_url; - } - - return null; - } catch (error) { - return null; - } -} - -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - ctx.progress(10); - - const isShow = ctx.media.type === 'show'; - const mediaType = isShow ? 'tv' : 'movie'; - const queryName = (ctx.media.title || '').replace('and', '&'); - - const { id: showboxId, baseUrl } = await getShowboxId(ctx, queryName, mediaType); - if (!showboxId) { - throw new NotFoundError('Could not find Showbox ID on any free domain'); - } - - const shareKey = await getShareKey(ctx, showboxId, mediaType, baseUrl); - if (!shareKey) { - throw new NotFoundError('Could not get Share Key'); - } - - let fid: number | null; - if (isShow) { - const showCtx = ctx as ShowScrapeContext; - fid = await findFileFid(ctx, shareKey, mediaType, showCtx.media.season.number, showCtx.media.episode.number); - } else { - fid = await findFileFid(ctx, shareKey, mediaType); - } - - if (!fid) { - throw new NotFoundError('Could not find file ID'); - } - - ctx.progress(50); - - const streamUrl = await getStreamUrl(ctx, fid, shareKey); - - if (!streamUrl) { - throw new NotFoundError('No stream found'); - } - - ctx.progress(90); - - return { - embeds: [], - stream: [ - { - id: 'primary', - type: 'hls', - playlist: streamUrl, - flags: [flags.CORS_ALLOWED], - captions: [], - headers: { - Referer: FEBBOX_BASE, - }, - }, - ], - }; -} - -export const bludclartScraper = makeSourcerer({ - id: 'bludclart', - name: 'Bludclart 🤝', - rank: 202, - disabled: false, - flags: [flags.CORS_ALLOWED], - scrapeMovie: comboScraper, - scrapeShow: comboScraper, -}); diff --git a/src/providers/sources/florence/bludclart.ts b/src/providers/sources/florence/bludclart.ts new file mode 100644 index 0000000..6a69cc0 --- /dev/null +++ b/src/providers/sources/florence/bludclart.ts @@ -0,0 +1,342 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const PROXY_URLS = [ + 'https://simple-proxy.asteral-ss2.workers.dev', + 'https://pcors.shipwr3ck.workers.dev', + 'https://pstream-proxy.katelyn-boreham.workers.dev', + 'https://simple-proxy-pstream.mohamdaimn.workers.dev', + 'https://simple-proxy.adzel.workers.dev', + 'https://simple-proxy.agenteg1000.workers.dev', + 'https://simple-proxy.bmk77778888.workers.dev', + 'https://simple-proxy.cacayi.workers.dev', + 'https://simple-proxy.chayokefck.workers.dev', + 'https://simple-proxy.f6zc5kyfjn.workers.dev', + 'https://simple-proxy.febopo2221.workers.dev', + 'https://simple-proxy.garble.workers.dev', + 'https://simple-proxy.hydrovolter.workers.dev', + 'https://simple-proxy.issaoui-said-info.workers.dev', + 'https://simple-proxy.jacobalt.workers.dev', + 'https://simple-proxy.justinbdaludado.workers.dev', + 'https://simple-proxy.kllxcai-2e8.workers.dev', + 'https://simple-proxy.lloydsymonds.workers.dev', + 'https://simple-proxy.quantality.workers.dev', + 'https://simple-proxy.ratshrabies.workers.dev', + 'https://simple-proxy.remuxbay.workers.dev', + 'https://simple-proxy.six666666666666666666-6-6-6-6-6-6-6-6-6-6.workers.dev', + 'https://simple-proxy.spencerjamesdeal.workers.dev', + 'https://simple-proxy.starseekerdude.workers.dev', + 'https://simple-proxy.subhashgottumukkala17.workers.dev', + 'https://simple-proxy.suvigya-goyal.workers.dev', + 'https://simple-proxy.tusharkhandare007.workers.dev', + 'https://simple-proxy.valdezmarcjoshua.workers.dev', + 'https://simple-proxy.ytonlyamit.workers.dev', + 'https://simple-proxy2.tonyvu4913.workers.dev', + 'https://simple-proxyyy.thinner-life-void.workers.dev', + 'https://bruh.jerry5890.workers.dev', + 'https://c719dda0-simple-proxy.sylaxx95.workers.dev', +].map((url) => url.trim()); + +const SHOWBOX_BASE = 'https://www.showbox.media'; +const FEBBOX_BASE = 'https://www.febbox.com'; +const MEDIA_PROXY_API = 'https://media-proxy.oct-cdn.co/api/fetchMp4'; + +const REQUEST_HEADERS = { + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', + accept: 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br, zstd', + origin: 'https://watch.bludclart.com', + referer: 'https://watch.bludclart.com/', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', +}; + +async function customFetcher( + ctx: ShowScrapeContext | MovieScrapeContext, + url: string, + options: any = {}, + maxConcurrent = 4, + timeoutMs = 7000, +): Promise { + const shuffled = [...PROXY_URLS].sort(() => 0.5 - Math.random()); + const ac = new AbortController(); + + const fetchWithTimeout = async (proxy: string) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeoutMs); + + try { + const proxied = `${proxy}/?destination=${encodeURIComponent(url)}`; + const res = await ctx.fetcher(proxied, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + ac.abort(); + return res; + } catch (err) { + clearTimeout(id); + throw err; + } + }; + + for (let i = 0; i < shuffled.length; i += maxConcurrent) { + const batch = shuffled.slice(i, i + maxConcurrent); + const promises = batch.map((proxy) => fetchWithTimeout(proxy).catch(() => null)); + + try { + const winner = await Promise.race( + promises.map((p) => p.then((res) => (res !== null ? res : new Promise(() => {})))), + ); + if (winner !== undefined) return winner; + } catch {} + } + + throw new Error('All proxies failed'); +} + +async function getShowboxId( + ctx: ShowScrapeContext | MovieScrapeContext, + query: string, + _mediaType: string, +): Promise { + const searchUrl = `${SHOWBOX_BASE}/search?keyword=${encodeURIComponent(query)}`; + try { + const response = await customFetcher(ctx, searchUrl, { headers: REQUEST_HEADERS }); + const $ = load(response); + const results = $('.film-poster-ahref'); + if (results.length === 0) return null; + + let exactMatch: string | null = null; + let startsWithMatch: string | null = null; + let firstResult: string | null = `${SHOWBOX_BASE}${$(results[0]).attr('href')}`; + + const queryLower = query.toLowerCase(); + for (const result of results.toArray()) { + const title = $(result).attr('title')?.trim()?.toLowerCase(); + const href = $(result).attr('href'); + if (title === queryLower) { + exactMatch = `${SHOWBOX_BASE}${href}`; + break; + } + if (!startsWithMatch && title?.startsWith(queryLower)) { + startsWithMatch = `${SHOWBOX_BASE}${href}`; + } + } + + const targetUrl = exactMatch || startsWithMatch || firstResult; + if (targetUrl) { + const detailResponse = await customFetcher(ctx, targetUrl, { headers: REQUEST_HEADERS }); + const detail$ = load(detailResponse); + const headingLink = detail$('h2.heading-name a'); + if (headingLink.length > 0) { + return headingLink.attr('href')?.split('/').pop() || null; + } + const match = targetUrl.match(/-(\d+)$/); + return match ? match[1] : null; + } + } catch {} + return null; +} + +async function getShareKey( + ctx: ShowScrapeContext | MovieScrapeContext, + showboxId: string, + mediaType: string, +): Promise { + const typeCode = mediaType === 'tv' ? '2' : '1'; + const shareLinkUrl = `${SHOWBOX_BASE}/index/share_link?id=${showboxId}&type=${typeCode}`; + try { + const response = await customFetcher(ctx, shareLinkUrl, { headers: REQUEST_HEADERS }); + if (typeof response === 'object' && response !== null) { + if ('data' in response && response.data && 'link' in response.data) { + return (response.data as any).link.split('/').pop(); + } + if ('link' in response) return (response as any).link.split('/').pop(); + if ('key' in response) return (response as any).key; + if ('data' in response) { + const val = (response as any).data; + if (typeof val === 'string' && val.includes('febbox.com')) { + return val.split('/').pop() || null; + } + return String(val); + } + } + return String(response); + } catch { + return null; + } +} + +async function getFebboxFileList(ctx: any, shareKey: string, parentId = 0) { + const url = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${parentId}`; + return await customFetcher(ctx, url, { headers: REQUEST_HEADERS }); +} + +async function findFileFid( + ctx: ShowScrapeContext | MovieScrapeContext, + shareKey: string, + mediaType: string, + season?: number, + episode?: number, +): Promise { + const rootData = await getFebboxFileList(ctx, shareKey); + if (!rootData?.data?.file_list) return null; + const files = rootData.data.file_list; + + if (mediaType === 'movie') { + const videoFiles = files.filter((f: any) => f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())); + if (videoFiles.length === 0) return null; + videoFiles.sort( + (a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10), + ); + return videoFiles[0].fid; + } + + if (mediaType === 'tv' && season && episode) { + const seasonStr = `Season ${season}`; + const seasonPad = `Season ${season.toString().padStart(2, '0')}`; + let seasonFid: number | null = null; + + for (const f of files) { + if (f.is_dir === 1) { + const name = f.file_name.trim(); + if (name.toLowerCase() === seasonStr.toLowerCase() || name.toLowerCase() === seasonPad.toLowerCase()) { + seasonFid = f.fid; + break; + } + } + } + + if (!seasonFid) { + const seasonRegex = new RegExp(`S0?${season}\\b`, 'i'); + for (const f of files) { + if (f.is_dir === 1) { + const name = f.file_name.trim(); + if (seasonRegex.test(name) && !/\b\d+\b.*season/i.test(name)) { + seasonFid = f.fid; + break; + } + } + } + } + + if (!seasonFid) return null; + const seasonData = await getFebboxFileList(ctx, shareKey, seasonFid); + if (!seasonData?.data?.file_list) return null; + + const epFiles = seasonData.data.file_list; + const patterns = [ + new RegExp(`S0?${season}E0?${episode}\\b`, 'i'), + new RegExp(`${season}x0?${episode}\\b`, 'i'), + new RegExp(`E0?${episode}\\b`, 'i'), + new RegExp(`\\b${episode}\\b`, 'i'), + ]; + + for (const f of epFiles) { + if (f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())) { + const name = f.file_name; + if (patterns.some((p) => p.test(name))) return f.fid; + } + } + } + return null; +} + +async function getStreamUrl( + ctx: ShowScrapeContext | MovieScrapeContext, + fid: number, + shareKey: string, +): Promise { + const payload = { fid, share_key: shareKey, user_token: null }; + try { + const response = await customFetcher(ctx, MEDIA_PROXY_API, { + method: 'POST', + headers: { ...REQUEST_HEADERS, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (response?.sources?.length) { + let sources = response.sources.filter( + (s: any) => s.download_url.includes('.m3u8') && s.download_url.includes('hls.shegu.net'), + ); + if (sources.length === 0) return null; + + const auto = sources.find((s: any) => s.quality.toUpperCase() === 'AUTO'); + if (auto) return auto.download_url; + + const rank = (q: string) => { + const u = q.toUpperCase(); + if (u.includes('1080')) return 100; + if (u.includes('720')) return 80; + if (u === 'ORG') return 60; + if (u.includes('480')) return 40; + if (u.includes('360')) return 20; + return 0; + }; + + sources.sort((a: any, b: any) => rank(b.quality) - rank(a.quality)); + return sources[0].download_url; + } + } catch {} + return null; +} + +async function scraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + ctx.progress(10); + const isShow = ctx.media.type === 'show'; + const mediaType = isShow ? 'tv' : 'movie'; + const queryName = (ctx.media.title || '').replace(/and/gi, '&'); + + const showboxId = await getShowboxId(ctx, queryName, mediaType); + if (!showboxId) throw new NotFoundError('Showbox ID not found'); + + const shareKey = await getShareKey(ctx, showboxId, mediaType); + if (!shareKey) throw new NotFoundError('Share key not found'); + + let fid: number | null; + if (isShow) { + const showCtx = ctx as ShowScrapeContext; + fid = await findFileFid(ctx, shareKey, mediaType, showCtx.media.season.number, showCtx.media.episode.number); + } else { + fid = await findFileFid(ctx, shareKey, mediaType); + } + if (!fid) throw new NotFoundError('File ID not found'); + + ctx.progress(50); + const streamUrl = await getStreamUrl(ctx, fid, shareKey); + if (!streamUrl) throw new NotFoundError('No stream found'); + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'hls', + playlist: streamUrl, + flags: [flags.CORS_ALLOWED], + captions: [], + headers: { + Referer: FEBBOX_BASE, + }, + }, + ], + }; +} + +export const bludclartScraper = makeSourcerer({ + id: 'bludclart', + name: 'Bludclart 🤝', + rank: 202, + disabled: false, + flags: [flags.CORS_ALLOWED], + scrapeMovie: scraper, + scrapeShow: scraper, +}); diff --git a/src/providers/sources/florence/rose.ts b/src/providers/sources/florence/rose.ts new file mode 100644 index 0000000..186be8c --- /dev/null +++ b/src/providers/sources/florence/rose.ts @@ -0,0 +1,342 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; + +const PROXY_URLS = [ + 'https://simple-proxy.asteral-ss2.workers.dev', + 'https://pcors.shipwr3ck.workers.dev', + 'https://pstream-proxy.katelyn-boreham.workers.dev', + 'https://simple-proxy-pstream.mohamdaimn.workers.dev', + 'https://simple-proxy.adzel.workers.dev', + 'https://simple-proxy.agenteg1000.workers.dev', + 'https://simple-proxy.bmk77778888.workers.dev', + 'https://simple-proxy.cacayi.workers.dev', + 'https://simple-proxy.chayokefck.workers.dev', + 'https://simple-proxy.f6zc5kyfjn.workers.dev', + 'https://simple-proxy.febopo2221.workers.dev', + 'https://simple-proxy.garble.workers.dev', + 'https://simple-proxy.hydrovolter.workers.dev', + 'https://simple-proxy.issaoui-said-info.workers.dev', + 'https://simple-proxy.jacobalt.workers.dev', + 'https://simple-proxy.justinbdaludado.workers.dev', + 'https://simple-proxy.kllxcai-2e8.workers.dev', + 'https://simple-proxy.lloydsymonds.workers.dev', + 'https://simple-proxy.quantality.workers.dev', + 'https://simple-proxy.ratshrabies.workers.dev', + 'https://simple-proxy.remuxbay.workers.dev', + 'https://simple-proxy.six666666666666666666-6-6-6-6-6-6-6-6-6-6.workers.dev', + 'https://simple-proxy.spencerjamesdeal.workers.dev', + 'https://simple-proxy.starseekerdude.workers.dev', + 'https://simple-proxy.subhashgottumukkala17.workers.dev', + 'https://simple-proxy.suvigya-goyal.workers.dev', + 'https://simple-proxy.tusharkhandare007.workers.dev', + 'https://simple-proxy.valdezmarcjoshua.workers.dev', + 'https://simple-proxy.ytonlyamit.workers.dev', + 'https://simple-proxy2.tonyvu4913.workers.dev', + 'https://simple-proxyyy.thinner-life-void.workers.dev', + 'https://bruh.jerry5890.workers.dev', + 'https://c719dda0-simple-proxy.sylaxx95.workers.dev', +].map((url) => url.trim()); + +const SHOWBOX_BASE = 'https://www.showbox.lat'; +const FEBBOX_BASE = 'https://www.febbox.com'; +const MEDIA_PROXY_API = 'https://media-proxy.oct-cdn.co/api/fetchMp4'; + +const REQUEST_HEADERS = { + 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0', + accept: 'application/json, text/plain, */*', + 'accept-language': 'en-US,en;q=0.5', + 'accept-encoding': 'gzip, deflate, br, zstd', + origin: 'https://watch.rose.stream', + referer: 'https://watch.rose.stream/', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'cross-site', +}; + +async function customFetcher( + ctx: ShowScrapeContext | MovieScrapeContext, + url: string, + options: any = {}, + maxConcurrent = 4, + timeoutMs = 7000, +): Promise { + const shuffled = [...PROXY_URLS].sort(() => 0.5 - Math.random()); + const ac = new AbortController(); + + const fetchWithTimeout = async (proxy: string) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeoutMs); + + try { + const proxied = `${proxy}/?destination=${encodeURIComponent(url)}`; + const res = await ctx.fetcher(proxied, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + ac.abort(); + return res; + } catch (err) { + clearTimeout(id); + throw err; + } + }; + + for (let i = 0; i < shuffled.length; i += maxConcurrent) { + const batch = shuffled.slice(i, i + maxConcurrent); + const promises = batch.map((proxy) => fetchWithTimeout(proxy).catch(() => null)); + + try { + const winner = await Promise.race( + promises.map((p) => p.then((res) => (res !== null ? res : new Promise(() => {})))), + ); + if (winner !== undefined) return winner; + } catch {} + } + + throw new Error('All proxies failed'); +} + +async function getShowboxId( + ctx: ShowScrapeContext | MovieScrapeContext, + query: string, + _mediaType: string, +): Promise { + const searchUrl = `${SHOWBOX_BASE}/search?keyword=${encodeURIComponent(query)}`; + try { + const response = await customFetcher(ctx, searchUrl, { headers: REQUEST_HEADERS }); + const $ = load(response); + const results = $('.film-poster-ahref'); + if (results.length === 0) return null; + + let exactMatch: string | null = null; + let startsWithMatch: string | null = null; + let firstResult: string | null = `${SHOWBOX_BASE}${$(results[0]).attr('href')}`; + + const queryLower = query.toLowerCase(); + for (const result of results.toArray()) { + const title = $(result).attr('title')?.trim()?.toLowerCase(); + const href = $(result).attr('href'); + if (title === queryLower) { + exactMatch = `${SHOWBOX_BASE}${href}`; + break; + } + if (!startsWithMatch && title?.startsWith(queryLower)) { + startsWithMatch = `${SHOWBOX_BASE}${href}`; + } + } + + const targetUrl = exactMatch || startsWithMatch || firstResult; + if (targetUrl) { + const detailResponse = await customFetcher(ctx, targetUrl, { headers: REQUEST_HEADERS }); + const detail$ = load(detailResponse); + const headingLink = detail$('h2.heading-name a'); + if (headingLink.length > 0) { + return headingLink.attr('href')?.split('/').pop() || null; + } + const match = targetUrl.match(/-(\d+)$/); + return match ? match[1] : null; + } + } catch {} + return null; +} + +async function getShareKey( + ctx: ShowScrapeContext | MovieScrapeContext, + showboxId: string, + mediaType: string, +): Promise { + const typeCode = mediaType === 'tv' ? '2' : '1'; + const shareLinkUrl = `${SHOWBOX_BASE}/index/share_link?id=${showboxId}&type=${typeCode}`; + try { + const response = await customFetcher(ctx, shareLinkUrl, { headers: REQUEST_HEADERS }); + if (typeof response === 'object' && response !== null) { + if ('data' in response && response.data && 'link' in response.data) { + return (response.data as any).link.split('/').pop(); + } + if ('link' in response) return (response as any).link.split('/').pop(); + if ('key' in response) return (response as any).key; + if ('data' in response) { + const val = (response as any).data; + if (typeof val === 'string' && val.includes('febbox.com')) { + return val.split('/').pop() || null; + } + return String(val); + } + } + return String(response); + } catch { + return null; + } +} + +async function getFebboxFileList(ctx: any, shareKey: string, parentId = 0) { + const url = `${FEBBOX_BASE}/file/file_share_list?share_key=${shareKey}&pwd=&parent_id=${parentId}`; + return await customFetcher(ctx, url, { headers: REQUEST_HEADERS }); +} + +async function findFileFid( + ctx: ShowScrapeContext | MovieScrapeContext, + shareKey: string, + mediaType: string, + season?: number, + episode?: number, +): Promise { + const rootData = await getFebboxFileList(ctx, shareKey); + if (!rootData?.data?.file_list) return null; + const files = rootData.data.file_list; + + if (mediaType === 'movie') { + const videoFiles = files.filter((f: any) => f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())); + if (videoFiles.length === 0) return null; + videoFiles.sort( + (a: any, b: any) => parseInt(b.file_size_bytes || '0', 10) - parseInt(a.file_size_bytes || '0', 10), + ); + return videoFiles[0].fid; + } + + if (mediaType === 'tv' && season && episode) { + const seasonStr = `Season ${season}`; + const seasonPad = `Season ${season.toString().padStart(2, '0')}`; + let seasonFid: number | null = null; + + for (const f of files) { + if (f.is_dir === 1) { + const name = f.file_name.trim(); + if (name.toLowerCase() === seasonStr.toLowerCase() || name.toLowerCase() === seasonPad.toLowerCase()) { + seasonFid = f.fid; + break; + } + } + } + + if (!seasonFid) { + const seasonRegex = new RegExp(`S0?${season}\\b`, 'i'); + for (const f of files) { + if (f.is_dir === 1) { + const name = f.file_name.trim(); + if (seasonRegex.test(name) && !/\b\d+\b.*season/i.test(name)) { + seasonFid = f.fid; + break; + } + } + } + } + + if (!seasonFid) return null; + const seasonData = await getFebboxFileList(ctx, shareKey, seasonFid); + if (!seasonData?.data?.file_list) return null; + + const epFiles = seasonData.data.file_list; + const patterns = [ + new RegExp(`S0?${season}E0?${episode}\\b`, 'i'), + new RegExp(`${season}x0?${episode}\\b`, 'i'), + new RegExp(`E0?${episode}\\b`, 'i'), + new RegExp(`\\b${episode}\\b`, 'i'), + ]; + + for (const f of epFiles) { + if (f.is_dir === 0 && f.ext && ['mp4', 'mkv', 'avi'].includes(f.ext.toLowerCase())) { + const name = f.file_name; + if (patterns.some((p) => p.test(name))) return f.fid; + } + } + } + return null; +} + +async function getStreamUrl( + ctx: ShowScrapeContext | MovieScrapeContext, + fid: number, + shareKey: string, +): Promise { + const payload = { fid, share_key: shareKey, user_token: null }; + try { + const response = await customFetcher(ctx, MEDIA_PROXY_API, { + method: 'POST', + headers: { ...REQUEST_HEADERS, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (response?.sources?.length) { + let sources = response.sources.filter( + (s: any) => s.download_url.includes('.m3u8') && s.download_url.includes('hls.shegu.net'), + ); + if (sources.length === 0) return null; + + const auto = sources.find((s: any) => s.quality.toUpperCase() === 'AUTO'); + if (auto) return auto.download_url; + + const rank = (q: string) => { + const u = q.toUpperCase(); + if (u.includes('1080')) return 100; + if (u.includes('720')) return 80; + if (u === 'ORG') return 60; + if (u.includes('480')) return 40; + if (u.includes('360')) return 20; + return 0; + }; + + sources.sort((a: any, b: any) => rank(b.quality) - rank(a.quality)); + return sources[0].download_url; + } + } catch {} + return null; +} + +async function scraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { + ctx.progress(10); + const isShow = ctx.media.type === 'show'; + const mediaType = isShow ? 'tv' : 'movie'; + const queryName = (ctx.media.title || '').replace(/and/gi, '&'); + + const showboxId = await getShowboxId(ctx, queryName, mediaType); + if (!showboxId) throw new NotFoundError('Showbox ID not found'); + + const shareKey = await getShareKey(ctx, showboxId, mediaType); + if (!shareKey) throw new NotFoundError('Share key not found'); + + let fid: number | null; + if (isShow) { + const showCtx = ctx as ShowScrapeContext; + fid = await findFileFid(ctx, shareKey, mediaType, showCtx.media.season.number, showCtx.media.episode.number); + } else { + fid = await findFileFid(ctx, shareKey, mediaType); + } + if (!fid) throw new NotFoundError('File ID not found'); + + ctx.progress(50); + const streamUrl = await getStreamUrl(ctx, fid, shareKey); + if (!streamUrl) throw new NotFoundError('No stream found'); + ctx.progress(90); + + return { + embeds: [], + stream: [ + { + id: 'primary', + type: 'hls', + playlist: streamUrl, + flags: [flags.CORS_ALLOWED], + captions: [], + headers: { + Referer: FEBBOX_BASE, + }, + }, + ], + }; +} + +export const roseScraper = makeSourcerer({ + id: 'rose', + name: 'Rosé 🌹', + rank: 203, + disabled: false, + flags: [flags.CORS_ALLOWED], + scrapeMovie: scraper, + scrapeShow: scraper, +});