From af9d9c17cd7cfdbecb0ddfdcb394b01781b3569c Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 13 Mar 2024 00:12:18 +0100 Subject: [PATCH] add hdrezka --- src/providers/all.ts | 2 + src/providers/sources/hdrezka/index.ts | 120 +++++++++++++++++++++++++ src/providers/sources/hdrezka/types.ts | 12 +++ src/providers/sources/hdrezka/utils.ts | 76 ++++++++++++++++ src/utils/quality.ts | 20 +++++ 5 files changed, 230 insertions(+) create mode 100644 src/providers/sources/hdrezka/index.ts create mode 100644 src/providers/sources/hdrezka/types.ts create mode 100644 src/providers/sources/hdrezka/utils.ts create mode 100644 src/utils/quality.ts diff --git a/src/providers/all.ts b/src/providers/all.ts index c93a669..7725558 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -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 { vidSrcToScraper, nepuScraper, goojaraScraper, + hdRezkaScraper, ]; } diff --git a/src/providers/sources/hdrezka/index.ts b/src/providers/sources/hdrezka/index.ts new file mode 100644 index 0000000..f93f285 --- /dev/null +++ b/src/providers/sources/hdrezka/index.ts @@ -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 { + const itemRegexPattern = /([^<]+)<\/span> \(([^)]+)\)/g; + const idRegexPattern = /\/(\d+)-[^/]+\.html$/; + + const searchData = await ctx.proxiedFetcher(`/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 { + 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('/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 => { + 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, +}); diff --git a/src/providers/sources/hdrezka/types.ts b/src/providers/sources/hdrezka/types.ts new file mode 100644 index 0000000..a72d85d --- /dev/null +++ b/src/providers/sources/hdrezka/types.ts @@ -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; +}; diff --git a/src/providers/sources/hdrezka/utils.ts b/src/providers/sources/hdrezka/utils.ts new file mode 100644 index 0000000..f8c7589 --- /dev/null +++ b/src/providers/sources/hdrezka/utils.ts @@ -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 }; diff --git a/src/utils/quality.ts b/src/utils/quality.ts new file mode 100644 index 0000000..609f2e8 --- /dev/null +++ b/src/utils/quality.ts @@ -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'; + } +}