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.
This commit is contained in:
Swasthik 2025-09-02 15:14:54 +05:30
parent 8b667bd776
commit 5469e6f40b
11 changed files with 658 additions and 231 deletions

99
PROVIDER_STATUS.md Normal file
View file

@ -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

View file

@ -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<Embed> {
vidnestAllmoviesEmbed,
vidnestFlixhqEmbed,
vidnestOfficialEmbed,
wecimaEmbedScraper,
];
}

View file

@ -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',
},

View file

@ -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<string>(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,
},
},
],
};
},
});

View file

@ -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<EmbedOutput> {
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<string, string> = 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<string, string> =
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: [],
},

View file

@ -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<SourcererOutput> {
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,
});

View file

@ -91,6 +91,7 @@ export const fsharetvScraper = makeSourcerer({
id: 'fsharetv',
name: 'FshareTV',
rank: 190,
disabled: true,
flags: [],
scrapeMovie: comboScraper,
});

View file

@ -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,
});

View file

@ -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<string>(url, {
method: 'POST',
body: new URLSearchParams({ play: '' }),
// Get the movie page
const moviePage = await ctx.proxiedFetcher<string>(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<string>(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<string>(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<string>(episodeLink, {
baseUrl,
});
} else {
// Try POST method with episode data
try {
episodePage = await ctx.proxiedFetcher<string>(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<string>(
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 {

View file

@ -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<SourcererOutput> {
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<string>(`/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<string>(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<string>(`/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<string>(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<string>(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<string>(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 };
},
});

View file

@ -3,41 +3,43 @@ import { getAnilistIdFromMedia } from '@/utils/anilist';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
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,
});