commit
f9cc1f6bf6
|
@ -27,6 +27,7 @@ import { vidCloudScraper } from './embeds/vidcloud';
|
|||
import { vidplayScraper } from './embeds/vidplay';
|
||||
import { wootlyScraper } from './embeds/wootly';
|
||||
import { goojaraScraper } from './sources/goojara';
|
||||
import { hdRezkaScraper } from './sources/hdrezka';
|
||||
import { nepuScraper } from './sources/nepu';
|
||||
import { ridooMoviesScraper } from './sources/ridomovies';
|
||||
import { smashyStreamScraper } from './sources/smashystream';
|
||||
|
@ -48,6 +49,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
|||
vidSrcToScraper,
|
||||
nepuScraper,
|
||||
goojaraScraper,
|
||||
hdRezkaScraper,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { flags } from '@/entrypoint/utils/targets';
|
||||
import { ScrapeMedia } from '@/index';
|
||||
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||
import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context';
|
||||
import { NotFoundError } from '@/utils/errors';
|
||||
|
||||
import { VideoLinks } from './types';
|
||||
import { extractTitleAndYear, generateRandomFavs, parseSubtitleLinks, parseVideoLinks } from './utils';
|
||||
|
||||
const rezkaBase = 'https://hdrzk.org';
|
||||
const baseHeaders = {
|
||||
'X-Hdrezka-Android-App': '1',
|
||||
'X-Hdrezka-Android-App-Version': '2.2.0',
|
||||
};
|
||||
|
||||
async function searchAndFindMediaId(ctx: ShowScrapeContext | MovieScrapeContext): Promise<string | null> {
|
||||
const itemRegexPattern = /<a href="([^"]+)"><span class="enty">([^<]+)<\/span> \(([^)]+)\)/g;
|
||||
const idRegexPattern = /\/(\d+)-[^/]+\.html$/;
|
||||
|
||||
const searchData = await ctx.proxiedFetcher<string>(`/engine/ajax/search.php`, {
|
||||
baseUrl: rezkaBase,
|
||||
headers: baseHeaders,
|
||||
query: { q: ctx.media.title },
|
||||
});
|
||||
|
||||
const movieData: {
|
||||
id: string | null;
|
||||
year: number | null;
|
||||
type: ScrapeMedia['type'];
|
||||
}[] = [];
|
||||
|
||||
for (const match of searchData.matchAll(itemRegexPattern)) {
|
||||
const url = match[1];
|
||||
const titleAndYear = match[3];
|
||||
|
||||
const result = extractTitleAndYear(titleAndYear);
|
||||
if (result !== null) {
|
||||
const id = url.match(idRegexPattern)?.[1] || null;
|
||||
|
||||
movieData.push({ id, year: result.year, type: ctx.media.type });
|
||||
}
|
||||
}
|
||||
|
||||
const filteredItems = movieData.filter((item) => item.type === ctx.media.type && item.year === ctx.media.releaseYear);
|
||||
|
||||
return filteredItems[0]?.id || null;
|
||||
}
|
||||
|
||||
async function getStream(id: string, ctx: ShowScrapeContext | MovieScrapeContext): Promise<VideoLinks> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('id', id);
|
||||
// Translator ID 238 represents the Original + subtitles player.
|
||||
searchParams.append('translator_id', '238');
|
||||
if (ctx.media.type === 'show') {
|
||||
searchParams.append('season', ctx.media.season.number.toString());
|
||||
searchParams.append('episode', ctx.media.episode.number.toString());
|
||||
}
|
||||
if (ctx.media.type === 'movie') {
|
||||
searchParams.append('is_camprip', '0');
|
||||
searchParams.append('is_ads', '0');
|
||||
searchParams.append('is_director', '0');
|
||||
}
|
||||
searchParams.append('favs', generateRandomFavs());
|
||||
searchParams.append('action', ctx.media.type === 'show' ? 'get_stream' : 'get_movie');
|
||||
|
||||
const response = await ctx.proxiedFetcher<string>('/ajax/get_cdn_series/', {
|
||||
baseUrl: rezkaBase,
|
||||
method: 'POST',
|
||||
body: searchParams,
|
||||
headers: baseHeaders,
|
||||
});
|
||||
|
||||
// Response content-type is text/html, but it's actually JSON
|
||||
return JSON.parse(response);
|
||||
}
|
||||
|
||||
const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Promise<SourcererOutput> => {
|
||||
const id = await searchAndFindMediaId(ctx);
|
||||
if (!id) throw new NotFoundError('No result found');
|
||||
|
||||
const { url: streamUrl, subtitle: streamSubtitle } = await getStream(id, ctx);
|
||||
const parsedVideos = parseVideoLinks(streamUrl);
|
||||
const parsedSubtitles = parseSubtitleLinks(streamSubtitle);
|
||||
|
||||
return {
|
||||
embeds: [],
|
||||
stream: [
|
||||
{
|
||||
id: 'primary',
|
||||
type: 'file',
|
||||
flags: [flags.CORS_ALLOWED, flags.IP_LOCKED],
|
||||
captions: parsedSubtitles,
|
||||
qualities: parsedVideos,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const hdRezkaScraper = makeSourcerer({
|
||||
id: 'hdrezka',
|
||||
name: 'HDRezka',
|
||||
rank: 195,
|
||||
flags: [flags.CORS_ALLOWED, flags.IP_LOCKED],
|
||||
scrapeShow: universalScraper,
|
||||
scrapeMovie: universalScraper,
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
export type VideoLinks = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
premium_content: number;
|
||||
url: string;
|
||||
quality: string;
|
||||
subtitle: boolean | string;
|
||||
subtitle_lns: boolean;
|
||||
subtitle_def: boolean;
|
||||
thumbnails: string;
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
import { getCaptionTypeFromUrl, labelToLanguageCode } from '@/providers/captions';
|
||||
import { FileBasedStream } from '@/providers/streams';
|
||||
import { NotFoundError } from '@/utils/errors';
|
||||
import { getValidQualityFromString } from '@/utils/quality';
|
||||
|
||||
function generateRandomFavs(): string {
|
||||
const randomHex = () => Math.floor(Math.random() * 16).toString(16);
|
||||
const generateSegment = (length: number) => Array.from({ length }, randomHex).join('');
|
||||
|
||||
return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(
|
||||
12,
|
||||
)}`;
|
||||
}
|
||||
|
||||
function parseSubtitleLinks(inputString?: string | boolean): FileBasedStream['captions'] {
|
||||
if (!inputString || typeof inputString === 'boolean') return [];
|
||||
const linksArray = inputString.split(',');
|
||||
const captions: FileBasedStream['captions'] = [];
|
||||
|
||||
linksArray.forEach((link) => {
|
||||
const match = link.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/);
|
||||
|
||||
if (match) {
|
||||
const type = getCaptionTypeFromUrl(match[2]);
|
||||
const language = labelToLanguageCode(match[1]);
|
||||
if (!type || !language) return;
|
||||
|
||||
captions.push({
|
||||
id: match[2],
|
||||
language,
|
||||
hasCorsRestrictions: false,
|
||||
type,
|
||||
url: match[2],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return captions;
|
||||
}
|
||||
|
||||
function parseVideoLinks(inputString?: string): FileBasedStream['qualities'] {
|
||||
if (!inputString) throw new NotFoundError('No video links found');
|
||||
const linksArray = inputString.split(',');
|
||||
const result: FileBasedStream['qualities'] = {};
|
||||
|
||||
linksArray.forEach((link) => {
|
||||
const match = link.match(/\[([^]+)](https?:\/\/[^\s,]+\.mp4)/);
|
||||
if (match) {
|
||||
const qualityText = match[1];
|
||||
const mp4Url = match[2];
|
||||
|
||||
const numericQualityMatch = qualityText.match(/(\d+p)/);
|
||||
const quality = numericQualityMatch ? numericQualityMatch[1] : 'Unknown';
|
||||
|
||||
console.log(quality, mp4Url);
|
||||
const validQuality = getValidQualityFromString(quality);
|
||||
result[validQuality] = { type: 'mp4', url: mp4Url };
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractTitleAndYear(input: string) {
|
||||
const regex = /^(.*?),.*?(\d{4})/;
|
||||
const match = input.match(regex);
|
||||
|
||||
if (match) {
|
||||
const title = match[1];
|
||||
const year = match[2];
|
||||
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { extractTitleAndYear, parseSubtitleLinks, parseVideoLinks, generateRandomFavs };
|
|
@ -0,0 +1,20 @@
|
|||
import { Qualities } from '@/providers/streams';
|
||||
|
||||
export function getValidQualityFromString(quality: string): Qualities {
|
||||
switch (quality.toLowerCase().replace('p', '')) {
|
||||
case '360':
|
||||
return '360';
|
||||
case '480':
|
||||
return '480';
|
||||
case '720':
|
||||
return '720';
|
||||
case '1080':
|
||||
return '1080';
|
||||
case '2160':
|
||||
return '4k';
|
||||
case '4k':
|
||||
return '4k';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue