feat: extract embed handling

This commit is contained in:
WebStreamr 2025-05-10 22:59:12 +00:00
parent 841f2a0811
commit f62fa944bb
No known key found for this signature in database
7 changed files with 41 additions and 49 deletions

View file

@ -10,14 +10,13 @@ export class Dropload implements EmbedExtractor {
const normalizedUrl = url.replace('/e/', '').replace('/embed-', '/');
const html = await cachedFetchText(normalizedUrl);
const resolutionMatch = html.match(/(\d{3,}x\d{3,}),/);
const resolution = resolutionMatch && resolutionMatch[1] ? scanFromResolution(resolutionMatch[1]) : undefined;
const resolution = scanFromResolution((html.match(/(\d{3,}x\d{3,}),/) as string[])[1] as string);
const sizeMatch = html.match(/([\d.]+) ?([GM]B)/);
const size = sizeMatch && sizeMatch[1] && sizeMatch[2] ? `${sizeMatch[1]} ${sizeMatch[2]}` : undefined;
const sizeMatch = html.match(/([\d.]+) ?([GM]B)/) as string[];
const size = `${sizeMatch[1]} ${sizeMatch[2]}`;
return {
url: await extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
name: `WebStreamr ${resolution}`,
title: `${this.label} | 💾 ${size} | ${iso2ToFlag(language)}`,
behaviorHints: {

View file

@ -0,0 +1,10 @@
import { EmbedExtractor } from './types';
import { Dropload } from './Dropload';
import { SuperVideo } from './SuperVideo';
type EmbedExtractorRegistryType = Record<string, EmbedExtractor>;
export const EmbedExtractorRegistry: EmbedExtractorRegistryType = {
dropload: new Dropload(),
supervideo: new SuperVideo(),
};

View file

@ -7,18 +7,15 @@ export class SuperVideo implements EmbedExtractor {
readonly label = 'SuperVideo';
readonly extract = async (url: string, language: string) => {
const normalizedUrl = url.replace('/e/', '').replace('/embed-', '/');
const normalizedUrl = url.replace('/e/', '/').replace('/embed-', '/');
const html = await cachedFetchText(normalizedUrl);
// TODO: resolution and size can be merged: <span class="downloadbox__size">1280x720, 699.8 MB</span>
const resolutionMatch = html.match(/(\d{3,}x\d{3,}),/);
const resolution = resolutionMatch && resolutionMatch[1] ? scanFromResolution(resolutionMatch[1]) : undefined;
const sizeMatch = html.match(/([\d.]+) ?([GM]B)/);
const size = sizeMatch && sizeMatch[1] && sizeMatch[2] ? `${sizeMatch[1]} ${sizeMatch[2]}` : undefined;
const resolutionAndSizeMatch = html.match(/(\d{3,}x\d{3,}), ([\d.]+) ?([GM]B)/) as string[];
const resolution = scanFromResolution(resolutionAndSizeMatch[1] as string);
const size = `${resolutionAndSizeMatch[2]} ${resolutionAndSizeMatch[3]}`;
return {
url: await extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
name: `WebStreamr ${resolution}`,
title: `${this.label} | 💾 ${size} | ${iso2ToFlag(language)}`,
behaviorHints: {

View file

@ -1,10 +1,2 @@
import { Dropload } from './Dropload';
import { SuperVideo } from './SuperVideo';
import { EmbedExtractor } from './types';
type EmbedExtractorRegistryType = Record<string, EmbedExtractor>;
export const EmbedExtractorRegistry: EmbedExtractorRegistryType = {
dropload: new Dropload(),
supervideo: new SuperVideo(),
};
export * from './EmbedExtractorRegistry';
export * from './types';

View file

@ -2,7 +2,7 @@ import * as cheerio from 'cheerio';
import slugify from 'slugify';
import { Handler } from './types';
import { ImdbId, cachedFetchText, fulfillAllPromises, parseImdbId } from '../utils';
import { EmbedExtractorRegistry } from '../embed-extractor';
import { EmbedExtractor, EmbedExtractorRegistry } from '../embed-extractor';
export class KinoKiste implements Handler {
readonly id = 'kinokiste';
@ -30,20 +30,16 @@ export class KinoKiste implements Handler {
const $ = cheerio.load(html);
return fulfillAllPromises(
$(`[data-num="${imdbId.series}x${imdbId.episode}"]`).map((_i, urlWrapperElement) => {
return $(urlWrapperElement).siblings('.mirrors').children('[data-link]')
.map((_i, urlElement) => ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
embedId: slugify($(urlElement).attr('data-m')!),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
embedUrl: $(urlElement).attr('data-link')!
.replace(/^(https:)?\/\//, 'https://'),
}))
.toArray()
.filter(({ embedId }) => embedId.match(/^(dropload|supervideo)$/));
})
$(`[data-num="${imdbId.series}x${imdbId.episode}"]`)
.siblings('.mirrors')
.children('[data-link]')
.map((_i, urlElement) => ({
embedId: slugify($(urlElement).attr('data-m') as string),
embedUrl: ($(urlElement).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'),
}))
.toArray()
.map(({ embedId, embedUrl }) => EmbedExtractorRegistry[embedId]?.extract(embedUrl, 'de'))
.filter(({ embedId }) => embedId.match(/^(dropload|supervideo)$/))
.map(({ embedId, embedUrl }) => (EmbedExtractorRegistry[embedId] as EmbedExtractor).extract(embedUrl, 'de'))
.filter(stream => stream !== undefined),
);
};

View file

@ -2,7 +2,7 @@ import * as cheerio from 'cheerio';
import slugify from 'slugify';
import { Handler } from './types';
import { cachedFetchText, fulfillAllPromises, parseImdbId } from '../utils';
import { EmbedExtractorRegistry } from '../embed-extractor';
import { EmbedExtractor, EmbedExtractorRegistry } from '../embed-extractor';
export class MeineCloud implements Handler {
readonly id = 'meinecloud';
@ -26,13 +26,11 @@ export class MeineCloud implements Handler {
$('[data-link!=""]')
.map((_i, el) => ({
embedId: slugify($(el).text()),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
embedUrl: $(el).attr('data-link')!
.replace(/^(https:)?\/\//, 'https://'),
embedUrl: ($(el).attr('data-link') as string).replace(/^(https:)?\/\//, 'https://'),
}))
.toArray()
.filter(({ embedId }) => embedId.match(/^(dropload|supervideo)$/))
.map(({ embedId, embedUrl }) => EmbedExtractorRegistry[embedId]?.extract(embedUrl, 'de'))
.map(({ embedId, embedUrl }) => (EmbedExtractorRegistry[embedId] as EmbedExtractor).extract(embedUrl, 'de'))
.filter(stream => stream !== undefined),
);
};

View file

@ -1,5 +1,4 @@
import { unpack } from 'unpacker';
import { cachedFetchText } from './fetch';
import { extractUrlFromPacked } from './embed';
jest.mock('unpacker', () => ({
@ -8,21 +7,22 @@ jest.mock('unpacker', () => ({
describe('extractUrlFromPacked', () => {
test('throw if no embed was found', () => {
expect(extractUrlFromPacked('', [/eval/])).rejects.toThrow('No p.a.c.k.e.d string found on https://some-url.test');
expect(() => {
extractUrlFromPacked('', [/sources:\[{file:"(.*?)"/]);
}).toThrow('No p.a.c.k.e.d string found');
});
test('throw if no link was found', () => {
(unpack as jest.Mock).mockReturnValue('some-unexpected-data');
expect(extractUrlFromPacked('eval(function(p,a,c,k,e,d){...})', [/eval/])).rejects.toThrow('Could not find a stream link on https://some-url.test');
expect(() => {
extractUrlFromPacked('eval(function(p,a,c,k,e,d){...}))', [/sources:\[{file:"(.*?)"/]);
}).toThrow('Could not find a stream link');
});
test('finds link', async () => {
(unpack as jest.Mock).mockReturnValue('{sources:[{file:"https://streaming-url.mp4"}');
(cachedFetchText as jest.Mock).mockReturnValue(
Promise.resolve('<html><body>Something to stream;1280x720, 834.6 MB<script>(eval(function(p,a,c,k,e,d){...}))</script></body></html>'),
);
expect(extractUrlFromPacked('eval(function(p,a,c,k,e,d){...})', [/eval/])).toBe('https://streaming-url.mp4');
expect(extractUrlFromPacked('eval(function(p,a,c,k,e,d){...}))', [/sources:\[{file:"(.*?)"/])).toBe('https://streaming-url.mp4');
});
});