mirror of
https://github.com/movie-web/providers.git
synced 2025-09-13 13:33:25 +00:00
add hdrezka
This commit is contained in:
@@ -27,6 +27,7 @@ import { vidCloudScraper } from './embeds/vidcloud';
|
|||||||
import { vidplayScraper } from './embeds/vidplay';
|
import { vidplayScraper } from './embeds/vidplay';
|
||||||
import { wootlyScraper } from './embeds/wootly';
|
import { wootlyScraper } from './embeds/wootly';
|
||||||
import { goojaraScraper } from './sources/goojara';
|
import { goojaraScraper } from './sources/goojara';
|
||||||
|
import { hdRezkaScraper } from './sources/hdrezka';
|
||||||
import { nepuScraper } from './sources/nepu';
|
import { nepuScraper } from './sources/nepu';
|
||||||
import { ridooMoviesScraper } from './sources/ridomovies';
|
import { ridooMoviesScraper } from './sources/ridomovies';
|
||||||
import { smashyStreamScraper } from './sources/smashystream';
|
import { smashyStreamScraper } from './sources/smashystream';
|
||||||
@@ -48,6 +49,7 @@ export function gatherAllSources(): Array<Sourcerer> {
|
|||||||
vidSrcToScraper,
|
vidSrcToScraper,
|
||||||
nepuScraper,
|
nepuScraper,
|
||||||
goojaraScraper,
|
goojaraScraper,
|
||||||
|
hdRezkaScraper,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
120
src/providers/sources/hdrezka/index.ts
Normal file
120
src/providers/sources/hdrezka/index.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { flags } from '@/entrypoint/utils/targets';
|
||||||
|
import { SourcererOutput, makeSourcerer } from '@/providers/base';
|
||||||
|
import { MovieScrapeContext, ScrapeContext, 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({
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
releaseYear,
|
||||||
|
ctx,
|
||||||
|
}: {
|
||||||
|
type: 'show' | 'movie';
|
||||||
|
title: string;
|
||||||
|
releaseYear: number;
|
||||||
|
ctx: ScrapeContext;
|
||||||
|
}): 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: title },
|
||||||
|
});
|
||||||
|
|
||||||
|
const movieData: {
|
||||||
|
id: string | null;
|
||||||
|
year: number | null;
|
||||||
|
type: 'show' | 'movie';
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = movieData.filter((item) => item.type === type && item.year === 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({
|
||||||
|
type: ctx.media.type === 'show' ? 'show' : 'movie',
|
||||||
|
title: ctx.media.title,
|
||||||
|
releaseYear: ctx.media.releaseYear,
|
||||||
|
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],
|
||||||
|
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,
|
||||||
|
});
|
12
src/providers/sources/hdrezka/types.ts
Normal file
12
src/providers/sources/hdrezka/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export type VideoLinks = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
premium_content: number;
|
||||||
|
url: string;
|
||||||
|
parseVideoLinks: string;
|
||||||
|
quality: string;
|
||||||
|
subtitle: boolean | string;
|
||||||
|
subtitle_lns: boolean;
|
||||||
|
subtitle_def: boolean;
|
||||||
|
thumbnails: string;
|
||||||
|
};
|
76
src/providers/sources/hdrezka/utils.ts
Normal file
76
src/providers/sources/hdrezka/utils.ts
Normal 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
20
src/utils/quality.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Qualities } from '../../lib';
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user