119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
import crypto from 'crypto';
|
|
// eslint-disable-next-line import/no-named-as-default
|
|
import KeyvSqlite from '@keyv/sqlite';
|
|
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
|
import * as cheerio from 'cheerio';
|
|
import { JSDOM } from 'jsdom';
|
|
import { ContentType } from 'stremio-addon-sdk';
|
|
import { Context, CountryCode } from '../types';
|
|
import { Fetcher, getCacheDir, getImdbId, Id, ImdbId } from '../utils';
|
|
import { Source, SourceResult } from './Source';
|
|
|
|
export class PrimeWire extends Source {
|
|
public readonly id = 'primewire';
|
|
|
|
public readonly label = 'PrimeWire';
|
|
|
|
public readonly contentTypes: ContentType[] = ['movie', 'series'];
|
|
|
|
public readonly countryCodes: CountryCode[] = [CountryCode.en];
|
|
|
|
public readonly baseUrl = 'https://www.primewire.tf';
|
|
|
|
private readonly fetcher: Fetcher;
|
|
|
|
private readonly redirectUrlCache = new Cacheable({
|
|
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
|
secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-primewire-redirect-url-cache.sqlite`)),
|
|
});
|
|
|
|
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);
|
|
|
|
// We need any non-protected entrypoint to grab the app.js
|
|
const html = await this.fetcher.text(ctx, new URL('/legal', this.baseUrl));
|
|
const appJsMatch = html.match(/src="(.*?app-.*\.js.*?)"/) as string[];
|
|
const appJs = await this.fetcher.text(ctx, new URL(appJsMatch[1] as string, this.baseUrl));
|
|
|
|
const pageUrl = await this.fetchPageUrl(ctx, appJs, imdbId);
|
|
if (!pageUrl) {
|
|
return [];
|
|
}
|
|
|
|
const pageHtml = await this.fetcher.text(ctx, pageUrl);
|
|
|
|
let linksHtml = pageHtml;
|
|
if (imdbId.season) {
|
|
const episodeUrl = await this.fetchEpisodeUrl(pageHtml, imdbId);
|
|
if (!episodeUrl) {
|
|
return [];
|
|
}
|
|
|
|
linksHtml = await this.fetcher.text(ctx, episodeUrl);
|
|
}
|
|
|
|
// Yes, we are aware of the risks.. 😰
|
|
const jsdom = new JSDOM(linksHtml, { runScripts: 'dangerously' });
|
|
const scriptElement = jsdom.window.document.createElement('script');
|
|
scriptElement.textContent = appJs;
|
|
jsdom.window.document.body.appendChild(scriptElement);
|
|
|
|
const $ = cheerio.load(jsdom.window.document.documentElement.outerHTML);
|
|
|
|
const linksTokenMatch = appJs.match(/t="(0\.x.*?)"/) as string[];
|
|
const linksToken = linksTokenMatch[1] as string;
|
|
|
|
return Promise.all(
|
|
$(`a.go-link`)
|
|
.map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl))
|
|
.toArray()
|
|
.map(async (redirectUrl) => {
|
|
let targetUrlHref = await this.redirectUrlCache.get<string>(redirectUrl.href);
|
|
|
|
/* istanbul ignore if */
|
|
if (!targetUrlHref) {
|
|
const linkFetchUrl = new URL(redirectUrl.href.replace('/gos/', '/go/'));
|
|
linkFetchUrl.searchParams.set('token', linksToken);
|
|
targetUrlHref = JSON.parse(await this.fetcher.text(ctx, linkFetchUrl))['link'] as string;
|
|
|
|
await this.redirectUrlCache.set<string>(redirectUrl.href, targetUrlHref);
|
|
}
|
|
|
|
return { countryCode: CountryCode.en, url: new URL(targetUrlHref) };
|
|
}),
|
|
);
|
|
};
|
|
|
|
private readonly fetchPageUrl = async (ctx: Context, appJs: string, imdbId: ImdbId): Promise<URL | undefined> => {
|
|
const sha1SuffixMatch = appJs.match(/s\.value\+"(.*?)"/) as string[];
|
|
|
|
const ds = crypto.createHash('sha1')
|
|
.update(imdbId.id + sha1SuffixMatch[1])
|
|
.digest('hex')
|
|
.slice(0, 10);
|
|
|
|
const searchResults = await this.fetcher.text(ctx, new URL(`/filter?s=${encodeURIComponent(imdbId.id)}&ds=${encodeURIComponent(ds)}`, this.baseUrl));
|
|
|
|
const $ = cheerio.load(searchResults);
|
|
|
|
return $('.index_item a')
|
|
.map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl))
|
|
.get(0);
|
|
};
|
|
|
|
private readonly fetchEpisodeUrl = async (pageHtml: string, imdbId: ImdbId): Promise<URL | undefined> => {
|
|
const $ = cheerio.load(pageHtml);
|
|
|
|
return $(`div[data-id="${imdbId.season}"]`)
|
|
.children(`.tv_episode_item:nth-child(${imdbId.episode})`)
|
|
.children('a[href]')
|
|
.map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl))
|
|
.get(0);
|
|
};
|
|
}
|