chore: better handle not found streams/links

This commit is contained in:
WebStreamr 2025-05-23 10:40:59 +00:00
parent 1e9ebd5905
commit 8e791bbe24
No known key found for this signature in database
10 changed files with 236 additions and 13 deletions

View file

@ -0,0 +1 @@
export class NotFoundError extends Error {}

1
src/error/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './NotFoundError';

View file

@ -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[];

View file

@ -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();
});
});

View file

@ -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;
}

View file

@ -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);
});
});

View file

@ -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;
}
};
}

View file

@ -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([]);
});
});

View file

@ -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}`,

View 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&#39;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>