Merge pull request #110 from movie-web/feature/hdrezka

Add hdrezka
This commit is contained in:
Jorrin 2024-03-13 00:36:19 +01:00 committed by GitHub
commit f9cc1f6bf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 215 additions and 0 deletions

View File

@ -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,
];
}

View File

@ -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,
});

View File

@ -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;
};

View File

@ -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 };

20
src/utils/quality.ts Normal file
View File

@ -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';
}
}