mirror of
https://github.com/p-stream/providers.git
synced 2026-05-09 20:20:52 +00:00
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:
parent
8b667bd776
commit
5469e6f40b
11 changed files with 658 additions and 231 deletions
99
PROVIDER_STATUS.md
Normal file
99
PROVIDER_STATUS.md
Normal 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
|
||||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
73
src/providers/embeds/wecima-embed.ts
Normal file
73
src/providers/embeds/wecima-embed.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const fsharetvScraper = makeSourcerer({
|
|||
id: 'fsharetv',
|
||||
name: 'FshareTV',
|
||||
rank: 190,
|
||||
disabled: true,
|
||||
flags: [],
|
||||
scrapeMovie: comboScraper,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue