add sources

This commit is contained in:
Pas 2026-03-05 10:04:58 -07:00
parent 33903e2b00
commit 6b24427c8f
8 changed files with 637 additions and 21 deletions

View file

@ -91,9 +91,13 @@ import { streamboxScraper } from './sources/streambox';
import { turbovidSourceScraper } from './sources/turbovid';
import { vidapiClickScraper } from './sources/vidapiclick';
import { vidifyScraper } from './sources/vidify';
import { vidlinkScraper } from './sources/vidlink';
import { vidnestScraper } from './sources/vidnest';
import { vidrockScraper } from './sources/vidrock';
import { warezcdnScraper } from './sources/warezcdn';
import { watchanimeworldScraper } from './sources/watchanimeworld';
import { wecimaScraper } from './sources/wecima';
import { yesmoviesScraper } from './sources/yesmovies';
import { zunimeScraper } from './sources/zunime';
export function gatherAllSources(): Array<Sourcerer> {
@ -138,6 +142,10 @@ export function gatherAllSources(): Array<Sourcerer> {
debridScraper,
cinehdplusScraper,
fullhdfilmizleScraper,
vidlinkScraper,
yesmoviesScraper,
vidrockScraper,
watchanimeworldScraper,
];
}

View file

@ -1,5 +1,5 @@
export const apiBaseUrl = 'https://borg.rips.cc';
export const username = '_sf_'; // I'd appreciate if you made your own account "_sf_" seems to be removed. Invite codes are: fmhy or mpgh
export const username = '_ps_';
export const password = 'defonotscraping';

View file

@ -33,7 +33,7 @@ export const lookmovieScraper = makeSourcerer({
id: 'lookmovie',
name: 'LookMovie',
disabled: false,
rank: 170,
rank: 171,
flags: [flags.IP_LOCKED],
scrapeShow: universalScraper,
scrapeMovie: universalScraper,

View file

@ -1,8 +1,6 @@
import { load } from 'cheerio';
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
import { closeLoadScraper } from '@/providers/embeds/closeload';
import { ridooScraper } from '@/providers/embeds/ridoo';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
@ -11,6 +9,14 @@ import { IframeSourceResult, SearchResult } from './types';
const ridoMoviesBase = `https://ridomovies.tv`;
const ridoMoviesApiBase = `${ridoMoviesBase}/core/api`;
const normalizeTitle = (title: string): string => {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, ' ');
};
const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
const searchResult = await ctx.proxiedFetcher<SearchResult>('/search', {
baseUrl: ridoMoviesApiBase,
@ -18,14 +24,37 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
q: ctx.media.title,
},
});
if (!searchResult.data?.items || searchResult.data.items.length === 0) {
throw new NotFoundError('No search results found');
}
const mediaData = searchResult.data.items.map((movieEl) => {
const name = movieEl.title;
const year = movieEl.contentable.releaseYear;
const fullSlug = movieEl.fullSlug;
return { name, year, fullSlug };
});
const targetMedia = mediaData.find((m) => m.name === ctx.media.title && m.year === ctx.media.releaseYear.toString());
if (!targetMedia?.fullSlug) throw new NotFoundError('No watchable item found');
const normalizedSearchTitle = normalizeTitle(ctx.media.title);
const searchYear = ctx.media.releaseYear.toString();
let targetMedia = mediaData.find((m) => normalizeTitle(m.name) === normalizedSearchTitle && m.year === searchYear);
if (!targetMedia) {
targetMedia = mediaData.find((m) => {
const normalizedName = normalizeTitle(m.name);
return (
m.year === searchYear &&
(normalizedName.includes(normalizedSearchTitle) || normalizedSearchTitle.includes(normalizedName))
);
});
}
if (!targetMedia?.fullSlug) {
throw new NotFoundError('No matching media found');
}
ctx.progress(40);
let iframeSourceUrl = `/${targetMedia.fullSlug}/videos`;
@ -34,14 +63,20 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
const showPageResult = await ctx.proxiedFetcher<string>(`/${targetMedia.fullSlug}`, {
baseUrl: ridoMoviesBase,
});
const fullEpisodeSlug = `season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`;
const regexPattern = new RegExp(
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\\\"fullSlug\\\\\\":\\\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\\\")`,
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\"fullSlug\\\\":\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\")`,
'g',
);
const matches = [...showPageResult.matchAll(regexPattern)];
const episodeIds = matches.map((match) => match[1]);
if (episodeIds.length === 0) throw new NotFoundError('No watchable item found');
if (episodeIds.length === 0) {
throw new NotFoundError('Episode not found');
}
const episodeId = episodeIds[episodeIds.length - 1];
iframeSourceUrl = `/episodes/${episodeId}/videos`;
}
@ -49,24 +84,38 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
const iframeSource = await ctx.proxiedFetcher<IframeSourceResult>(iframeSourceUrl, {
baseUrl: ridoMoviesApiBase,
});
if (!iframeSource.data || iframeSource.data.length === 0) {
throw new NotFoundError('No video sources found');
}
const iframeSource$ = load(iframeSource.data[0].url);
const iframeUrl = iframeSource$('iframe').attr('data-src');
if (!iframeUrl) throw new NotFoundError('No watchable item found');
if (!iframeUrl) {
throw new NotFoundError('No iframe URL found');
}
ctx.progress(60);
const embeds: SourcererEmbed[] = [];
if (iframeUrl.includes('closeload')) {
embeds.push({
embedId: closeLoadScraper.id,
url: iframeUrl,
});
}
let embedId = 'closeload';
if (iframeUrl.includes('ridoo')) {
embeds.push({
embedId: ridooScraper.id,
url: iframeUrl,
});
embedId = 'ridoo';
}
embeds.push({
embedId,
url: iframeUrl,
});
ctx.progress(80);
if (embeds.length === 0) {
throw new NotFoundError('No supported embeds found');
}
ctx.progress(90);
return {
@ -77,9 +126,9 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
export const ridooMoviesScraper = makeSourcerer({
id: 'ridomovies',
name: 'RidoMovies',
rank: 210,
rank: 203,
flags: [],
disabled: true,
disabled: false,
scrapeMovie: universalScraper,
scrapeShow: universalScraper,
});

View file

@ -0,0 +1,113 @@
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const API_BASE = 'https://enc-dec.app/api';
const VIDLINK_BASE = 'https://vidlink.pro/api/b';
const headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
Connection: 'keep-alive',
Referer: 'https://vidlink.pro/',
Origin: 'https://vidlink.pro',
};
async function encryptTmdbId(ctx: MovieScrapeContext | ShowScrapeContext, tmdbId: string): Promise<string> {
const response = await ctx.proxiedFetcher<{ result: string }>(`${API_BASE}/enc-vidlink`, {
method: 'GET',
query: { text: tmdbId },
});
if (!response?.result) {
throw new NotFoundError('Failed to encrypt TMDB ID');
}
return response.result;
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const { tmdbId } = ctx.media;
ctx.progress(10);
const encryptedId = await encryptTmdbId(ctx, tmdbId.toString());
ctx.progress(30);
const apiUrl =
ctx.media.type === 'movie'
? `${VIDLINK_BASE}/movie/${encryptedId}`
: `${VIDLINK_BASE}/tv/${encryptedId}/${ctx.media.season.number}/${ctx.media.episode.number}`;
const vidlinkResponse = await ctx.proxiedFetcher<string>(apiUrl, {
headers,
});
if (!vidlinkResponse) {
throw new NotFoundError('No response from vidlink API');
}
ctx.progress(60);
let vidlinkData;
try {
vidlinkData = JSON.parse(vidlinkResponse);
} catch (e) {
throw new NotFoundError('Invalid JSON response from vidlink API');
}
ctx.progress(80);
if (!vidlinkData || !vidlinkData.stream) {
throw new NotFoundError('No stream data found in vidlink response');
}
const { stream } = vidlinkData;
const captions = [];
if (stream.captions && Array.isArray(stream.captions)) {
for (const caption of stream.captions) {
const captionType = caption.type === 'srt' ? 'srt' : 'vtt';
captions.push({
id: caption.id || caption.url,
url: caption.url,
language: caption.language || 'Unknown',
type: captionType as 'srt' | 'vtt',
hasCorsRestrictions: caption.hasCorsRestrictions || false,
});
}
}
// const flags = stream.flags || [];
// if (vidlinkData.flags) {
// flags.push(...vidlinkData.flags);
// }
ctx.progress(90);
return {
embeds: [],
stream: [
{
id: stream.id || 'primary',
type: stream.type || 'file',
qualities: stream.qualities || {},
playlist: stream.playlist,
captions,
flags: [],
headers: stream.headers || headers,
},
],
};
}
export const vidlinkScraper = makeSourcerer({
id: 'vidlink',
name: 'VidLink',
rank: 240,
disabled: false,
flags: [],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,165 @@
import CryptoJS from 'crypto-js';
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const headers = {
Origin: 'https://vidrock.net',
Referer: 'https://vidrock.net/',
};
const passphrase = 'x7k9mPqT2rWvY8zA5bC3nF6hJ2lK4mN9';
const key = CryptoJS.enc.Utf8.parse(passphrase);
const iv = CryptoJS.enc.Utf8.parse(passphrase.substring(0, 16));
const baseUrl = 'https://vidrock.net/api';
const userAgent =
'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36';
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const itemType = ctx.media.type;
let itemId: string;
if (itemType === 'movie') {
itemId = ctx.media.tmdbId;
} else {
itemId = `${ctx.media.tmdbId}_${ctx.media.season.number}_${ctx.media.episode.number}`;
}
const encrypted = CryptoJS.AES.encrypt(itemId, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
let encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
encryptedBase64 = encryptedBase64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const encoded = encodeURIComponent(encryptedBase64);
const url = `${baseUrl}/${itemType}/${encoded}`;
const res = await ctx.proxiedFetcher<any>(url, {
headers: {
...headers,
'User-Agent': userAgent,
},
});
let parsedRes = res;
if (typeof res === 'string') {
try {
parsedRes = JSON.parse(res);
} catch (e) {
throw new NotFoundError('No sources found from Vidrock API: Invalid JSON response');
}
}
if (!parsedRes || typeof parsedRes !== 'object' || Array.isArray(parsedRes)) {
throw new NotFoundError('No sources found from Vidrock API: Invalid response');
}
const embeds = [];
const createMirrorEmbed = (serverName: string, serverData: any) => {
if (!serverData?.url) return null;
if (serverName.includes('Astra') || serverData.url.includes('.workers.dev')) return null;
const context = {
type: 'hls',
stream: serverData.url,
headers,
flags: [flags.CORS_ALLOWED],
captions: [],
};
return {
embedId: 'mirror',
url: JSON.stringify(context),
};
};
for (const sourceKey of Object.keys(parsedRes)) {
const sourceData = parsedRes[sourceKey];
if (sourceData?.url && sourceData.url !== null) {
// Handle Atlas server which returns a playlist URL
if (sourceKey === 'Atlas' || sourceData.url.includes('cdn.vidrock.store/playlist/')) {
try {
const playlistRes = await ctx.proxiedFetcher<any>(sourceData.url, {
headers: {
...headers,
'User-Agent': userAgent,
},
});
let playlistData = playlistRes;
if (typeof playlistRes === 'string') {
try {
playlistData = JSON.parse(playlistRes);
} catch (e) {
continue;
}
}
if (Array.isArray(playlistData) && playlistData.length > 0) {
// Build qualities object from playlist
const qualities: Record<string, { type: 'mp4'; url: string }> = {};
for (const stream of playlistData) {
if (stream?.url && stream?.resolution) {
const resolution = stream.resolution.toString();
qualities[resolution] = {
type: 'mp4',
url: stream.url,
};
}
}
if (Object.keys(qualities).length > 0) {
const context = {
type: 'file',
qualities,
headers,
flags: [flags.CORS_ALLOWED],
captions: [],
};
embeds.push({
embedId: 'mirror',
url: JSON.stringify(context),
});
}
}
} catch (e) {
// If playlist fetch fails, skip this source
continue;
}
} else {
const embed = createMirrorEmbed(sourceKey, sourceData);
if (embed) embeds.push(embed);
}
}
}
if (embeds.length === 0) {
throw new NotFoundError('No valid sources found from Vidrock API');
}
return {
embeds,
};
}
export const vidrockScraper = makeSourcerer({
id: 'vidrock',
name: 'Granite',
rank: 170,
disabled: false,
flags: [],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,162 @@
import { load } from 'cheerio';
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const baseUrl = 'https://watchanimeworld.in';
const zephyrBaseUrl = 'https://play.zephyrflick.top';
const tmdbApiKey = '5b9790d9305dca8713b9a0afad42ea8d'; // Same key used in hianime
interface TMDBShowResponse {
name: string;
original_name: string;
}
interface TMDBMovieResponse {
title: string;
original_title: string;
}
interface ZephyrStreamResponse {
hls: boolean;
videoSource: string;
securedLink: string;
}
async function fetchTMDBData(tmdbId: string | number, mediaType: 'movie' | 'tv'): Promise<string> {
const endpoint = mediaType === 'movie' ? 'movie' : 'tv';
const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${tmdbApiKey}`);
if (!response.ok) {
throw new NotFoundError('Failed to fetch TMDB data');
}
const data = await response.json();
// Return the English title, falling back to original title
if (mediaType === 'movie') {
const movieData = data as TMDBMovieResponse;
return movieData.title || movieData.original_title;
}
const showData = data as TMDBShowResponse;
return showData.name || showData.original_name;
}
function normalizeTitle(title: string): string {
return title
.toLowerCase()
.normalize('NFD') // Decompose unicode characters
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
.trim();
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
// Determine if this is a TV show based on context
const isTVShow = 'season' in ctx.media;
const endpoint = isTVShow ? 'tv' : 'movie';
// Get the title from TMDB
const title = await fetchTMDBData(ctx.media.tmdbId, endpoint);
const normalizedTitle = normalizeTitle(title);
// Build the watchanimeworld URL
let watchUrl: string;
if (ctx.media.type === 'movie') {
watchUrl = `${baseUrl}/movies/${normalizedTitle}/`;
} else {
const season = ctx.media.season.number;
const episode = ctx.media.episode.number;
watchUrl = `${baseUrl}/episode/${normalizedTitle}-${season}x${episode}/`;
}
ctx.progress(30);
// Fetch the watch page
const watchPage = await ctx.proxiedFetcher(watchUrl, {
headers: {
'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',
},
});
// Extract iframe src
const $ = load(watchPage);
const iframeSrc = $('iframe[data-src]').attr('data-src') || $('iframe[src]').attr('src');
if (!iframeSrc) {
throw new NotFoundError('No iframe found on watch page');
}
// Extract the hash from the iframe URL
const hashMatch = iframeSrc.match(/\/video\/([a-f0-9]+)/);
if (!hashMatch) {
throw new NotFoundError('Could not extract video hash from iframe');
}
const videoHash = hashMatch[1];
ctx.progress(60);
// Construct the zephyrflick API URL
const apiUrl = `${zephyrBaseUrl}/player/index.php?data=${videoHash}&do=getVideo`;
// Fetch stream data
const streamResponse = await ctx.proxiedFetcher(apiUrl, {
method: 'POST',
headers: {
'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',
Referer: `${zephyrBaseUrl}/`,
Origin: zephyrBaseUrl,
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
},
body: `data=${videoHash}&do=getVideo`,
});
const streamData: ZephyrStreamResponse = JSON.parse(streamResponse);
if (!streamData.hls || !streamData.videoSource) {
throw new NotFoundError('No HLS stream found');
}
ctx.progress(90);
const streamHeaders = {
Referer: `${zephyrBaseUrl}/`,
Origin: zephyrBaseUrl,
'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',
};
// Return the stream
return {
embeds: [],
stream: [
{
id: 'primary',
type: 'hls',
playlist: streamData.videoSource,
headers: streamHeaders,
flags: [flags.CORS_ALLOWED],
captions: [],
},
],
};
}
export const watchanimeworldScraper = makeSourcerer({
id: 'watchanimeworld',
name: 'WatchAnimeWorld',
rank: 116,
disabled: false,
flags: [],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});

View file

@ -0,0 +1,119 @@
import { flags } from '@/entrypoint/utils/targets';
import { SourcererOutput, makeSourcerer } from '@/providers/base';
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
import { NotFoundError } from '@/utils/errors';
const baseUrl = 'https://ww1.yesmovies.ag';
function base64UrlEncode(str: string) {
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function encodeUtf8(str: string) {
return new TextEncoder().encode(str);
}
async function encox(plaintext: string, pwd: string): Promise<string> {
const pwdData = encodeUtf8(pwd);
const pwHash = await crypto.subtle.digest('SHA-256', pwdData);
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await crypto.subtle.importKey('raw', pwHash, { name: 'AES-GCM' }, false, ['encrypt']);
const plaintextData = encodeUtf8(plaintext);
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintextData);
// Web Crypto API includes the auth tag in the encrypted output for GCM
const encryptedArray = new Uint8Array(encrypted);
const full = new Uint8Array(iv.length + encryptedArray.length);
full.set(iv);
full.set(encryptedArray, iv.length);
return btoa(String.fromCharCode(...full));
}
async function generateDynamicHashURL(
mid: string,
eid: string,
sv: string,
plyURL: string,
ctx: ShowScrapeContext | MovieScrapeContext,
): Promise<string> {
const traceRes = await ctx.proxiedFetcher<string>('https://www.cloudflare.com/cdn-cgi/trace/');
const locMatch = traceRes.match(/^loc=(\w{2})/m);
const countryCode = locMatch ? locMatch[1] : 'US';
const timestamp = Math.floor(Date.now() / 1000);
const plaintextPayload = `${mid}+${eid}+${sv}+${countryCode}+${timestamp}`;
const encryptedData = await encox(plaintextPayload, countryCode);
const uriEncoded = base64UrlEncode(encryptedData);
const basePlayerURL = atob(plyURL);
const finalURL = `${basePlayerURL}/watch/?v${sv}${eid}#${uriEncoded}`;
return finalURL;
}
async function comboScraper(ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> {
const query = ctx.media.title.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '+');
const results = await ctx.proxiedFetcher(`${baseUrl}/searching?q=${query}&limit=60&offset=0`);
let data: { t: string; s: string; e: number; n: number }[];
if (results && results.data && Array.isArray(results.data)) {
data = results.data;
} else if (Array.isArray(results)) {
data = results;
} else {
throw new NotFoundError('Invalid search response');
}
let constructedSlug: string;
if (ctx.media.type === 'show') {
constructedSlug = `${ctx.media.title} - Season ${ctx.media.season.number}`;
} else {
constructedSlug = ctx.media.title;
}
const selectedSlug = data.filter((e) => e.t === constructedSlug)[0];
const idMatch = selectedSlug?.s.match(/-(\d+)$/);
const id = idMatch ? idMatch[1] : null;
if (id === null) {
throw new NotFoundError('No id found');
}
ctx.progress(20);
let embedUrl;
if (ctx.media.type === 'show') {
embedUrl = await generateDynamicHashURL(
id,
ctx.media.episode.number.toString(),
'1',
'aHR0cHM6Ly9tb3Z1bmEueHl6',
ctx,
);
} else {
embedUrl = await generateDynamicHashURL(id, '1', '1', 'aHR0cHM6Ly9tb3Z1bmEueHl6', ctx);
}
return {
embeds: [
{
embedId: 'movuna',
url: embedUrl,
},
],
};
}
export const yesmoviesScraper = makeSourcerer({
id: 'yesmovies',
name: 'YesMovies',
rank: 173,
disabled: false,
flags: [flags.CORS_ALLOWED],
scrapeMovie: comboScraper,
scrapeShow: comboScraper,
});