webstreamr-github/src/source/HDHub4u.ts
2026-03-27 22:03:14 +00:00

119 lines
4.2 KiB
TypeScript

import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode, Meta } from '../types';
import { Fetcher, findCountryCodes, getImdbId, Id, ImdbId } from '../utils';
import { resolveRedirectUrl } from './hd-hub-helper';
import { Source, SourceResult } from './Source';
interface SearchResponsePartial {
hits: {
document: {
imdb_id: string;
permalink: string;
post_title: string;
};
}[];
}
export class HDHub4u extends Source {
public readonly id = 'hdhub4u';
public readonly label = 'HDHub4u';
public readonly contentTypes: ContentType[] = ['movie', 'series'];
public readonly countryCodes: CountryCode[] = [CountryCode.multi, CountryCode.gu, CountryCode.hi, CountryCode.ml, CountryCode.pa, CountryCode.ta, CountryCode.te];
public readonly baseUrl = 'https://new5.hdhub4u.fo';
private readonly searchUrl = 'https://search.pingora.fyi';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrls = await this.fetchPageUrls(ctx, imdbId);
return (await Promise.all(
pageUrls.map(async (pageUrl) => {
return await this.handlePage(ctx, pageUrl, imdbId);
}),
)).flat();
};
private readonly handlePage = async (ctx: Context, pageUrl: URL, imdbId: ImdbId): Promise<SourceResult[]> => {
const html = await this.fetcher.text(ctx, pageUrl);
const $ = cheerio.load(html);
const meta = {
countryCodes: [CountryCode.multi, ...findCountryCodes($('div:contains("Language"):not(:has(div)):first').text())],
};
if (!imdbId.episode) {
return [
...this.extractHubDriveUrlResults(html, meta),
...(await Promise.all(
$('a[href*="gadgetsweb"]').map((_i, el) => this.handleHubLinks(ctx, new URL($(el).attr('href') as string), pageUrl, meta)),
)).flat(),
];
}
return [
...(await Promise.all(
$(`a:contains("EPiSODE ${imdbId.episode}"), a:contains("EPiSODE ${String(imdbId.episode).padStart(2, '0')}")`)
.map(async (_i, el) => this.handleHubLinks(ctx, new URL($(el).attr('href') as string), pageUrl, meta)),
)).flat(),
...this.extractHubDriveUrlResults(
$(`h4:contains("EPiSODE ${imdbId.episode}"), h4:contains("EPiSODE ${String(imdbId.episode).padStart(2, '0')}")`)
.first()
.nextUntil('hr')
.map((_i, el) => $.html(el))
.get()
.join(''),
meta,
),
];
};
private readonly handleHubLinks = async (ctx: Context, redirectUrl: URL, refererUrl: URL, meta: Meta): Promise<SourceResult[]> => {
const hubLinksUrl = await resolveRedirectUrl(ctx, this.fetcher, redirectUrl);
const hubLinksHtml = await this.fetcher.text(ctx, hubLinksUrl, { headers: { Referer: refererUrl.href } });
return [
...this.extractHubDriveUrlResults(hubLinksHtml, { ...meta, referer: hubLinksUrl.href }),
];
};
private readonly extractHubDriveUrlResults = (html: string, meta: Meta): SourceResult[] => {
const $ = cheerio.load(html);
return $('a[href*="hubdrive"]:not(:contains("⚡"))')
.map((_i, el) => ({ url: new URL($(el).attr('href') as string), meta }))
.toArray();
};
private readonly fetchPageUrls = async (ctx: Context, imdbId: ImdbId): Promise<URL[]> => {
const searchUrl = new URL(`/collections/post/documents/search?query_by=imdb_id&q=${encodeURIComponent(imdbId.id)}`, this.searchUrl);
const searchResponse = await this.fetcher.json(ctx, searchUrl, { headers: { Referer: this.baseUrl } }) as SearchResponsePartial;
return searchResponse.hits
.filter(hit =>
hit.document.imdb_id === imdbId.id
&& (
!imdbId.season
|| hit.document.post_title.includes(`Season ${imdbId.season}`)
|| hit.document.post_title.includes(`S${String(imdbId.season)}`)
|| hit.document.post_title.includes(`S${String(imdbId.season).padStart(2, '0')}`)
),
)
.map(hit => new URL(hit.document.permalink, this.baseUrl));
};
}