chore: better handle not found streams/links
This commit is contained in:
parent
1e9ebd5905
commit
8e791bbe24
10 changed files with 236 additions and 13 deletions
1
src/error/NotFoundError.ts
Normal file
1
src/error/NotFoundError.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export class NotFoundError extends Error {}
|
||||
1
src/error/index.ts
Normal file
1
src/error/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './NotFoundError';
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import bytes from 'bytes';
|
||||
import { Extractor } from './types';
|
||||
import { extractUrlFromPacked, Fetcher } from '../utils';
|
||||
import { Context } from '../types';
|
||||
import bytes from 'bytes';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
export class Dropload implements Extractor {
|
||||
readonly id = 'dropload';
|
||||
|
|
@ -22,6 +23,10 @@ export class Dropload implements Extractor {
|
|||
url.pathname = url.pathname.replace('/e/', '').replace('/embed-', '/');
|
||||
const html = await this.fetcher.text(ctx, url);
|
||||
|
||||
if (html.includes('File Not Found')) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const heightMatch = html.match(/\d{3,}x(\d{3,}),/) as string[];
|
||||
|
||||
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
|
||||
|
|
|
|||
|
|
@ -24,4 +24,17 @@ describe('ExtractorRegistry', () => {
|
|||
|
||||
expect(urlResult2).toBe(urlResult1);
|
||||
});
|
||||
|
||||
test('returns from memory cache if possible', async () => {
|
||||
const urlResult1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de');
|
||||
const urlResult2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de');
|
||||
|
||||
expect(urlResult2).toBe(urlResult1);
|
||||
});
|
||||
|
||||
test('ignores not found errors', async () => {
|
||||
const urlResult = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), 'de');
|
||||
|
||||
expect(urlResult).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { DoodStream } from './DoodStream';
|
|||
import { Dropload } from './Dropload';
|
||||
import { SuperVideo } from './SuperVideo';
|
||||
import { ExternalUrl } from './ExternalUrl';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
export class ExtractorRegistry {
|
||||
private readonly logger: winston.Logger;
|
||||
|
|
@ -36,6 +37,10 @@ export class ExtractorRegistry {
|
|||
try {
|
||||
urlResult = await extractor.extract(ctx, url, countryCode);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.logger.warn(`${extractor.id} error: ${error}`, ctx);
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import winston from 'winston';
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
const axiosMock = new AxiosMockAdapter(axios);
|
||||
|
||||
|
|
@ -97,4 +98,16 @@ describe('fetch', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('converts 404 to custom NotFoundError', async () => {
|
||||
axiosMock.onGet().reply(404);
|
||||
|
||||
await expect(fetcher.text(ctx, new URL('https://some-url.test/'))).rejects.toBeInstanceOf(NotFoundError);
|
||||
});
|
||||
|
||||
test('passes through other errors', async () => {
|
||||
axiosMock.onGet().networkError();
|
||||
|
||||
await expect(fetcher.text(ctx, new URL('https://some-url.test/'))).rejects.toBeInstanceOf(AxiosError);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { AxiosInstance, AxiosRequestConfig, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios';
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponseHeaders, RawAxiosResponseHeaders, isAxiosError } from 'axios';
|
||||
import TTLCache from '@isaacs/ttlcache';
|
||||
import UserAgent from 'user-agents';
|
||||
import winston from 'winston';
|
||||
import { Context } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
export class Fetcher {
|
||||
private readonly axios: AxiosInstance;
|
||||
|
|
@ -18,25 +19,19 @@ export class Fetcher {
|
|||
readonly text = async (ctx: Context, url: URL, config?: AxiosRequestConfig): Promise<string> => {
|
||||
this.logger.info(`Fetch ${url}`, ctx);
|
||||
|
||||
const response = await this.axios.get(url.href, this.getConfig(ctx, url, config));
|
||||
|
||||
return response.data;
|
||||
return (await this.errorWrapper(() => this.axios.get(url.href, this.getConfig(ctx, url, config)))).data;
|
||||
};
|
||||
|
||||
readonly textPost = async (ctx: Context, url: URL, data: unknown, config?: AxiosRequestConfig): Promise<string> => {
|
||||
this.logger.info(`Fetch post ${url}`, ctx);
|
||||
|
||||
const response = await this.axios.post(url.href, data, this.getConfig(ctx, url, config));
|
||||
|
||||
return response.data;
|
||||
return (await this.errorWrapper(() => this.axios.post(url.href, data, this.getConfig(ctx, url, config)))).data;
|
||||
};
|
||||
|
||||
readonly head = async (ctx: Context, url: URL, config?: AxiosRequestConfig): Promise<RawAxiosResponseHeaders | AxiosResponseHeaders> => {
|
||||
this.logger.info(`Fetch head ${url}`, ctx);
|
||||
|
||||
const response = await this.axios.head(url.href, this.getConfig(ctx, url, config));
|
||||
|
||||
return response.headers;
|
||||
return (await this.errorWrapper(() => this.axios.head(url.href, this.getConfig(ctx, url, config)))).headers;
|
||||
};
|
||||
|
||||
private readonly getConfig = (ctx: Context, url: URL, config?: AxiosRequestConfig): AxiosRequestConfig => {
|
||||
|
|
@ -64,4 +59,15 @@ export class Fetcher {
|
|||
|
||||
return userAgent;
|
||||
};
|
||||
|
||||
private readonly errorWrapper = async <T>(callable: () => T): Promise<T> => {
|
||||
try {
|
||||
return await callable();
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === 404) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import winston from 'winston';
|
||||
import { ExtractorRegistry } from '../extractor';
|
||||
import { StreamResolver } from './StreamResolver';
|
||||
import { MeineCloud, MostraGuarda } from '../handler';
|
||||
import { Handler, MeineCloud, MostraGuarda } from '../handler';
|
||||
import { Fetcher } from './Fetcher';
|
||||
import { Context } from '../types';
|
||||
import { NotFoundError } from '../error';
|
||||
jest.mock('../utils/Fetcher');
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
|
|
@ -55,4 +56,17 @@ describe('resolve', () => {
|
|||
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt29141112');
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('ignores not found errors', async () => {
|
||||
const mockHandler: Handler = {
|
||||
id: 'mockhandler',
|
||||
label: 'MockHandler',
|
||||
contentTypes: ['movie'],
|
||||
languages: ['de'],
|
||||
handle: jest.fn().mockRejectedValue(new NotFoundError()),
|
||||
};
|
||||
|
||||
const streams = await streamResolver.resolve(ctx, [mockHandler], 'movie', 'tt12345678');
|
||||
expect(streams).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import winston from 'winston';
|
|||
import bytes from 'bytes';
|
||||
import { Context, UrlResult } from '../types';
|
||||
import { Handler } from '../handler';
|
||||
import { NotFoundError } from '../error';
|
||||
|
||||
export class StreamResolver {
|
||||
private readonly logger: winston.Logger;
|
||||
|
|
@ -35,6 +36,10 @@ export class StreamResolver {
|
|||
|
||||
urlResults.push(...(handlerUrlResults.filter(handlerUrlResult => handlerUrlResult !== undefined)));
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) {
|
||||
return;
|
||||
}
|
||||
|
||||
streams.push({
|
||||
name: 'WebStreamr',
|
||||
title: `❌ Error with handler "${handler.id}". Please create an issue if this persists. Request-id: ${ctx.id}`,
|
||||
|
|
|
|||
160
src/utils/__fixtures__/Fetcher/https:dropload.ioasdfghijklmn.html
generated
Normal file
160
src/utils/__fixtures__/Fetcher/https:dropload.ioasdfghijklmn.html
generated
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Dropload - Revolution Video Hosting</title>
|
||||
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#FFF">
|
||||
<meta name="format-detection" conten="telephone=no">
|
||||
<link rel="apple-touch-icon" href="https://dropload.io/assets2/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="https://dropload.io/assets2/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" href="https://dropload.io/assets2/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="manifest" href="https://dropload.io/assets2/images/favicon/manifest.json">
|
||||
<link rel="icon" href="https://dropload.io/assets2/images/favicon/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link href="https://dropload.io/assets2/css/style.css?v=2" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js" type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
<script src="https://dropload.io/assets2/js/bootstrap.bundle.min.js" type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
<script src="https://dropload.io/assets2/js/xupload.js?v=13" type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
<script src="https://dropload.io/assets2/js/app.js" type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center flex-nowrap">
|
||||
<div class="col-auto d-lg-none me-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn icon-btn text-primary" data-bs-toggle="dropdown" data-bs-offset="0, 16">
|
||||
<i class="icon icon-menu icon-size-18"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
|
||||
<a href="/?op=registration" class="dropdown-item">Upload</a>
|
||||
|
||||
<a href="/make_money.html" class="dropdown-item">Earn money</a>
|
||||
<a href="/api.html" class="dropdown-item">Service API</a>
|
||||
<a href="/tos" class="dropdown-item">Terms and Conditions</a>
|
||||
<a href="/contact" class="dropdown-item">Contacts</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto me-lg-5">
|
||||
<a href="/">
|
||||
<img src="https://dropload.io/assets2/images/logo.svg" class="logo" alt="Dropload.io - ">
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto d-none d-lg-block">
|
||||
<nav class="nav">
|
||||
|
||||
<a href="/?op=registration" class="nav-link">Upload</a>
|
||||
|
||||
<a href="/make_money.html" class="nav-link">Earn money</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto d-none d-lg-block ms-auto">
|
||||
<a href="/login.html" class="btn login-btn me-2">
|
||||
<i class="icon icon-account me-2"></i>Login</a>
|
||||
<a href="/?op=registration" class="btn reg-btn">Create an Account</a>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-lg-none ms-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn icon-btn text-primary" data-bs-toggle="dropdown" data-bs-offset="0, 16">
|
||||
<i class="icon icon-account icon-size-18"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/login.html">Login</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/?op=registration">Create an Account</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
<script data-cfasync="false" async type="text/javascript" src="//sx.impubicpuddly.com/rO7FQr7AANJdtg/111561"></script>
|
||||
<script src='https://dropload.io/tag2.js' type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
<script src='https://dropload.io/tag1.js' type="895f6eb15d5fa2cf4d814bb0-text/javascript"></script>
|
||||
<section class="mb-5 mb-lg-7">
|
||||
<div class="container">
|
||||
<div class="message mb-6">
|
||||
<div class="row align-items-center justify-content-center">
|
||||
<div class="col-8 col-lg-6 pe-lg-5 text-lg-end mb-4 mb-lg-0">
|
||||
<img src="https://dropload.io/assets2/images/nofile.svg" class="img-fluid" alt="">
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<h4 class="text-uppercase">File Not Found</h4>
|
||||
<p>The file you were looking for could not be found, sorry for any inconvenience.<br><br>
|
||||
Possible causes of this error could be:</p>
|
||||
<ul>
|
||||
<li>The file expired
|
||||
<li>The file was deleted by its owner
|
||||
<li>The file was deleted by administration because it didn't comply with our Terms of Use
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row text-center text-lg-start flex-lg-nowrap">
|
||||
<div class="col">
|
||||
<div class="mb-3">
|
||||
<a href="/">
|
||||
<img src="https://dropload.io/assets2/images/logo.svg" alt="">
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-muted mb-4 small">© 2025 Dropload - Revolution Video Hosting. All rights reserved.</div>
|
||||
</div>
|
||||
<div class="col d-none d-lg-block">
|
||||
<div class="nav flex-column mb-3">
|
||||
<a href="/faq" class="nav-link">FAQ</a>
|
||||
<a href="/contact" class="nav-link">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col d-none d-lg-block">
|
||||
<div class="nav flex-column mb-3">
|
||||
<a href="/tos" class="nav-link">Terms of service</a>
|
||||
<a href="#" class="nav-link">Privacy Policy</a>
|
||||
<a href="#" class="nav-link">Copyright Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col d-none d-lg-block">
|
||||
<div class="nav flex-column mb-3">
|
||||
<a href="/api.html" class="nav-link">API</a>
|
||||
<a href="/make_money.html" class="nav-link">Make Money</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="895f6eb15d5fa2cf4d814bb0-|49" defer></script><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9443bec2eccebbcd',t:'MTc0Nzk5NDgzNC4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
Loading…
Reference in a new issue