mirror of
https://github.com/p-stream/providers.git
synced 2026-03-11 09:45:36 +00:00
add sources
This commit is contained in:
parent
33903e2b00
commit
6b24427c8f
8 changed files with 637 additions and 21 deletions
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
113
src/providers/sources/vidlink.ts
Normal file
113
src/providers/sources/vidlink.ts
Normal 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,
|
||||
});
|
||||
165
src/providers/sources/vidrock.ts
Normal file
165
src/providers/sources/vidrock.ts
Normal 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,
|
||||
});
|
||||
162
src/providers/sources/watchanimeworld.ts
Normal file
162
src/providers/sources/watchanimeworld.ts
Normal 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,
|
||||
});
|
||||
119
src/providers/sources/yesmovies.ts
Normal file
119
src/providers/sources/yesmovies.ts
Normal 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,
|
||||
});
|
||||
Loading…
Reference in a new issue