chore: add more error infos to results
This commit is contained in:
parent
ed501a7ee9
commit
ef2c45e2cb
11 changed files with 211 additions and 77 deletions
|
|
@ -12,10 +12,10 @@ const extractorRegistry = new ExtractorRegistry(logger, fetcher);
|
|||
describe('ExtractorRegistry', () => {
|
||||
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { de: 'on' } };
|
||||
|
||||
test('returns undefined when no extractor can be found', async () => {
|
||||
test('returns error result from extractor', async () => {
|
||||
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'), { countryCode: 'en' });
|
||||
|
||||
expect(urlResult).toBeUndefined();
|
||||
expect(urlResult).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('return external URLs by default', async () => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { DoodStream } from './DoodStream';
|
|||
import { Dropload } from './Dropload';
|
||||
import { SuperVideo } from './SuperVideo';
|
||||
import { ExternalUrl } from './ExternalUrl';
|
||||
import { BlockedError, NotFoundError } from '../error';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
export class ExtractorRegistry {
|
||||
private readonly logger: winston.Logger;
|
||||
|
|
@ -47,24 +47,14 @@ export class ExtractorRegistry {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (error instanceof BlockedError && !('excludeExternalUrls' in ctx.config)) {
|
||||
this.logger.warn(`${extractor.id}: Request was blocked. Reason: ${error.reason}`, ctx);
|
||||
|
||||
return {
|
||||
url: url,
|
||||
isExternal: true,
|
||||
blocked: error.reason,
|
||||
label: url.host,
|
||||
sourceId: '',
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
|
||||
this.logger.warn(`${extractor.id} error: ${error}, cause: ${cause}`, ctx);
|
||||
return undefined;
|
||||
return {
|
||||
url,
|
||||
isExternal: true,
|
||||
error,
|
||||
label: url.host,
|
||||
sourceId: `${extractor.id}`,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
this.urlResultCache.set(url.href, urlResult, { ttl: extractor.ttl });
|
||||
|
|
|
|||
|
|
@ -11,3 +11,16 @@ exports[`ExtractorRegistry return external URLs by default 1`] = `
|
|||
"url": "https://mixdrop.ag/e/3nzwveprim63or6",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ExtractorRegistry returns error result from extractor 1`] = `
|
||||
{
|
||||
"error": [Error: TypeError: fetch failed],
|
||||
"isExternal": true,
|
||||
"label": "some-url.test",
|
||||
"meta": {
|
||||
"countryCode": "en",
|
||||
},
|
||||
"sourceId": "external",
|
||||
"url": "https://some-url.test/",
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,17 @@
|
|||
|
||||
exports[`Frembed handle imdb black mirror s4e2 1`] = `
|
||||
[
|
||||
{
|
||||
"error": [Error: TypeError: fetch failed],
|
||||
"isExternal": true,
|
||||
"label": "ahvsh.com",
|
||||
"meta": {
|
||||
"countryCode": "fr",
|
||||
"title": "Black Mirror 4x2",
|
||||
},
|
||||
"sourceId": "external",
|
||||
"url": "https://ahvsh.com/e/83izf7qzwpae,https://netu.frembed.fun/e/0DFgfkcXOsDP,https://likessb.com/e/7yjgl1x56n08.html,https://ds2play.com/e/fzfvfq3ngig0",
|
||||
},
|
||||
{
|
||||
"label": "DoodStream",
|
||||
"meta": {
|
||||
|
|
@ -34,5 +45,16 @@ exports[`Frembed handle imdb black mirror s4e2 1`] = `
|
|||
"sourceId": "external_fr",
|
||||
"url": "https://johnalwayssame.com/e/cqy9oue7sv0g",
|
||||
},
|
||||
{
|
||||
"error": [Error: TypeError: fetch failed],
|
||||
"isExternal": true,
|
||||
"label": "ds2play.com",
|
||||
"meta": {
|
||||
"countryCode": "fr",
|
||||
"title": "Black Mirror 4x2",
|
||||
},
|
||||
"sourceId": "external",
|
||||
"url": "https://ds2play.com/e/fzfvfq3ngig0",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ exports[`FrenchCloud handle imdb the devil's bath 1`] = `
|
|||
"sourceId": "external_fr",
|
||||
"url": "https://mixdrop.ag/e/l7v73zqrfdj19z",
|
||||
},
|
||||
{
|
||||
"error": [Error: Fetcher error: 404: Not Found],
|
||||
"isExternal": true,
|
||||
"label": "streamtape.com",
|
||||
"meta": {
|
||||
"countryCode": "fr",
|
||||
},
|
||||
"sourceId": "external",
|
||||
"url": "https://streamtape.com/e/gjA1OQ4klyHxgJ",
|
||||
},
|
||||
{
|
||||
"label": "DoodStream",
|
||||
"meta": {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ exports[`VerHdLink handle titanic 1`] = `
|
|||
"sourceId": "external_mx",
|
||||
"url": "https://mixdrop.ag/e/vn0wx308fq984q",
|
||||
},
|
||||
{
|
||||
"error": [Error: Fetcher error: 404: Not Found],
|
||||
"isExternal": true,
|
||||
"label": "streamtape.com",
|
||||
"meta": {
|
||||
"countryCode": "mx",
|
||||
},
|
||||
"sourceId": "external",
|
||||
"url": "https://streamtape.com/e/Bjp2vjrdBxsK82",
|
||||
},
|
||||
{
|
||||
"label": "SuperVideo",
|
||||
"meta": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Manifest, ManifestConfig } from 'stremio-addon-sdk';
|
||||
|
||||
export const TIMEOUT = 'timeout';
|
||||
|
||||
export interface Context {
|
||||
id: string;
|
||||
ip: string;
|
||||
|
|
@ -25,9 +27,9 @@ export interface Meta {
|
|||
export interface UrlResult {
|
||||
url: URL;
|
||||
isExternal?: boolean;
|
||||
blocked?: BlockedReason;
|
||||
error?: unknown;
|
||||
label: string;
|
||||
sourceId?: string;
|
||||
sourceId: string;
|
||||
meta: Meta;
|
||||
requestHeaders?: Record<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import CachePolicy from 'http-cache-semantics';
|
||||
import TTLCache from '@isaacs/ttlcache';
|
||||
import winston from 'winston';
|
||||
import { Context } from '../types';
|
||||
import { Context, TIMEOUT } from '../types';
|
||||
import { BlockedError, NotFoundError } from '../error';
|
||||
import { clearTimeout } from 'node:timers';
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ export class Fetcher {
|
|||
this.logger.info(`Fetch ${request.method} ${url}`, ctx);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 10000);
|
||||
const timer = setTimeout(() => controller.abort(TIMEOUT), 10000);
|
||||
|
||||
let response;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { ExtractorRegistry } from '../extractor';
|
|||
import { StreamResolver } from './StreamResolver';
|
||||
import { Handler, MeineCloud, MostraGuarda } from '../handler';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context, CountryCode, UrlResult } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, CountryCode, TIMEOUT, UrlResult } from '../types';
|
||||
import { BlockedError, NotFoundError } from '../error';
|
||||
jest.mock('../utils/Fetcher');
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
|
|
@ -35,7 +35,7 @@ describe('resolve', () => {
|
|||
|
||||
expect(streams).toStrictEqual([{
|
||||
name: 'WebStreamr',
|
||||
title: '❌ Error with handler "meinecloud". Please create an issue if this persists. Request-id: id',
|
||||
title: '🔗 MeineCloud\n❌ Request failed. Request-id: id.',
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
}]);
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ describe('resolve', () => {
|
|||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('adds Cloudflare blocked info', async () => {
|
||||
test('adds error info', async () => {
|
||||
class MockHandler implements Handler {
|
||||
readonly id = 'mockhandler';
|
||||
|
||||
|
|
@ -68,21 +68,46 @@ describe('resolve', () => {
|
|||
readonly countryCodes: CountryCode[] = ['de'];
|
||||
|
||||
readonly handle = async (): Promise<(UrlResult | undefined)[]> => {
|
||||
return [{
|
||||
url: new URL('https://example.com'),
|
||||
isExternal: true,
|
||||
blocked: 'cloudflare_challenge',
|
||||
label: 'example.com',
|
||||
sourceId: '',
|
||||
meta: {
|
||||
countryCode: 'de',
|
||||
return [
|
||||
{
|
||||
url: new URL('https://example.com'),
|
||||
isExternal: true,
|
||||
error: new BlockedError('cloudflare_challenge'),
|
||||
label: 'hoster.com',
|
||||
sourceId: '',
|
||||
meta: {
|
||||
countryCode: 'de',
|
||||
},
|
||||
},
|
||||
}];
|
||||
{
|
||||
url: new URL('https://example2.com'),
|
||||
isExternal: true,
|
||||
error: new TypeError(),
|
||||
label: 'hoster.com',
|
||||
sourceId: '',
|
||||
meta: {
|
||||
countryCode: 'de',
|
||||
},
|
||||
},
|
||||
{
|
||||
url: new URL('https://example2.com'),
|
||||
isExternal: true,
|
||||
error: TIMEOUT,
|
||||
label: 'hoster.com',
|
||||
sourceId: '',
|
||||
meta: {
|
||||
countryCode: 'de',
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
const streams = await streamResolver.resolve(ctx, [new MockHandler()], 'movie', 'tt11655566');
|
||||
expect(streams).toMatchSnapshot();
|
||||
|
||||
const streamsWithoutExternalUrls = await streamResolver.resolve({ ...ctx, config: { ...ctx.config, excludeExternalUrls: 'on' } }, [new MockHandler()], 'movie', 'tt11655566');
|
||||
expect(streamsWithoutExternalUrls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('ignores not found errors', async () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Stream } from 'stremio-addon-sdk';
|
|||
import { flag } from 'country-emoji';
|
||||
import winston from 'winston';
|
||||
import bytes from 'bytes';
|
||||
import { Context, UrlResult } from '../types';
|
||||
import { Context, TIMEOUT, UrlResult } from '../types';
|
||||
import { Handler } from '../handler';
|
||||
import { BlockedError, NotFoundError } from '../error';
|
||||
import { languageFromCountryCode } from './languageFromCountryCode';
|
||||
|
|
@ -41,28 +41,11 @@ export class StreamResolver {
|
|||
return;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (error instanceof BlockedError) {
|
||||
this.logger.warn(`${handler.id}: Request was blocked. Reason: ${error.reason}`, ctx);
|
||||
|
||||
streams.push({
|
||||
name: process.env['MANIFEST_NAME'] || 'WebStreamr',
|
||||
title: `⚠️ Request was blocked for handler "${handler.id}".`,
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
streams.push({
|
||||
name: process.env['MANIFEST_NAME'] || 'WebStreamr',
|
||||
title: `❌ Error with handler "${handler.id}". Please create an issue if this persists. Request-id: ${ctx.id}`,
|
||||
title: [`🔗 ${handler.label}`, this.logErrorAndReturnNiceString(ctx, handler.id, error)].join('\n'),
|
||||
ytId: 'E4WlUXrJgy4',
|
||||
});
|
||||
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
|
||||
this.logger.error(`${handler.id} error: ${error}, cause: ${cause}`, ctx);
|
||||
}
|
||||
});
|
||||
await Promise.all(handlerPromises);
|
||||
|
|
@ -84,37 +67,71 @@ export class StreamResolver {
|
|||
this.logger.info(`Return ${urlResults.length} streams`, ctx);
|
||||
|
||||
streams.push(
|
||||
...urlResults.map(urlResult => ({
|
||||
[urlResult.isExternal ? 'externalUrl' : 'url']: urlResult.url.href,
|
||||
name: this.buildName(urlResult),
|
||||
title: this.buildTitle(urlResult),
|
||||
behaviorHints: {
|
||||
...(urlResult.sourceId && { bingeGroup: `webstreamr-${urlResult.sourceId}` }),
|
||||
...(urlResult.requestHeaders !== undefined && {
|
||||
notWebReady: true,
|
||||
proxyHeaders: { request: urlResult.requestHeaders },
|
||||
}),
|
||||
...(urlResult.meta.bytes && { videoSize: urlResult.meta.bytes }),
|
||||
},
|
||||
})),
|
||||
...urlResults.filter(urlResult => !urlResult.isExternal || this.showExternalUrls(ctx) || urlResult.error)
|
||||
.map(urlResult => ({
|
||||
...this.buildUrl(ctx, urlResult),
|
||||
name: this.buildName(ctx, urlResult),
|
||||
title: this.buildTitle(ctx, urlResult),
|
||||
behaviorHints: {
|
||||
...(urlResult.sourceId && { bingeGroup: `webstreamr-${urlResult.sourceId}` }),
|
||||
...(urlResult.requestHeaders !== undefined && {
|
||||
notWebReady: true,
|
||||
proxyHeaders: { request: urlResult.requestHeaders },
|
||||
}),
|
||||
...(urlResult.meta.bytes && { videoSize: urlResult.meta.bytes }),
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
return streams;
|
||||
};
|
||||
|
||||
private readonly buildName = (urlResult: UrlResult): string => {
|
||||
private readonly showExternalUrls = (ctx: Context): boolean => !('excludeExternalUrls' in ctx.config);
|
||||
|
||||
private readonly buildUrl = (ctx: Context, urlResult: UrlResult): { externalUrl: string } | { url: string } | { ytId: string } => {
|
||||
if (!urlResult.isExternal) {
|
||||
return { url: urlResult.url.href };
|
||||
}
|
||||
|
||||
if (this.showExternalUrls(ctx)) {
|
||||
return { externalUrl: urlResult.url.href };
|
||||
}
|
||||
|
||||
return { ytId: 'E4WlUXrJgy4' };
|
||||
};
|
||||
|
||||
private readonly buildName = (ctx: Context, urlResult: UrlResult): string => {
|
||||
let name = process.env['MANIFEST_NAME'] || 'WebStreamr';
|
||||
|
||||
name += urlResult.meta.height ? ` ${urlResult.meta.height}P` : ' N/A';
|
||||
|
||||
if (urlResult.isExternal) {
|
||||
if (urlResult.isExternal && this.showExternalUrls(ctx)) {
|
||||
name += ` ⚠️ external`;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
private readonly buildTitle = (urlResult: UrlResult): string => {
|
||||
private readonly logErrorAndReturnNiceString = (ctx: Context, source: string, error: unknown): string => {
|
||||
if (error instanceof BlockedError) {
|
||||
this.logger.warn(`${source}: Request was blocked. Reason: ${error.reason}`, ctx);
|
||||
|
||||
return '⚠️ Request was blocked.';
|
||||
}
|
||||
|
||||
if (error === TIMEOUT) {
|
||||
this.logger.warn(`${source}: Request timed out.`, ctx);
|
||||
|
||||
return '🐢 Request timed out.';
|
||||
}
|
||||
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
this.logger.error(`${source} error: ${error}, cause: ${cause}`, ctx);
|
||||
|
||||
return `❌ Request failed. Request-id: ${ctx.id}.`;
|
||||
};
|
||||
|
||||
private readonly buildTitle = (ctx: Context, urlResult: UrlResult): string => {
|
||||
const titleLines = [];
|
||||
|
||||
if (urlResult.meta.title) {
|
||||
|
|
@ -131,8 +148,8 @@ export class StreamResolver {
|
|||
|
||||
titleLines.push(`🔗 ${urlResult.label}`);
|
||||
|
||||
if (urlResult.blocked) {
|
||||
titleLines.push('⚠️ Request was blocked.');
|
||||
if (urlResult.error) {
|
||||
titleLines.push(this.logErrorAndReturnNiceString(ctx, urlResult.sourceId, urlResult.error));
|
||||
}
|
||||
|
||||
return titleLines.join('\n');
|
||||
|
|
|
|||
|
|
@ -1,15 +1,60 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`resolve adds Cloudflare blocked info 1`] = `
|
||||
exports[`resolve adds error info 1`] = `
|
||||
[
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"externalUrl": "https://example.com/",
|
||||
"name": "WebStreamr N/A ⚠️ external",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 example.com
|
||||
🔗 hoster.com
|
||||
⚠️ Request was blocked.",
|
||||
},
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"externalUrl": "https://example2.com/",
|
||||
"name": "WebStreamr N/A ⚠️ external",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 hoster.com
|
||||
❌ Request failed. Request-id: id.",
|
||||
},
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"externalUrl": "https://example2.com/",
|
||||
"name": "WebStreamr N/A ⚠️ external",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 hoster.com
|
||||
🐢 Request timed out.",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`resolve adds error info 2`] = `
|
||||
[
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"name": "WebStreamr N/A",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 hoster.com
|
||||
⚠️ Request was blocked.",
|
||||
"ytId": "E4WlUXrJgy4",
|
||||
},
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"name": "WebStreamr N/A",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 hoster.com
|
||||
❌ Request failed. Request-id: id.",
|
||||
"ytId": "E4WlUXrJgy4",
|
||||
},
|
||||
{
|
||||
"behaviorHints": {},
|
||||
"name": "WebStreamr N/A",
|
||||
"title": "🌐 German 🇩🇪
|
||||
🔗 hoster.com
|
||||
🐢 Request timed out.",
|
||||
"ytId": "E4WlUXrJgy4",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue