From af9d9c17cd7cfdbecb0ddfdcb394b01781b3569c Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 13 Mar 2024 00:12:18 +0100 Subject: [PATCH 1/4] 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'; + } +} From dd3942488c539bb246c237f81e4bf448b1534502 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 13 Mar 2024 00:19:21 +0100 Subject: [PATCH 2/4] cleanup --- src/providers/sources/hdrezka/index.ts | 30 +++++++------------------- src/utils/quality.ts | 2 +- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/providers/sources/hdrezka/index.ts b/src/providers/sources/hdrezka/index.ts index f93f285..353bafc 100644 --- a/src/providers/sources/hdrezka/index.ts +++ b/src/providers/sources/hdrezka/index.ts @@ -1,6 +1,7 @@ import { flags } from '@/entrypoint/utils/targets'; +import { ScrapeMedia } from '@/index'; import { SourcererOutput, makeSourcerer } from '@/providers/base'; -import { MovieScrapeContext, ScrapeContext, ShowScrapeContext } from '@/utils/context'; +import { MovieScrapeContext, ShowScrapeContext } from '@/utils/context'; import { NotFoundError } from '@/utils/errors'; import { VideoLinks } from './types'; @@ -12,30 +13,20 @@ const baseHeaders = { '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 { +async function searchAndFindMediaId(ctx: ShowScrapeContext | MovieScrapeContext): 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 }, + query: { q: ctx.media.title }, }); const movieData: { id: string | null; year: number | null; - type: 'show' | 'movie'; + type: ScrapeMedia['type']; }[] = []; for (const match of searchData.matchAll(itemRegexPattern)) { @@ -46,11 +37,11 @@ async function searchAndFindMediaId({ if (result !== null) { const id = url.match(idRegexPattern)?.[1] || null; - movieData.push({ id, year: result.year, type }); + movieData.push({ id, year: result.year, type: ctx.media.type }); } } - const filteredItems = movieData.filter((item) => item.type === type && item.year === releaseYear); + const filteredItems = movieData.filter((item) => item.type === ctx.media.type && item.year === ctx.media.releaseYear); return filteredItems[0]?.id || null; } @@ -84,12 +75,7 @@ async function getStream(id: string, ctx: ShowScrapeContext | MovieScrapeContext } 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, - }); + const id = await searchAndFindMediaId(ctx); if (!id) throw new NotFoundError('No result found'); const { url: streamUrl, subtitle: streamSubtitle } = await getStream(id, ctx); diff --git a/src/utils/quality.ts b/src/utils/quality.ts index 609f2e8..8854ca5 100644 --- a/src/utils/quality.ts +++ b/src/utils/quality.ts @@ -1,4 +1,4 @@ -import { Qualities } from '../../lib'; +import { Qualities } from '@/providers/streams'; export function getValidQualityFromString(quality: string): Qualities { switch (quality.toLowerCase().replace('p', '')) { From 845bf7c3dbb7b2a3088d0e1912ecca273a0e2f10 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 13 Mar 2024 00:27:49 +0100 Subject: [PATCH 3/4] Update types.ts --- src/providers/sources/hdrezka/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/providers/sources/hdrezka/types.ts b/src/providers/sources/hdrezka/types.ts index a72d85d..b578a4c 100644 --- a/src/providers/sources/hdrezka/types.ts +++ b/src/providers/sources/hdrezka/types.ts @@ -3,7 +3,6 @@ export type VideoLinks = { message: string; premium_content: number; url: string; - parseVideoLinks: string; quality: string; subtitle: boolean | string; subtitle_lns: boolean; From e1a0ce72cc997ca0214356b524e5c27ad20c6311 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 13 Mar 2024 00:30:02 +0100 Subject: [PATCH 4/4] add IP_LOCKED flag --- src/providers/sources/hdrezka/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/sources/hdrezka/index.ts b/src/providers/sources/hdrezka/index.ts index 353bafc..92f64fe 100644 --- a/src/providers/sources/hdrezka/index.ts +++ b/src/providers/sources/hdrezka/index.ts @@ -88,7 +88,7 @@ const universalScraper = async (ctx: ShowScrapeContext | MovieScrapeContext): Pr { id: 'primary', type: 'file', - flags: [flags.CORS_ALLOWED], + flags: [flags.CORS_ALLOWED, flags.IP_LOCKED], captions: parsedSubtitles, qualities: parsedVideos, },