From 5469e6f40bd996faffbb8ad2ed9a57a69a7f2e7f Mon Sep 17 00:00:00 2001 From: Swasthik Date: Tue, 2 Sep 2025 15:14:54 +0530 Subject: [PATCH] feat: Add Wecima embed scraper and update related sources - Implemented wecimaEmbedScraper to extract HLS stream URLs from Wecima embeds. - Updated all.ts to include wecimaEmbedScraper in gatherAllEmbeds. - Refactored animetsuScraper to use animetsuId instead of anilistId. - Enhanced zunimeScraper to support different content types and improved error handling. - Updated fsharetv, ridomovies, and tugaflix scrapers to reflect current status and disabled state. - Added detailed provider status report with testing results and current issues. --- PROVIDER_STATUS.md | 99 +++++++++ src/providers/all.ts | 2 + src/providers/embeds/animetsu.ts | 4 +- src/providers/embeds/wecima-embed.ts | 73 ++++++ src/providers/embeds/zunime.ts | 95 ++++++-- src/providers/sources/animetsu.ts | 112 ++++++---- src/providers/sources/fsharetv.ts | 1 + src/providers/sources/ridomovies/index.ts | 5 +- src/providers/sources/tugaflix/index.ts | 171 ++++++++++---- src/providers/sources/wecima.ts | 259 ++++++++++++++-------- src/providers/sources/zunime.ts | 68 +++--- 11 files changed, 658 insertions(+), 231 deletions(-) create mode 100644 PROVIDER_STATUS.md create mode 100644 src/providers/embeds/wecima-embed.ts diff --git a/PROVIDER_STATUS.md b/PROVIDER_STATUS.md new file mode 100644 index 0000000..c2bc39c --- /dev/null +++ b/PROVIDER_STATUS.md @@ -0,0 +1,99 @@ +# Provider Status Report + +Last Updated: September 2, 2025 + +## Testing Details +- **Test Movie**: Fight Club (TMDB ID: 550) +- **Fetcher**: Native +- **Environment**: Windows PowerShell + +## Current Testing Progress +- **Total Tested**: 11/~50 providers +- **Success Rate**: 45% (5/11 working) +- **Working**: 5 providers +- **Failed**: 6 providers + +## Source Providers Status + +### ✅ Working Providers +| Provider | Status | Notes | Last Tested | +|----------|--------|-------|-------------| +| cloudnestra | ✅ Working | Returns HLS stream with proxy depth 2 | 2025-09-02 | +| rgshows | ✅ Working | Returns HLS stream with custom headers | 2025-09-02 | +| lookmovie | ✅ Working | Returns HLS index.m3u8 + 44 subtitle languages, ip-locked flag | 2025-09-02 | +| vidnest | ✅ Working | Returns 2 embed URLs (hollymoviehd, allmovies) | 2025-09-02 | +| hdrezka | ✅ Working | File-based MP4 streams, 4 qualities + 3 subtitle languages | 2025-09-02 | + +### ❌ Failing Providers +| Provider | Status | Error | Last Tested | +|----------|--------|-------|-------------| +| ridomovies | ❌ Disabled | Depends on closeload embed, which has broken decoding logic | 2025-09-02 | +| zunime | ❌ Disabled | API authentication issues with backend services | 2025-09-02 | +| tugaflix | ❌ Disabled | Mixed embeds (mixdrop/streamtape), streamtape blocked in India | 2025-09-02 | +| animetsu | ❌ Disabled | Returns embed URLs but no actual streams, anime-only | 2025-09-02 | +| wecima | ❌ Disabled | Arabic site, complex embed structure not extracting streams | 2025-09-02 | +| fsharetv | ❌ Disabled | Geo-blocked, works only with VPN (provides MP4 streams) | 2025-09-02 | + +### ⏳ Pending Tests +| Provider | Rank | Media Types | Notes | +|----------|------|-------------|-------| +| vidsrcvip | 200 | Movies/Shows | - | +| vidify | 180 | Movies/Shows | - | +| animeflv | 170 | Movies/Shows | - | +| animeflv | 170 | Movies/Shows | - | +| catflix | 160 | Movies/Shows | - | +| coitus | 150 | Movies/Shows | - | +| cuevana3 | 140 | Movies/Shows | - | +| embedsu | 130 | Movies/Shows | - | +| fsharetv | 120 | Movies/Shows | - | +| iosmirror | 110 | Movies/Shows | - | +| iosmirrorpv | 105 | Movies/Shows | - | +| madplay | 100 | Movies/Shows | - | +| mp4hydra | 95 | Movies/Shows | - | +| nepu | 90 | Movies/Shows | - | +| nunflix | 85 | Movies/Shows | - | +| pirxcy | 80 | Movies/Shows | - | +| slidemovies | 70 | Movies/Shows | - | +| streambox | 65 | Movies/Shows | - | +| streambox | 65 | Movies/Shows | - | +| vidapiclick | 60 | Movies/Shows | - | +| wecima | 55 | Movies/Shows | - | +| zoechip | 50 | Movies/Shows | - | +| autoembed | 35 | Movies/Shows | - | +| cinemaos | 30 | Movies/Shows | - | + +## Embed Providers Status + +### ⏳ Pending Tests +| Provider | Rank | Notes | +|----------|------|-------| +| upcloud | 201 | - | +| vidcloud | 201 | Disabled, uses upcloud | +| streamwish | 125 | - | +| mp4hydra | 120 | - | +| mixdrop | 115 | - | +| streamtape | 110 | - | +| dood | 105 | - | +| streamvid | 100 | - | +| vidsrcsu | 95 | - | +| ridoo | 90 | - | +| turbovid | 85 | - | +| closeload | 80 | - | +| viper | 75 | - | +| madplay | 70 | - | +| animetsu | 65 | - | +| autoembed | 60 | - | +| cinemaos | 55 | - | +| zunime | 50 | - | + +## Common Issues Encountered +1. **Connection Timeouts**: Sites may be down or blocking requests +2. **Domain Changes**: Streaming sites frequently change domains +3. **Anti-Bot Protection**: Sites may have enhanced protection +4. **Geo-blocking**: Some sites may be region-restricted + +## Next Steps +- Continue testing providers in rank order +- Update status as testing progresses +- Identify patterns in failures +- Document working provider configurations diff --git a/src/providers/all.ts b/src/providers/all.ts index 5231ab1..e308d23 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -63,6 +63,7 @@ import { viperScraper } from './embeds/viper'; import { warezcdnembedHlsScraper } from './embeds/warezcdn/hls'; import { warezcdnembedMp4Scraper } from './embeds/warezcdn/mp4'; import { warezPlayerScraper } from './embeds/warezcdn/warezplayer'; +import { wecimaEmbedScraper } from './embeds/wecima-embed'; import { zunimeEmbeds } from './embeds/zunime'; import { EightStreamScraper } from './sources/8stream'; import { animeflvScraper } from './sources/animeflv'; @@ -187,5 +188,6 @@ export function gatherAllEmbeds(): Array { vidnestAllmoviesEmbed, vidnestFlixhqEmbed, vidnestOfficialEmbed, + wecimaEmbedScraper, ]; } diff --git a/src/providers/embeds/animetsu.ts b/src/providers/embeds/animetsu.ts index fb2c0c7..304613f 100644 --- a/src/providers/embeds/animetsu.ts +++ b/src/providers/embeds/animetsu.ts @@ -23,7 +23,7 @@ export function makeAnimetsuEmbed(id: string, rank: number = 100) { const serverName = id as (typeof ANIMETSU_SERVERS)[number]; const query = JSON.parse(ctx.url); - const { type, anilistId, episode } = query; + const { type, animetsuId, episode } = query; // Use animetsuId instead of anilistId if (type !== 'movie' && type !== 'show') { throw new NotFoundError('Unsupported media type'); @@ -34,7 +34,7 @@ export function makeAnimetsuEmbed(id: string, rank: number = 100) { headers, query: { server: serverName, - id: String(anilistId), + id: String(animetsuId), // Use animetsu internal ID num: String(episode ?? 1), subType: 'dub', }, diff --git a/src/providers/embeds/wecima-embed.ts b/src/providers/embeds/wecima-embed.ts new file mode 100644 index 0000000..e93c2c8 --- /dev/null +++ b/src/providers/embeds/wecima-embed.ts @@ -0,0 +1,73 @@ +import { load } from 'cheerio'; + +import { flags } from '@/entrypoint/utils/targets'; +import { makeEmbed } from '@/providers/base'; + +export const wecimaEmbedScraper = makeEmbed({ + id: 'wecima-embed', + name: 'Wecima Embed', + rank: 90, + async scrape(ctx) { + // Get the embed page + const embedPage = await ctx.proxiedFetcher(ctx.url); + const $ = load(embedPage); + + // Look for the HLS master playlist URL in the JavaScript + // Based on your analysis, it should be in the format: + // https://fdewsdc.sbs/stream/TOKEN/ID/TIMESTAMP/FILEID/master.m3u8 + + let hlsUrl: string | undefined; + + // Try to extract from the JavaScript setup + const scriptTags = $('script'); + for (const script of scriptTags) { + const scriptContent = $(script).html() || ''; + + // Look for master.m3u8 URLs + const hlsMatch = scriptContent.match(/https?:\/\/[^"']+\/master\.m3u8/); + if (hlsMatch) { + hlsUrl = hlsMatch[0]; + break; + } + + // Also look for the stream URL pattern you found + const streamMatch = scriptContent.match(/https?:\/\/fdewsdc\.sbs\/stream\/[^"']+\/master\.m3u8/); + if (streamMatch) { + hlsUrl = streamMatch[0]; + break; + } + } + + // If not found in scripts, try to find it in data attributes or other places + if (!hlsUrl) { + const videoElements = $('video, source, div[data-src], div[data-url]'); + for (const element of videoElements) { + const src = $(element).attr('src') || $(element).attr('data-src') || $(element).attr('data-url'); + if (src && src.includes('master.m3u8')) { + hlsUrl = src; + break; + } + } + } + + if (!hlsUrl) { + throw new Error('No HLS stream URL found in wecima embed'); + } + + return { + stream: [ + { + id: 'primary', + type: 'hls', + flags: [flags.IP_LOCKED], + captions: [], + playlist: hlsUrl, + headers: { + Referer: ctx.url, + Origin: new URL(ctx.url).origin, + }, + }, + ], + }; + }, +}); diff --git a/src/providers/embeds/zunime.ts b/src/providers/embeds/zunime.ts index 521385f..7036d64 100644 --- a/src/providers/embeds/zunime.ts +++ b/src/providers/embeds/zunime.ts @@ -4,7 +4,7 @@ import { EmbedOutput, makeEmbed } from '../base'; const ZUNIME_SERVERS = ['hd-2', 'miko', 'shiro', 'zaza']; -const baseUrl = 'https://backend.xaiby.sbs'; +const baseUrl = 'https://vidnest.fun'; // Try direct vidnest API const headers = { referer: 'https://vidnest.fun/', origin: 'https://vidnest.fun', @@ -19,33 +19,94 @@ export function makeZunimeEmbed(id: string, rank: number = 100) { rank, async scrape(ctx): Promise { const serverName = id as (typeof ZUNIME_SERVERS)[number]; + const embedUrl = ctx.url; - const query = JSON.parse(ctx.url); - const { anilistId, episode } = query; + // Parse the embed URL to determine content type and parameters + let apiPath = ''; + let apiQuery: any = {}; - const res = await ctx.proxiedFetcher(`${'/sources'}`, { + if (embedUrl.includes('/movie/')) { + // Movie format: https://vidnest.fun/movie/550 + const tmdbId = embedUrl.split('/movie/')[1]; + apiPath = '/api/movie'; // Try API prefix + apiQuery = { + tmdb: tmdbId, + server: serverName, + }; + } else if (embedUrl.includes('/tv/')) { + // TV format: https://vidnest.fun/tv/1396/1/1 + const pathParts = embedUrl.split('/tv/')[1].split('/'); + const tmdbId = pathParts[0]; + const season = pathParts[1] || '1'; + const episode = pathParts[2] || '1'; + + apiPath = '/api/tv'; // Try API prefix + apiQuery = { + tmdb: tmdbId, + season, + episode, + server: serverName, + }; + } else if (embedUrl.includes('/anime/')) { + // Anime format: https://vidnest.fun/anime/16498/1/dub + const pathParts = embedUrl.split('/anime/')[1].split('/'); + const anilistId = pathParts[0]; + const episode = pathParts[1] || '1'; + const type = pathParts[2] || 'dub'; + + apiPath = '/api/anime'; // Try API prefix + apiQuery = { + anilist: anilistId, + episode, + server: serverName, + type, + }; + } else { + throw new NotFoundError('Invalid embed URL format'); + } + + // Call the backend API + const res = await ctx.proxiedFetcher(apiPath, { baseUrl, headers, - query: { - id: String(anilistId), - ep: String(episode ?? 1), - host: serverName, - type: 'dub', - }, + query: apiQuery, }); // eslint-disable-next-line no-console - console.log(res); + console.log('API Response:', res); const resAny: any = res as any; - if (!resAny?.success || !resAny?.sources?.url) { + // Check for different possible response structures + let streamUrl: string | null = null; + let streamHeaders: Record = headers; + + if (resAny?.success && resAny?.sources?.url) { + // Standard response structure + streamUrl = resAny.sources.url; + streamHeaders = resAny?.sources?.headers || headers; + } else if (resAny?.url) { + // Direct URL response + streamUrl = resAny.url; + } else if (resAny?.stream?.url) { + // Alternative response structure + streamUrl = resAny.stream.url; + streamHeaders = resAny?.stream?.headers || headers; + } else if (typeof resAny === 'string' && resAny.startsWith('http')) { + // Direct string URL response + streamUrl = resAny; + } + + if (!streamUrl) { throw new NotFoundError('No stream URL found in response'); } - const streamUrl = resAny.sources.url; - const upstreamHeaders: Record = - resAny?.sources?.headers && Object.keys(resAny.sources.headers).length > 0 ? resAny.sources.headers : headers; + // If the URL is already proxied through vidnest.fun, use it directly + // Otherwise, wrap it with the old proxy + let finalStreamUrl = streamUrl; + if (!streamUrl.includes('proxy.vidnest.fun') && !streamUrl.includes('proxy-2.madaraverse.online')) { + finalStreamUrl = `https://proxy-2.madaraverse.online/proxy?url=${encodeURIComponent(streamUrl)}`; + } ctx.progress(100); @@ -54,8 +115,8 @@ export function makeZunimeEmbed(id: string, rank: number = 100) { { id: 'primary', type: 'hls', - playlist: `https://proxy-2.madaraverse.online/proxy?url=${encodeURIComponent(streamUrl)}`, - headers: upstreamHeaders, + playlist: finalStreamUrl, + headers: streamHeaders, flags: [], captions: [], }, diff --git a/src/providers/sources/animetsu.ts b/src/providers/sources/animetsu.ts index 2df248c..6cb41f3 100644 --- a/src/providers/sources/animetsu.ts +++ b/src/providers/sources/animetsu.ts @@ -1,54 +1,88 @@ import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { getAnilistIdFromMedia } from '@/utils/anilist'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { NotFoundError } from '@/utils/errors'; async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const anilistId = await getAnilistIdFromMedia(ctx, ctx.media); + // First, search animetsu for the content using title + const searchQuery = ctx.media.title; + try { + // Search animetsu's own database + const searchRes = await ctx.proxiedFetcher('/api/search', { + baseUrl: 'https://backend.animetsu.to', + headers: { + referer: 'https://animetsu.cc/', + origin: 'https://animetsu.cc', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + }, + query: { + q: searchQuery, + limit: '10', + }, + }); - const query: any = { - type: ctx.media.type, - title: ctx.media.title, - tmdbId: ctx.media.tmdbId, - imdbId: ctx.media.imdbId, - anilistId, - ...(ctx.media.type === 'show' && { - season: ctx.media.season.number, - episode: ctx.media.episode.number, - }), - ...(ctx.media.type === 'movie' && { episode: 1 }), - releaseYear: ctx.media.releaseYear, - }; + // eslint-disable-next-line no-console + console.log('Animetsu Search Response:', JSON.stringify(searchRes, null, 2)); - return { - embeds: [ - { - embedId: 'animetsu-pahe', - url: JSON.stringify(query), - }, - { - embedId: 'animetsu-zoro', - url: JSON.stringify(query), - }, - { - embedId: 'animetsu-zaza', - url: JSON.stringify(query), - }, - { - embedId: 'animetsu-meg', - url: JSON.stringify(query), - }, - { - embedId: 'animetsu-bato', - url: JSON.stringify(query), - }, - ], - }; + // Find the best match from search results + const results = searchRes?.results || searchRes?.data || []; + if (!results || results.length === 0) { + throw new NotFoundError('No results found in animetsu search'); + } + + // Get the first result (you could improve matching logic here) + const firstResult = results[0]; + const animetsuId = firstResult?.id || firstResult?.animetsu_id; + if (!animetsuId) { + throw new NotFoundError('No animetsu ID found in search results'); + } + + const query: any = { + type: ctx.media.type, + title: ctx.media.title, + animetsuId, // Use animetsu's internal ID instead of anilist + ...(ctx.media.type === 'show' && { + season: ctx.media.season.number, + episode: ctx.media.episode.number, + }), + ...(ctx.media.type === 'movie' && { episode: 1 }), + releaseYear: ctx.media.releaseYear, + }; + + return { + embeds: [ + { + embedId: 'animetsu-pahe', + url: JSON.stringify(query), + }, + { + embedId: 'animetsu-zoro', + url: JSON.stringify(query), + }, + { + embedId: 'animetsu-zaza', + url: JSON.stringify(query), + }, + { + embedId: 'animetsu-meg', + url: JSON.stringify(query), + }, + { + embedId: 'animetsu-bato', + url: JSON.stringify(query), + }, + ], + }; + } catch (error) { + throw new NotFoundError(`Animetsu search failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } } export const animetsuScraper = makeSourcerer({ id: 'animetsu', name: 'Animetsu', rank: 112, + disabled: true, flags: [], scrapeShow: comboScraper, }); diff --git a/src/providers/sources/fsharetv.ts b/src/providers/sources/fsharetv.ts index 8d8a2ea..3a366f3 100644 --- a/src/providers/sources/fsharetv.ts +++ b/src/providers/sources/fsharetv.ts @@ -91,6 +91,7 @@ export const fsharetvScraper = makeSourcerer({ id: 'fsharetv', name: 'FshareTV', rank: 190, + disabled: true, flags: [], scrapeMovie: comboScraper, }); diff --git a/src/providers/sources/ridomovies/index.ts b/src/providers/sources/ridomovies/index.ts index 9a5aab2..99759dc 100644 --- a/src/providers/sources/ridomovies/index.ts +++ b/src/providers/sources/ridomovies/index.ts @@ -1,5 +1,6 @@ import { load } from 'cheerio'; +import { flags } from '@/entrypoint/utils/targets'; import { SourcererEmbed, makeSourcerer } from '@/providers/base'; import { closeLoadScraper } from '@/providers/embeds/closeload'; import { ridooScraper } from '@/providers/embeds/ridoo'; @@ -78,8 +79,8 @@ export const ridooMoviesScraper = makeSourcerer({ id: 'ridomovies', name: 'RidoMovies', rank: 210, - flags: [], - disabled: false, + flags: [flags.CF_BLOCKED], + disabled: true, // Disabled: closeload embed scraper is broken scrapeMovie: universalScraper, scrapeShow: universalScraper, }); diff --git a/src/providers/sources/tugaflix/index.ts b/src/providers/sources/tugaflix/index.ts index 7b6f1e4..a54ee46 100644 --- a/src/providers/sources/tugaflix/index.ts +++ b/src/providers/sources/tugaflix/index.ts @@ -11,6 +11,7 @@ export const tugaflixScraper = makeSourcerer({ id: 'tugaflix', name: 'Tugaflix', rank: 70, + disabled: true, flags: [flags.IP_LOCKED], scrapeMovie: async (ctx) => { const searchResults = parseSearch( @@ -27,39 +28,62 @@ export const tugaflixScraper = makeSourcerer({ if (!url) throw new NotFoundError('No watchable item found'); ctx.progress(50); - const videoPage = await ctx.proxiedFetcher(url, { - method: 'POST', - body: new URLSearchParams({ play: '' }), + // Get the movie page + const moviePage = await ctx.proxiedFetcher(url, { + baseUrl, }); - const $ = load(videoPage); + const $ = load(moviePage); const embeds: SourcererEmbed[] = []; - for (const element of $('.play a')) { - const embedUrl = $(element).attr('href'); + // Look for mixdrop embed links + // Check for player buttons or iframe sources + const playerElements = $('iframe[src*="mixdrop"], a[href*="mixdrop"], button[data-url*="mixdrop"]'); + + for (const element of playerElements) { + const embedUrl = $(element).attr('src') || $(element).attr('href') || $(element).attr('data-url'); if (!embedUrl) continue; - const embedPage = await ctx.proxiedFetcher.full( - embedUrl.startsWith('https://') ? embedUrl : `https://${embedUrl}`, - ); - - const finalUrl = load(embedPage.body)('a:contains("Download Filme")').attr('href'); - if (!finalUrl) continue; - - if (finalUrl.includes('streamtape')) { + if (embedUrl.includes('mixdrop')) { embeds.push({ - embedId: 'streamtape', - url: finalUrl, - }); - // found doodstream on a few shows, maybe movies use it too? - // the player 2 is just streamtape in a custom player - } else if (finalUrl.includes('dood')) { - embeds.push({ - embedId: 'dood', - url: finalUrl, + embedId: 'mixdrop', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, }); } } + + // If no direct mixdrop links found, look for watch buttons that might lead to mixdrop + if (embeds.length === 0) { + const watchButtons = $('a:contains("Watch"), a:contains("Assistir"), .watch-btn, .play-btn'); + + for (const button of watchButtons) { + const buttonUrl = $(button).attr('href'); + if (!buttonUrl) continue; + + try { + const buttonPage = await ctx.proxiedFetcher(buttonUrl, { + baseUrl, + }); + const $button = load(buttonPage); + + // Look for mixdrop links in the button page + const mixdropLinks = $button('iframe[src*="mixdrop"], a[href*="mixdrop"]'); + for (const link of mixdropLinks) { + const embedUrl = $button(link).attr('src') || $button(link).attr('href'); + if (embedUrl && embedUrl.includes('mixdrop')) { + embeds.push({ + embedId: 'mixdrop', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, + }); + } + } + } catch (error) { + // Continue to next button if this one fails + continue; + } + } + } + ctx.progress(90); return { @@ -81,36 +105,91 @@ export const tugaflixScraper = makeSourcerer({ if (!url) throw new NotFoundError('No watchable item found'); ctx.progress(50); - const s = ctx.media.season.number < 10 ? `0${ctx.media.season.number}` : ctx.media.season.number.toString(); - const e = ctx.media.episode.number < 10 ? `0${ctx.media.episode.number}` : ctx.media.episode.number.toString(); - const videoPage = await ctx.proxiedFetcher(url, { - method: 'POST', - body: new URLSearchParams({ [`S${s}E${e}`]: '' }), - }); - - const embedUrl = load(videoPage)('iframe[name="player"]').attr('src'); - if (!embedUrl) throw new Error('Failed to find iframe'); - - const playerPage = await ctx.proxiedFetcher(embedUrl.startsWith('https:') ? embedUrl : `https:${embedUrl}`, { - method: 'POST', - body: new URLSearchParams({ submit: '' }), + // Get the show page + const showPage = await ctx.proxiedFetcher(url, { + baseUrl, }); + const $ = load(showPage); const embeds: SourcererEmbed[] = []; - const finalUrl = load(playerPage)('a:contains("Download Episodio")').attr('href'); + // Look for episode selection or season/episode links + const s = ctx.media.season.number < 10 ? `0${ctx.media.season.number}` : ctx.media.season.number.toString(); + const e = ctx.media.episode.number < 10 ? `0${ctx.media.episode.number}` : ctx.media.episode.number.toString(); - if (finalUrl?.includes('streamtape')) { - embeds.push({ - embedId: 'streamtape', - url: finalUrl, - }); - } else if (finalUrl?.includes('dood')) { - embeds.push({ - embedId: 'dood', - url: finalUrl, + // Try to find episode link or submit episode form + const episodeLink = $( + `a:contains("S${s}E${e}"), a:contains("${ctx.media.season.number}x${ctx.media.episode.number}")`, + ).attr('href'); + + let episodePage = showPage; + if (episodeLink) { + episodePage = await ctx.proxiedFetcher(episodeLink, { + baseUrl, }); + } else { + // Try POST method with episode data + try { + episodePage = await ctx.proxiedFetcher(url, { + method: 'POST', + body: new URLSearchParams({ [`S${s}E${e}`]: '' }), + baseUrl, + }); + } catch (error) { + // If POST fails, continue with the original page + } } + + const $episode = load(episodePage); + + // Look for mixdrop embed links + const playerElements = $episode('iframe[src*="mixdrop"], a[href*="mixdrop"], button[data-url*="mixdrop"]'); + + for (const element of playerElements) { + const embedUrl = + $episode(element).attr('src') || $episode(element).attr('href') || $episode(element).attr('data-url'); + if (!embedUrl) continue; + + if (embedUrl.includes('mixdrop')) { + embeds.push({ + embedId: 'mixdrop', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, + }); + } + } + + // If no direct mixdrop links found, look for player iframes or watch buttons + if (embeds.length === 0) { + const iframes = $episode('iframe[name="player"], iframe[src]'); + + for (const iframe of iframes) { + const iframeUrl = $episode(iframe).attr('src'); + if (!iframeUrl) continue; + + try { + const iframePage = await ctx.proxiedFetcher( + iframeUrl.startsWith('http') ? iframeUrl : `https:${iframeUrl}`, + ); + const $iframe = load(iframePage); + + // Look for mixdrop links in the iframe page + const mixdropLinks = $iframe('iframe[src*="mixdrop"], a[href*="mixdrop"]'); + for (const link of mixdropLinks) { + const embedUrl = $iframe(link).attr('src') || $iframe(link).attr('href'); + if (embedUrl && embedUrl.includes('mixdrop')) { + embeds.push({ + embedId: 'mixdrop', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, + }); + } + } + } catch (error) { + // Continue to next iframe if this one fails + continue; + } + } + } + ctx.progress(90); return { diff --git a/src/providers/sources/wecima.ts b/src/providers/sources/wecima.ts index 16f721a..1316b62 100644 --- a/src/providers/sources/wecima.ts +++ b/src/providers/sources/wecima.ts @@ -1,102 +1,175 @@ import { load } from 'cheerio'; -import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { flags } from '@/entrypoint/utils/targets'; +import { SourcererEmbed, makeSourcerer } from '@/providers/base'; +import { compareMedia } from '@/utils/compare'; import { NotFoundError } from '@/utils/errors'; -const baseUrl = 'https://wecima.tube'; - -async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const searchPage = await ctx.proxiedFetcher(`/search/${encodeURIComponent(ctx.media.title)}/`, { - baseUrl, - }); - - const search$ = load(searchPage); - const firstResult = search$('.Grid--WecimaPosts .GridItem a').first(); - if (!firstResult.length) throw new NotFoundError('No results found'); - - const contentUrl = firstResult.attr('href'); - if (!contentUrl) throw new NotFoundError('No content URL found'); - ctx.progress(30); - - const contentPage = await ctx.proxiedFetcher(contentUrl, { baseUrl }); - const content$ = load(contentPage); - - let embedUrl: string | undefined; - - if (ctx.media.type === 'movie') { - embedUrl = content$('meta[itemprop="embedURL"]').attr('content'); - } else { - const seasonLinks = content$('.List--Seasons--Episodes a'); - let seasonUrl: string | undefined; - - for (const element of seasonLinks) { - const text = content$(element).text().trim(); - if (text.includes(`موسم ${ctx.media.season}`)) { - seasonUrl = content$(element).attr('href'); - break; - } - } - - if (!seasonUrl) throw new NotFoundError(`Season ${ctx.media.season} not found`); - - const seasonPage = await ctx.proxiedFetcher(seasonUrl, { baseUrl }); - const season$ = load(seasonPage); - - const episodeLinks = season$('.Episodes--Seasons--Episodes a'); - for (const element of episodeLinks) { - const epTitle = season$(element).find('episodetitle').text().trim(); - if (epTitle === `الحلقة ${ctx.media.episode}`) { - const episodeUrl = season$(element).attr('href'); - if (episodeUrl) { - const episodePage = await ctx.proxiedFetcher(episodeUrl, { baseUrl }); - const episode$ = load(episodePage); - embedUrl = episode$('meta[itemprop="embedURL"]').attr('content'); - } - break; - } - } - } - - if (!embedUrl) throw new NotFoundError('No embed URL found'); - ctx.progress(60); - - // Get the final video URL - const embedPage = await ctx.proxiedFetcher(embedUrl); - const embed$ = load(embedPage); - const videoSource = embed$('source[type="video/mp4"]').attr('src'); - - if (!videoSource) throw new NotFoundError('No video source found'); - ctx.progress(90); - - return { - embeds: [], - stream: [ - { - id: 'primary', - type: 'file', - flags: [], - headers: { - referer: baseUrl, - }, - qualities: { - unknown: { - type: 'mp4', - url: videoSource, - }, - }, - captions: [], - }, - ], - }; -} +const baseUrl = 'https://mycima.pics/'; export const wecimaScraper = makeSourcerer({ id: 'wecima', name: 'Wecima (Arabic)', - rank: 3, - disabled: false, - flags: [], - scrapeMovie: comboScraper, - scrapeShow: comboScraper, + rank: 55, + disabled: true, + flags: [flags.IP_LOCKED], + scrapeMovie: async (ctx) => { + // Search for the movie + const searchResults = await ctx.proxiedFetcher(`/search/${encodeURIComponent(ctx.media.title)}/`, { + baseUrl, + }); + + const search$ = load(searchResults); + const movieLinks = search$('.Grid--WecimaPosts .GridItem a'); + + let movieUrl: string | undefined; + for (const element of movieLinks) { + const title = search$(element).find('.title').text().trim(); + const year = search$(element).find('.year').text().trim(); + + if (compareMedia(ctx.media, title, year ? parseInt(year, 10) : undefined)) { + movieUrl = search$(element).attr('href'); + break; + } + } + + if (!movieUrl) throw new NotFoundError('Movie not found'); + ctx.progress(40); + + // Get movie page + const moviePage = await ctx.proxiedFetcher(movieUrl, { baseUrl }); + const movie$ = load(moviePage); + + const embeds: SourcererEmbed[] = []; + + // Look for server buttons or embed links + const serverButtons = movie$('.servers-list a, .server-item a, button[data-server]'); + + for (const button of serverButtons) { + const embedUrl = + movie$(button).attr('href') || movie$(button).attr('data-url') || movie$(button).attr('data-server'); + if (!embedUrl) continue; + + // Check if it's an fdewsdc.sbs embed (the host you found) + if (embedUrl.includes('fdewsdc.sbs') || embedUrl.includes('/embed/')) { + embeds.push({ + embedId: 'wecima-embed', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, + }); + } + } + + // Also check for any embed iframes + const iframes = movie$('iframe[src*="/embed/"], iframe[src*="fdewsdc"], iframe[src*="player"]'); + for (const iframe of iframes) { + const iframeSrc = movie$(iframe).attr('src'); + if (iframeSrc) { + embeds.push({ + embedId: 'wecima-embed', + url: iframeSrc.startsWith('http') ? iframeSrc : `https:${iframeSrc}`, + }); + } + } + + ctx.progress(90); + return { embeds }; + }, + + scrapeShow: async (ctx) => { + // Search for the show + const searchResults = await ctx.proxiedFetcher(`/search/${encodeURIComponent(ctx.media.title)}/`, { + baseUrl, + }); + + const search$ = load(searchResults); + const showLinks = search$('.Grid--WecimaPosts .GridItem a'); + + let showUrl: string | undefined; + for (const element of showLinks) { + const title = search$(element).find('.title').text().trim(); + const year = search$(element).find('.year').text().trim(); + + if (compareMedia(ctx.media, title, year ? parseInt(year, 10) : undefined)) { + showUrl = search$(element).attr('href'); + break; + } + } + + if (!showUrl) throw new NotFoundError('Show not found'); + ctx.progress(30); + + // Get show page and find season + const showPage = await ctx.proxiedFetcher(showUrl, { baseUrl }); + const show$ = load(showPage); + + // Look for season links + const seasonLinks = show$('.List--Seasons--Episodes a, .season-link'); + let seasonUrl: string | undefined; + + for (const element of seasonLinks) { + const text = show$(element).text().trim(); + if (text.includes(`موسم ${ctx.media.season.number}`) || text.includes(`Season ${ctx.media.season.number}`)) { + seasonUrl = show$(element).attr('href'); + break; + } + } + + if (!seasonUrl) throw new NotFoundError(`Season ${ctx.media.season.number} not found`); + ctx.progress(50); + + // Get season page and find episode + const seasonPage = await ctx.proxiedFetcher(seasonUrl, { baseUrl }); + const season$ = load(seasonPage); + + const episodeLinks = season$('.Episodes--Seasons--Episodes a, .episode-link'); + let episodeUrl: string | undefined; + + for (const element of episodeLinks) { + const text = season$(element).text().trim(); + if (text.includes(`الحلقة ${ctx.media.episode.number}`) || text.includes(`Episode ${ctx.media.episode.number}`)) { + episodeUrl = season$(element).attr('href'); + break; + } + } + + if (!episodeUrl) throw new NotFoundError(`Episode ${ctx.media.episode.number} not found`); + ctx.progress(70); + + // Get episode page + const episodePage = await ctx.proxiedFetcher(episodeUrl, { baseUrl }); + const episode$ = load(episodePage); + + const embeds: SourcererEmbed[] = []; + + // Look for server buttons or embed links + const serverButtons = episode$('.servers-list a, .server-item a, button[data-server]'); + + for (const button of serverButtons) { + const embedUrl = + episode$(button).attr('href') || episode$(button).attr('data-url') || episode$(button).attr('data-server'); + if (!embedUrl) continue; + + if (embedUrl.includes('fdewsdc.sbs') || embedUrl.includes('/embed/')) { + embeds.push({ + embedId: 'wecima-embed', + url: embedUrl.startsWith('http') ? embedUrl : `https:${embedUrl}`, + }); + } + } + + // Also check for any embed iframes + const iframes = episode$('iframe[src*="/embed/"], iframe[src*="fdewsdc"], iframe[src*="player"]'); + for (const iframe of iframes) { + const iframeSrc = episode$(iframe).attr('src'); + if (iframeSrc) { + embeds.push({ + embedId: 'wecima-embed', + url: iframeSrc.startsWith('http') ? iframeSrc : `https:${iframeSrc}`, + }); + } + } + + ctx.progress(90); + return { embeds }; + }, }); diff --git a/src/providers/sources/zunime.ts b/src/providers/sources/zunime.ts index 2f043e2..9834493 100644 --- a/src/providers/sources/zunime.ts +++ b/src/providers/sources/zunime.ts @@ -3,41 +3,43 @@ import { getAnilistIdFromMedia } from '@/utils/anilist'; import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise { - const anilistId = await getAnilistIdFromMedia(ctx, ctx.media); + let embedUrls: { embedId: string; url: string }[] = []; - const query: any = { - type: ctx.media.type, - title: ctx.media.title, - tmdbId: ctx.media.tmdbId, - imdbId: ctx.media.imdbId, - anilistId, - ...(ctx.media.type === 'show' && { - season: ctx.media.season.number, - episode: ctx.media.episode.number, - }), - ...(ctx.media.type === 'movie' && { episode: 1 }), - releaseYear: ctx.media.releaseYear, - }; + // Generate proper embed URLs based on content type + if (ctx.media.type === 'movie') { + // For movies, use TMDB-based embed URLs + const baseEmbedUrl = `https://vidnest.fun/movie/${ctx.media.tmdbId}`; + embedUrls = [ + { embedId: 'zunime-hd-2', url: baseEmbedUrl }, + { embedId: 'zunime-miko', url: baseEmbedUrl }, + { embedId: 'zunime-shiro', url: baseEmbedUrl }, + { embedId: 'zunime-zaza', url: baseEmbedUrl }, + ]; + } else if (ctx.media.type === 'show') { + try { + // Try to get Anilist ID for anime shows + const anilistId = await getAnilistIdFromMedia(ctx, ctx.media); + const baseEmbedUrl = `https://vidnest.fun/anime/${anilistId}/${ctx.media.episode.number}/dub`; + embedUrls = [ + { embedId: 'zunime-hd-2', url: baseEmbedUrl }, + { embedId: 'zunime-miko', url: baseEmbedUrl }, + { embedId: 'zunime-shiro', url: baseEmbedUrl }, + { embedId: 'zunime-zaza', url: baseEmbedUrl }, + ]; + } catch { + // Fallback to TMDB for regular TV shows + const baseEmbedUrl = `https://vidnest.fun/tv/${ctx.media.tmdbId}/${ctx.media.season.number}/${ctx.media.episode.number}`; + embedUrls = [ + { embedId: 'zunime-hd-2', url: baseEmbedUrl }, + { embedId: 'zunime-miko', url: baseEmbedUrl }, + { embedId: 'zunime-shiro', url: baseEmbedUrl }, + { embedId: 'zunime-zaza', url: baseEmbedUrl }, + ]; + } + } return { - embeds: [ - { - embedId: 'zunime-hd-2', - url: JSON.stringify(query), - }, - { - embedId: 'zunime-miko', - url: JSON.stringify(query), - }, - { - embedId: 'zunime-shiro', - url: JSON.stringify(query), - }, - { - embedId: 'zunime-zaza', - url: JSON.stringify(query), - }, - ], + embeds: embedUrls, }; } @@ -45,6 +47,8 @@ export const zunimeScraper = makeSourcerer({ id: 'zunime', name: 'Zunime', rank: 125, + disabled: true, // Disabled due to API authentication issues flags: [], + scrapeMovie: comboScraper, scrapeShow: comboScraper, });