mirror of
https://github.com/p-stream/providers.git
synced 2026-03-11 17:55: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 { turbovidSourceScraper } from './sources/turbovid';
|
||||||
import { vidapiClickScraper } from './sources/vidapiclick';
|
import { vidapiClickScraper } from './sources/vidapiclick';
|
||||||
import { vidifyScraper } from './sources/vidify';
|
import { vidifyScraper } from './sources/vidify';
|
||||||
|
import { vidlinkScraper } from './sources/vidlink';
|
||||||
import { vidnestScraper } from './sources/vidnest';
|
import { vidnestScraper } from './sources/vidnest';
|
||||||
|
import { vidrockScraper } from './sources/vidrock';
|
||||||
import { warezcdnScraper } from './sources/warezcdn';
|
import { warezcdnScraper } from './sources/warezcdn';
|
||||||
|
import { watchanimeworldScraper } from './sources/watchanimeworld';
|
||||||
import { wecimaScraper } from './sources/wecima';
|
import { wecimaScraper } from './sources/wecima';
|
||||||
|
import { yesmoviesScraper } from './sources/yesmovies';
|
||||||
import { zunimeScraper } from './sources/zunime';
|
import { zunimeScraper } from './sources/zunime';
|
||||||
|
|
||||||
export function gatherAllSources(): Array<Sourcerer> {
|
export function gatherAllSources(): Array<Sourcerer> {
|
||||||
|
|
@ -138,6 +142,10 @@ export function gatherAllSources(): Array<Sourcerer> {
|
||||||
debridScraper,
|
debridScraper,
|
||||||
cinehdplusScraper,
|
cinehdplusScraper,
|
||||||
fullhdfilmizleScraper,
|
fullhdfilmizleScraper,
|
||||||
|
vidlinkScraper,
|
||||||
|
yesmoviesScraper,
|
||||||
|
vidrockScraper,
|
||||||
|
watchanimeworldScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export const apiBaseUrl = 'https://borg.rips.cc';
|
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';
|
export const password = 'defonotscraping';
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export const lookmovieScraper = makeSourcerer({
|
||||||
id: 'lookmovie',
|
id: 'lookmovie',
|
||||||
name: 'LookMovie',
|
name: 'LookMovie',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
rank: 170,
|
rank: 171,
|
||||||
flags: [flags.IP_LOCKED],
|
flags: [flags.IP_LOCKED],
|
||||||
scrapeShow: universalScraper,
|
scrapeShow: universalScraper,
|
||||||
scrapeMovie: universalScraper,
|
scrapeMovie: universalScraper,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
|
|
||||||
import { SourcererEmbed, makeSourcerer } from '@/providers/base';
|
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 { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||||
import { NotFoundError } from '@/utils/errors';
|
import { NotFoundError } from '@/utils/errors';
|
||||||
|
|
||||||
|
|
@ -11,6 +9,14 @@ import { IframeSourceResult, SearchResult } from './types';
|
||||||
const ridoMoviesBase = `https://ridomovies.tv`;
|
const ridoMoviesBase = `https://ridomovies.tv`;
|
||||||
const ridoMoviesApiBase = `${ridoMoviesBase}/core/api`;
|
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 universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) => {
|
||||||
const searchResult = await ctx.proxiedFetcher<SearchResult>('/search', {
|
const searchResult = await ctx.proxiedFetcher<SearchResult>('/search', {
|
||||||
baseUrl: ridoMoviesApiBase,
|
baseUrl: ridoMoviesApiBase,
|
||||||
|
|
@ -18,14 +24,37 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
|
||||||
q: ctx.media.title,
|
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 mediaData = searchResult.data.items.map((movieEl) => {
|
||||||
const name = movieEl.title;
|
const name = movieEl.title;
|
||||||
const year = movieEl.contentable.releaseYear;
|
const year = movieEl.contentable.releaseYear;
|
||||||
const fullSlug = movieEl.fullSlug;
|
const fullSlug = movieEl.fullSlug;
|
||||||
return { name, year, 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);
|
ctx.progress(40);
|
||||||
|
|
||||||
let iframeSourceUrl = `/${targetMedia.fullSlug}/videos`;
|
let iframeSourceUrl = `/${targetMedia.fullSlug}/videos`;
|
||||||
|
|
@ -34,14 +63,20 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
|
||||||
const showPageResult = await ctx.proxiedFetcher<string>(`/${targetMedia.fullSlug}`, {
|
const showPageResult = await ctx.proxiedFetcher<string>(`/${targetMedia.fullSlug}`, {
|
||||||
baseUrl: ridoMoviesBase,
|
baseUrl: ridoMoviesBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullEpisodeSlug = `season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`;
|
const fullEpisodeSlug = `season-${ctx.media.season.number}/episode-${ctx.media.episode.number}`;
|
||||||
const regexPattern = new RegExp(
|
const regexPattern = new RegExp(
|
||||||
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\\\"fullSlug\\\\\\":\\\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\\\")`,
|
`\\\\"id\\\\":\\\\"(\\d+)\\\\"(?=.*?\\\\"fullSlug\\\\":\\\\"[^"]*${fullEpisodeSlug}[^"]*\\\\")`,
|
||||||
'g',
|
'g',
|
||||||
);
|
);
|
||||||
|
|
||||||
const matches = [...showPageResult.matchAll(regexPattern)];
|
const matches = [...showPageResult.matchAll(regexPattern)];
|
||||||
const episodeIds = matches.map((match) => match[1]);
|
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];
|
const episodeId = episodeIds[episodeIds.length - 1];
|
||||||
iframeSourceUrl = `/episodes/${episodeId}/videos`;
|
iframeSourceUrl = `/episodes/${episodeId}/videos`;
|
||||||
}
|
}
|
||||||
|
|
@ -49,24 +84,38 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
|
||||||
const iframeSource = await ctx.proxiedFetcher<IframeSourceResult>(iframeSourceUrl, {
|
const iframeSource = await ctx.proxiedFetcher<IframeSourceResult>(iframeSourceUrl, {
|
||||||
baseUrl: ridoMoviesApiBase,
|
baseUrl: ridoMoviesApiBase,
|
||||||
});
|
});
|
||||||
|
if (!iframeSource.data || iframeSource.data.length === 0) {
|
||||||
|
throw new NotFoundError('No video sources found');
|
||||||
|
}
|
||||||
|
|
||||||
const iframeSource$ = load(iframeSource.data[0].url);
|
const iframeSource$ = load(iframeSource.data[0].url);
|
||||||
const iframeUrl = iframeSource$('iframe').attr('data-src');
|
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);
|
ctx.progress(60);
|
||||||
|
|
||||||
const embeds: SourcererEmbed[] = [];
|
const embeds: SourcererEmbed[] = [];
|
||||||
if (iframeUrl.includes('closeload')) {
|
|
||||||
embeds.push({
|
let embedId = 'closeload';
|
||||||
embedId: closeLoadScraper.id,
|
|
||||||
url: iframeUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (iframeUrl.includes('ridoo')) {
|
if (iframeUrl.includes('ridoo')) {
|
||||||
embeds.push({
|
embedId = 'ridoo';
|
||||||
embedId: ridooScraper.id,
|
|
||||||
url: iframeUrl,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
embeds.push({
|
||||||
|
embedId,
|
||||||
|
url: iframeUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.progress(80);
|
||||||
|
|
||||||
|
if (embeds.length === 0) {
|
||||||
|
throw new NotFoundError('No supported embeds found');
|
||||||
|
}
|
||||||
|
|
||||||
ctx.progress(90);
|
ctx.progress(90);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -77,9 +126,9 @@ const universalScraper = async (ctx: MovieScrapeContext | ShowScrapeContext) =>
|
||||||
export const ridooMoviesScraper = makeSourcerer({
|
export const ridooMoviesScraper = makeSourcerer({
|
||||||
id: 'ridomovies',
|
id: 'ridomovies',
|
||||||
name: 'RidoMovies',
|
name: 'RidoMovies',
|
||||||
rank: 210,
|
rank: 203,
|
||||||
flags: [],
|
flags: [],
|
||||||
disabled: true,
|
disabled: false,
|
||||||
scrapeMovie: universalScraper,
|
scrapeMovie: universalScraper,
|
||||||
scrapeShow: 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