Compare commits

..

No commits in common. "main" and "v0.12.0" have entirely different histories.

702 changed files with 7033 additions and 189745 deletions

View file

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,23 +0,0 @@
---
name: Request source/extractor
about: Request a source or extractor to get URLs from
title: Request to add <source/extractor>
labels: enhancement
assignees: ''
---
**What is the site you want to get added?**
A link to the source or embed hoster
**What is the benefit of adding this?**
More content is not a good reason. If it's already covered via an existing source/extractor, why should we add another one?
**Are you aware of any protection the site uses?**
E.g. rate-limits or Cloudflare captchas and such
**What languages does this relate to?**
E.g. multi, German, ..
**Example**
A movie/series IMDb/TMDB ID + source link or embed URL to test with.

View file

@ -1,28 +0,0 @@
name: Build and push latest
on:
push:
tags:
- 'v*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU (for cross-arch)
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
-
name: Build and push
uses: docker/build-push-action@v7
with:
push: true
platforms: linux/amd64,linux/arm64
tags: webstreamr/webstreamr:latest

View file

@ -1,18 +1,15 @@
name: Build name: Build
on: on: push
push:
pull_request:
workflow_dispatch:
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
push: false push: false
tags: webstreamr/webstreamr:latest tags: webstreamr/webstreamr:latest

View file

@ -1,17 +1,26 @@
name: Tests name: Tests
on: on: push
push:
pull_request:
workflow_dispatch:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "22"
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install

View file

@ -1,3 +1,3 @@
{ {
".": "0.69.1" ".": "0.12.0"
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
FROM node:24 AS builder FROM node:22.15-alpine AS builder
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
@ -10,7 +10,7 @@ RUN npm run build
RUN npm ci --only=production RUN npm ci --only=production
FROM node:24 FROM node:22.15-alpine
WORKDIR /app WORKDIR /app
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist

117
README.md
View file

@ -1,117 +0,0 @@
# WebStreamr
[![Tests](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml/badge.svg)](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
[![GitHub release](https://img.shields.io/github/v/release/webstreamr/webstreamr)](https://github.com/webstreamr/webstreamr/releases)
![GitHub License](https://img.shields.io/github/license/webstreamr/webstreamr)
[Stremio](https://www.stremio.com/) add-on which provides HTTP URLs from streaming websites.
HTTP streams have limitations.
For a better experience, I'd advise using a Debrid service and WebStreamr as fallback.
[TorBox](https://torbox.app/subscription?referral=f22eb00d-27ce-4e20-85fc-68da3d018b99) is working very well.
## Public instance
A public instance is available at https://webstreamr.hayd.uk. Hosting infrastructure for this instance is donated by [ElfHosted](https://elfhosted.com), and independently maintained by [Hayduk](https://hayd.uk).
## Known issues / limitations
- PixelServer / pixeldrain has a daily limit of 6 GB per IP: https://pixeldrain.dev
- Dropload and SuperVideo on Android do not work because Stremio does not use the `Referer` header properly via HLS playlists: https://github.com/Stremio/stremio-bugs/issues/2389, maybe https://github.com/Stremio/stremio-bugs/issues/1579
- MediaFlow proxy has to be used in an inefficient way because Stremio on Android or its players cannot deal with HLS playlist with redirects: https://github.com/Stremio/stremio-bugs/issues/1574
- FlareSolverr cookies cannot be used because Cloudflare does techniques like TLS fingerprinting most likely. But FlareSolverr uses a session per host and should be quick.
- VidSrc works but rate limits heavily and is therefore only queried as fallback if nothing else is found.
- RgShows detects shared usage and blocks IPs. It therefore only works on private instances.
## MediaFlow Proxy
[MediaFlow Proxy](https://github.com/mhdzumair/mediaflow-proxy/) can be added when configuration the add-on to gain access to a couple of more file hosters.
It depends highly on the language / source used if that unlocks more streams or not.
MediaFlow proxy is needed because some hosters ip-lock streams and the add-on does not run on the same device that will stream the video.
The following hosters can be used only with MediaFlow Proxy:
- Fastream
- FileLions
- FileMoon
- LuluStream
- Mixdrop
- Streamtape
- VOE
MediaFlow proxy can either be self-hosted or acquired via bundle from [ElfHosted](https://docs.elfhosted.com/app/mediaflow-proxy/).
## Hosting
Don't want to use the public instance, or concerned about reliability during periods of high use? It's open-source, you can host it yourself!
### ElfHosted (easy mode)
Self-hosting to stressful? ElfHosted offer [ready-to-go, turn-key WebStreamr instances](https://store.elfhosted.com/product/webstreamr/) with $1, 7-day trials. Additionally, 33% of your subscription directly supports your developer! ❤️
(*ElfHosted also offer advanced private hosting of the [top Stremio Addons](https://store.elfhosted.com/product-category/stremio-addons/elf/webstreamr/), as well as [turn-key bundles providing streaming from RealDebrid with Plex, Emby, or Jellyfin](https://store.elfhosted.com/product-category/streaming-bundles/elf/webstreamr/)*)
### Self-Hosting
You can run the latest WebStreamr via Docker. E.g.
```shell
docker run \
--detach=true \
--name webstreamr \
--rm \
--pull always \
--publish 51546:51546 \
--env TMDB_ACCESS_TOKEN="YOUR_TOKEN" \
--volume /tmp:/tmp \
webstreamr/webstreamr
```
### Environment variables
#### `CACHE_DIR`
Optional. Directory for persistent caches using SQLite files. Default: OS tmp dir.
#### `CONFIGURATION_DESCRIPTION`
Optional. To customize the description shown on the configuration page.
#### `DISABLED_EXTRACTORS`
Optional. Comma separated list of extractors which should be disabled. E.g. `doodstream,vidsrc`
#### `DISABLED_SOURCES`
Optional. Comma separated list of sources which should be disabled. E.g. `frembed,vidsrc`
#### `FLARESOLVERR_ENDPOINT`
Optional. If domains show Cloudflare challenges, FlareSolverr can be used to work around them. E.g. `http://flaresolverr:8191`
Proxy configuration is passed-through and only a single session is used to save resources. Byparr is not supported.
#### `MANIFEST_ID`
Optional. Add-on manifest ID. Default: `webstreamr`
#### `MANIFEST_NAME`
Optional. Add-on manifest name. Default: `WebStreamr`
#### `PORT`
Optional. Port of the node web server. Default: `51546`
#### `PROXY_CONFIG`
Optional. Proxies which should be used based on domain. Supports minimatch. E.g. `dood.to:http://USERNAME:PASSWORD@IP:PORT,*:socks5://172.17.0.1:1080` would use an http proxy for dood.to and a socks5 proxy for all other domains.
Some hosters are a bit picky when it comes to IPs. Best case is if you use a residential IP.
If you can't do that, then I suggest to use a VPN / proxy like Cloudflare WARP.
DoodStream is not working with WARP.
Free Webshare proxies seem to work with it.
#### `TMDB_ACCESS_TOKEN`
**Required**. TMDB access token to get information like title and year for content. Use the [API Read Access Token](https://www.themoviedb.org/settings/api).

View file

@ -1,12 +1,9 @@
import eslint from '@eslint/js'; import eslint from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin'; import stylistic from '@stylistic/eslint-plugin';
import importPlugin from 'eslint-plugin-import';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
export default tseslint.config( export default tseslint.config(
eslint.configs.recommended, eslint.configs.recommended,
importPlugin.flatConfigs.recommended,
importPlugin.flatConfigs.typescript,
tseslint.configs.strict, tseslint.configs.strict,
tseslint.configs.stylistic, tseslint.configs.stylistic,
stylistic.configs.customize({ stylistic.configs.customize({
@ -16,11 +13,4 @@ export default tseslint.config(
{ {
ignores: ['./dist'], ignores: ['./dist'],
}, },
{
rules: {
'@typescript-eslint/explicit-member-accessibility': 'error',
'import/no-unresolved': ['error', { ignore: ['typescript-eslint', 'stremio-addon-sdk'] }],
'import/order': ['error', { 'alphabetize': { order: 'asc', caseInsensitive: true }, 'named': { enabled: true }, 'newlines-between': 'never' }],
},
},
); );

View file

@ -1,8 +1,6 @@
import type { Config } from 'jest'; import type { Config } from 'jest';
const config: Config = { const config: Config = {
automock: false,
clearMocks: true,
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [
'<rootDir>/src/**/*.ts', '<rootDir>/src/**/*.ts',
@ -13,7 +11,6 @@ const config: Config = {
coverageDirectory: '<rootDir>/coverage', coverageDirectory: '<rootDir>/coverage',
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'/src/controller/', '/src/controller/',
'/src/utils/dispatcher.ts',
], ],
coverageProvider: 'babel', coverageProvider: 'babel',
coverageThreshold: { coverageThreshold: {
@ -25,15 +22,10 @@ const config: Config = {
}, },
}, },
resetModules: true, resetModules: true,
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], restoreMocks: true,
testEnvironment: 'node', testEnvironment: 'node',
testEnvironmentOptions: {
globalsCleanup: 'on',
},
transform: { transform: {
'^.+.tsx?$': ['ts-jest', { '^.+.tsx?$': ['ts-jest', {}],
tsconfig: 'tsconfig.dev.json',
}],
}, },
modulePathIgnorePatterns: [ modulePathIgnorePatterns: [
'<rootDir>/dist', '<rootDir>/dist',

View file

@ -1,17 +0,0 @@
process.env['CACHE_DIR'] = '/dev/null';
process.env['HOST'] = 'example.test';
process.env['PROTOCOL'] = 'https';
if (!process.env['TMDB_ACCESS_TOKEN']) {
process.env['TMDB_ACCESS_TOKEN'] = 'some access token';
}
jest.mock('randomstring', () => ({
generate: jest.fn(() => 'mocked-random-string'),
}));
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 639837296000);
});
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };

7775
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "webstreamr", "name": "webstreamr",
"description": "Provides HTTP URLs from streaming websites.", "description": "Provides HTTP URLs from streaming websites.",
"version": "0.69.1", "version": "0.12.0",
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
"analyse": "tsc --noEmit --project ./tsconfig.dev.json", "analyse": "tsc --noEmit --project ./tsconfig.dev.json",
@ -11,7 +11,6 @@
"lint:fix": "eslint --fix", "lint:fix": "eslint --fix",
"start": "node dist/index.js", "start": "node dist/index.js",
"test": "jest", "test": "jest",
"test:update-fixtures": "TEST_UPDATE_FIXTURES=1 npm run test",
"ci": "npm run lint && npm run analyse && npm run test" "ci": "npm run lint && npm run analyse && npm run test"
}, },
"repository": { "repository": {
@ -28,61 +27,35 @@
"url": "https://github.com/webstreamr/webstreamr/issues" "url": "https://github.com/webstreamr/webstreamr/issues"
}, },
"engines": { "engines": {
"node": "^22.0.0 || ^24.0.0" "node": "^22.0.0"
},
"devEngines": {
"runtime": {
"name": "node",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"onFail": "error"
}
}, },
"dependencies": { "dependencies": {
"@keyv/sqlite": "^4.0.5", "@isaacs/ttlcache": "^1.4.1",
"async-mutex": "^0.5.0", "axios-cache-interceptor": "^1.8.0",
"axios": "^1.13.2",
"axios-cache-interceptor": "^1.8.3",
"axios-mock-adapter": "^2.1.0",
"axios-retry": "^4.5.0", "axios-retry": "^4.5.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
"cacheable": "^2.0.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"country-emoji": "^1.5.6",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.2.1",
"fast-levenshtein": "^3.0.0",
"glob": "^13.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"memoizee": "^0.4.17",
"minimatch": "^10.0.3",
"randomstring": "^1.3.1",
"rot13-cipher": "^1.0.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"socks-proxy-agent": "^8.0.5",
"sqlite3": "^5.1.7",
"tough-cookie": "^6.0.0",
"unpacker": "^1.0.1", "unpacker": "^1.0.1",
"user-agents": "^1.1.536",
"winston": "^3.17.0" "winston": "^3.17.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.26.0", "@eslint/js": "^9.26.0",
"@stylistic/eslint-plugin": "^5.0.0", "@stylistic/eslint-plugin": "^4.2.0",
"@tsconfig/node24": "^24.0.0",
"@types/bytes": "^3.1.5", "@types/bytes": "^3.1.5",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/fast-levenshtein": "^0.0.4", "@types/jest": "^29.5.14",
"@types/jest": "^30.0.0", "@types/make-fetch-happen": "^10.0.4",
"@types/memoizee": "^0.4.12", "@types/node": "^22.15.3",
"@types/node": "^24.0.0",
"@types/randomstring": "^1.3.0",
"@types/stremio-addon-sdk": "^1.6.11", "@types/stremio-addon-sdk": "^1.6.11",
"babel-jest": "^30.0.0", "@types/user-agents": "^1.0.4",
"axios-mock-adapter": "^2.1.0",
"babel-jest": "^29.7.0",
"eslint": "^9.26.0", "eslint": "^9.26.0",
"eslint-plugin-import": "^2.32.0", "jest": "^29.7.0",
"jest": "^30.0.0",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"ts-jest": "^29.3.2", "ts-jest": "^29.3.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

View file

@ -1,35 +1,27 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { Extractor } from '../extractor';
import { landingTemplate } from '../landingTemplate'; import { landingTemplate } from '../landingTemplate';
import { Source } from '../source'; import { buildManifest } from '../utils';
import { Handler } from '../handler';
import { Config } from '../types'; import { Config } from '../types';
import { buildManifest, getDefaultConfig, isElfHostedInstance } from '../utils';
export class ConfigureController { export class ConfigureController {
public readonly router: Router; public readonly router: Router;
private readonly sources: Source[]; private readonly handlers: Handler[];
private readonly extractors: Extractor[];
public constructor(sources: Source[], extractors: Extractor[]) { constructor(handlers: Handler[]) {
this.router = Router(); this.router = Router();
this.sources = sources; this.handlers = handlers;
this.extractors = extractors;
this.router.get('/configure', this.getConfigure.bind(this)); this.router.get('/configure', this.getConfigure.bind(this));
this.router.get('/:config/configure', this.getConfigure.bind(this)); this.router.get('/:config/configure', this.getConfigure.bind(this));
} }
private getConfigure(req: Request, res: Response) { private readonly getConfigure = (req: Request, res: Response) => {
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig())); const config: Config = JSON.parse(req.params['config'] || '{}');
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy const manifest = buildManifest(this.handlers, config);
if (!req.params['config'] && isElfHostedInstance(req)) {
config.mediaFlowProxyUrl = `${req.protocol}://${req.host.replace('webstreamr', 'mediaflow-proxy')}`;
}
const manifest = buildManifest(this.sources, this.extractors, config);
res.setHeader('content-type', 'text/html'); res.setHeader('content-type', 'text/html');
res.send(landingTemplate(manifest)); res.send(landingTemplate(manifest));

View file

@ -1,53 +0,0 @@
import { Mutex } from 'async-mutex';
import { Request, Response, Router } from 'express';
import winston from 'winston';
import { ExtractorRegistry } from '../extractor';
import { contextFromRequestAndResponse, Fetcher } from '../utils';
export class ExtractController {
public readonly router: Router;
private readonly logger: winston.Logger;
private readonly extractorRegistry: ExtractorRegistry;
private readonly locks = new Map<string, Mutex>();
public constructor(logger: winston.Logger, _fetcher: Fetcher, extractorRegistry: ExtractorRegistry) {
this.router = Router();
this.logger = logger;
this.extractorRegistry = extractorRegistry;
this.router.get('/extract', this.extract.bind(this));
}
private async extract(req: Request, res: Response) {
if (req.method !== 'GET') {
res.status(405).send('Method Not Allowed');
return;
}
const ctx = contextFromRequestAndResponse(req, res);
const index = parseInt(req.query['index'] as string);
const url = new URL(req.query['url'] as string);
this.logger.info(`Lazy extract index ${index} of URL ${url} for ip ${ctx.ip}`, ctx);
let mutex = this.locks.get(url.href);
if (!mutex) {
mutex = new Mutex();
this.locks.set(url.href, mutex);
}
await mutex.runExclusive(async () => {
const urlResults = await this.extractorRegistry.handle(ctx, url);
res.redirect(urlResults[index]?.url.href as string);
});
if (!mutex.isLocked()) {
this.locks.delete(url.href);
}
};
}

View file

@ -1,29 +1,26 @@
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { Extractor } from '../extractor'; import { buildManifest } from '../utils';
import { Source } from '../source'; import { Handler } from '../handler';
import { Config } from '../types'; import { Config } from '../types';
import { buildManifest, getDefaultConfig } from '../utils';
export class ManifestController { export class ManifestController {
public readonly router: Router; public readonly router: Router;
private readonly sources: Source[]; private readonly handlers: Handler[];
private readonly extractors: Extractor[];
public constructor(sources: Source[], extractors: Extractor[]) { constructor(handlers: Handler[]) {
this.router = Router(); this.router = Router();
this.sources = sources; this.handlers = handlers;
this.extractors = extractors;
this.router.get('/manifest.json', this.getManifest.bind(this)); this.router.get('/manifest.json', this.getManifest.bind(this));
this.router.get('/:config/manifest.json', this.getManifest.bind(this)); this.router.get('/:config/manifest.json', this.getManifest.bind(this));
} }
private getManifest(req: Request, res: Response) { private readonly getManifest = (req: Request, res: Response) => {
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig())); const config: Config = JSON.parse(req.params['config'] || '{}');
const manifest = buildManifest(this.sources, this.extractors, config); const manifest = buildManifest(this.handlers, config);
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
res.send(manifest); res.send(manifest);

View file

@ -1,70 +1,38 @@
import { Mutex } from 'async-mutex';
import { Request, Response, Router } from 'express'; import { Request, Response, Router } from 'express';
import { ContentType } from 'stremio-addon-sdk';
import winston from 'winston'; import winston from 'winston';
import { Source } from '../source'; import { Handler } from '../handler';
import { contextFromRequestAndResponse, envIsProd, Id, ImdbId, StreamResolver, TmdbId } from '../utils'; import { Config } from '../types';
import { StreamResolver } from '../utils';
export class StreamController { export class StreamController {
public readonly router: Router; public readonly router: Router;
private readonly logger: winston.Logger; private readonly logger: winston.Logger;
private readonly sources: Source[]; private readonly handlers: Handler[];
private readonly streamResolver: StreamResolver; private readonly streamResolver: StreamResolver;
private readonly locks = new Map<string, Mutex>(); constructor(logger: winston.Logger, handlers: Handler[], streams: StreamResolver) {
public constructor(logger: winston.Logger, sources: Source[], streams: StreamResolver) {
this.router = Router(); this.router = Router();
this.logger = logger; this.logger = logger;
this.sources = sources; this.handlers = handlers;
this.streamResolver = streams; this.streamResolver = streams;
this.router.get('/stream/:type/:id.json', this.getStream.bind(this));
this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this)); this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this));
} }
private async getStream(req: Request, res: Response) { private readonly getStream = async (req: Request, res: Response) => {
const type: ContentType = (req.params['type'] || '') as ContentType; const config: Config = JSON.parse(req.params['config'] || '{}');
const rawId: string = req.params['id'] as string || ''; const type: string = req.params['type'] || '';
const id: string = req.params['id'] || '';
let id: Id; this.logger.info(`Search stream for type "${type}" and id "${id}"`);
if (rawId.startsWith('tmdb:')) {
id = TmdbId.fromString(rawId.replace('tmdb:', ''));
} else if (rawId.startsWith('tt')) {
id = ImdbId.fromString(rawId);
} else {
res.status(400).send(`Unsupported ID: ${rawId}`);
return; const selectedHandlers = this.handlers.filter(handler => handler.id in config);
}
const ctx = contextFromRequestAndResponse(req, res); const streams = await this.streamResolver.resolve({ ip: (req.ip as string) }, selectedHandlers, type, id);
this.logger.info(`Search stream for type "${type}" and id "${rawId}" for ip ${ctx.ip}`, ctx); res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ streams }));
const sources = this.sources.filter(source => source.countryCodes.filter(countryCode => countryCode in ctx.config).length);
let mutex = this.locks.get(rawId);
if (!mutex) {
mutex = new Mutex();
this.locks.set(rawId, mutex);
}
await mutex.runExclusive(async () => {
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, id);
if (ttl && envIsProd()) {
res.setHeader('Cache-Control', `public, max-age=${Math.floor(ttl / 1000)}, immutable`);
}
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({ streams }));
});
if (!mutex.isLocked()) {
this.locks.delete(rawId);
}
}; };
} }

View file

@ -1,4 +1,3 @@
export * from './ExtractController';
export * from './ConfigureController'; export * from './ConfigureController';
export * from './ManifestController'; export * from './ManifestController';
export * from './StreamController'; export * from './StreamController';

View file

@ -0,0 +1,38 @@
import { EmbedExtractor } from './types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context } from '../types';
import bytes from 'bytes';
export class Dropload implements EmbedExtractor {
readonly id = 'dropload';
readonly label = 'Dropload';
readonly ttl = 900000; // 15m
private readonly fetcher: Fetcher;
constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
readonly supports = (url: URL): boolean => null !== url.host.match(/dropload/);
readonly extract = async (ctx: Context, url: URL, countryCode: string) => {
const normalizedUrl = url.toString().replace('/e/', '').replace('/embed-', '/');
const html = await this.fetcher.text(ctx, new URL(normalizedUrl));
const heightMatch = html.match(/\d{3,}x(\d{3,}),/) as string[];
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
return {
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
label: this.label,
sourceId: `${this.id}_${countryCode.toLowerCase()}`,
height: parseInt(heightMatch[1] as string) as number,
bytes: bytes.parse(sizeMatch[1] as string) as number,
countryCode,
};
};
}

View file

@ -0,0 +1,27 @@
import winston from 'winston';
import { EmbedExtractorRegistry } from './EmbedExtractorRegistry';
import { Context } from '../types';
import { Fetcher } from '../utils';
jest.mock('../utils/Fetcher');
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
// @ts-expect-error No constructor args needed
const fetcher = new Fetcher();
const embedExtractors = new EmbedExtractorRegistry(logger, fetcher);
describe('EmbedExtractorRegistry', () => {
const ctx: Context = { ip: '127.0.0.1' };
test('returns undefined when no embed extractor can be found', async () => {
const urlResult = await embedExtractors.handle(ctx, new URL('https://some-url.test'), 'en');
expect(urlResult).toBeUndefined();
});
test('returns from memory cache if possible', async () => {
const urlResult1 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de');
const urlResult2 = await embedExtractors.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), 'de');
expect(urlResult2).toBe(urlResult1);
});
});

View file

@ -0,0 +1,41 @@
import TTLCache from '@isaacs/ttlcache';
import winston from 'winston';
import { EmbedExtractor } from './types';
import { Context, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Dropload } from './Dropload';
import { SuperVideo } from './SuperVideo';
export class EmbedExtractorRegistry {
private readonly logger: winston.Logger;
private readonly embedExtractors: EmbedExtractor[];
private readonly urlResultCache: TTLCache<string, UrlResult>;
constructor(logger: winston.Logger, fetcher: Fetcher) {
this.logger = logger;
this.embedExtractors = [
new Dropload(fetcher),
new SuperVideo(fetcher),
];
this.urlResultCache = new TTLCache({ max: 1024 });
}
readonly handle = async (ctx: Context, url: URL, countryCode: string): Promise<UrlResult | undefined> => {
let urlResult = this.urlResultCache.get(url.href);
if (urlResult) {
return urlResult;
}
const embedExtractor = this.embedExtractors.find(embedExtractor => embedExtractor.supports(url));
if (undefined === embedExtractor) {
return undefined;
}
this.logger.info(`Extract stream URL using ${embedExtractor.id} extractor from ${url}`);
urlResult = await embedExtractor.extract(ctx, url, countryCode);
this.urlResultCache.set(url.href, urlResult, { ttl: embedExtractor.ttl });
return urlResult;
};
}

View file

@ -0,0 +1,36 @@
import bytes from 'bytes';
import { EmbedExtractor } from './types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Context } from '../types';
export class SuperVideo implements EmbedExtractor {
readonly id = 'supervideo';
readonly label = 'SuperVideo';
readonly ttl = 900000; // 15m
private readonly fetcher: Fetcher;
constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
readonly supports = (url: URL): boolean => null !== url.host.match(/supervideo/);
readonly extract = async (ctx: Context, url: URL, countryCode: string) => {
const normalizedUrl = url.toString().replace('/e/', '/').replace('/embed-', '/');
const html = await this.fetcher.text(ctx, new URL(normalizedUrl));
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
return {
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
label: this.label,
sourceId: `${this.id}_${countryCode.toLowerCase()}`,
height: parseInt(heightAndSizeMatch[1] as string) as number,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
countryCode,
};
};
}

View file

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

View file

@ -0,0 +1,13 @@
import { Context, UrlResult } from '../types';
export interface EmbedExtractor {
readonly id: string;
readonly label: string;
readonly ttl: number;
readonly supports: (url: URL) => boolean;
readonly extract: (ctx: Context, url: URL, countryCode: string) => Promise<UrlResult>;
}

View file

@ -1,16 +0,0 @@
import { AxiosResponse } from 'axios';
import { BlockedReason } from '../types';
export class BlockedError extends Error {
public readonly url: URL;
public readonly reason: BlockedReason;
public readonly headers: AxiosResponse['headers'];
public constructor(url: URL, reason: BlockedReason, headers: AxiosResponse['headers']) {
super();
this.url = url;
this.reason = reason;
this.headers = headers;
}
}

View file

@ -1,17 +0,0 @@
import { AxiosResponse } from 'axios';
export class HttpError extends Error {
public readonly url: URL;
public readonly status: number;
public readonly statusText: string;
public readonly headers: AxiosResponse['headers'];
public constructor(url: URL, status: number, statusText: string, headers: AxiosResponse['headers']) {
super();
this.url = url;
this.status = status;
this.statusText = statusText;
this.headers = headers;
}
}

View file

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

View file

@ -1,9 +0,0 @@
export class QueueIsFullError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -1,9 +0,0 @@
export class TimeoutError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -1,11 +0,0 @@
export class TooManyRequestsError extends Error {
public readonly url: URL;
public readonly retryAfter: number;
public constructor(url: URL, retryAfter: number) {
super();
this.url = url;
this.retryAfter = retryAfter;
}
}

View file

@ -1,9 +0,0 @@
export class TooManyTimeoutsError extends Error {
public readonly url: URL;
public constructor(url: URL) {
super();
this.url = url;
}
}

View file

@ -1,74 +0,0 @@
import winston from 'winston';
import { BlockedReason, Context } from '../types';
import { BlockedError } from './BlockedError';
import { HttpError } from './HttpError';
import { QueueIsFullError } from './QueueIsFullError';
import { TimeoutError } from './TimeoutError';
import { TooManyRequestsError } from './TooManyRequestsError';
import { TooManyTimeoutsError } from './TooManyTimeoutsError';
export * from './BlockedError';
export * from './HttpError';
export * from './NotFoundError';
export * from './QueueIsFullError';
export * from './TimeoutError';
export * from './TooManyRequestsError';
export * from './TooManyTimeoutsError';
export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger, source: string, error: unknown): string => {
if (error instanceof BlockedError) {
if (error.reason === BlockedReason.media_flow_proxy_auth) {
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
}
logger.warn(`${source}: Request to ${error.url} was blocked, reason: ${error.reason}, headers: ${JSON.stringify(error.headers)}.`, ctx);
return `⚠️ Request to ${error.url.host} was blocked. Reason: ${error.reason}`;
}
if (error instanceof TooManyRequestsError) {
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
}
if (error instanceof TooManyTimeoutsError) {
logger.warn(`${source}: Too many timeouts when requesting ${error.url}.`, ctx);
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
}
if (error instanceof TimeoutError) {
logger.warn(`${source}: Request to ${error.url} timed out.`, ctx);
return `🐢 Request to ${error.url.host} timed out.`;
}
if (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) {
// sometimes this gets through, no idea why..
logger.warn(`${source}: Request timed out.`, ctx);
return '🐢 Request timed out.';
}
if (error instanceof QueueIsFullError) {
logger.warn(`${source}: Request queue for ${error.url.host} is full.`, ctx);
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
}
if (error instanceof HttpError) {
logger.error(`${source}: Error when requesting url ${error.url}, HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
if (error.status >= 500) {
return `❌ Remote server ${error.url.host} has issues. We can't fix this, please try later again.`;
}
return `❌ Request to ${error.url.host} failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
}
const cause = (error as Error & { cause?: unknown }).cause;
logger.error(`${source} error: ${error}, cause: ${cause}, stack: ${(error as Error).stack}`, ctx);
return `❌ Request failed. Request-id: ${ctx.id}.`;
};

View file

@ -1,24 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { DoodStream } from './DoodStream';
import { ExtractorRegistry } from './ExtractorRegistry';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new DoodStream(new FetcherMock(`${__dirname}/__fixtures__/DoodStream`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('DoodStream', () => {
test('dood.to', async () => {
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
});
test('can guess height from title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
});
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://myvidplay.com/e/cz7cus0bvlzr'))).toMatchSnapshot();
});
});

View file

@ -1,55 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class DoodStream extends Extractor {
public readonly id = 'doodstream';
public readonly label = 'DoodStream';
public override readonly ttl: number = 21600000; // 6h
public override viaMediaFlowProxy = true;
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/doodstream.py */
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|dsvplay|d0o0d|do0od|d0000d|d000d|myvidplay|vidply|all3do|doply|vide0|vvide0|d-s/) && supportsMediaFlowProxy(ctx);
};
public override normalize(url: URL): URL {
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
return new URL(`http://dood.to/e/${videoId}`);
};
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/Video not found/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
const downloadHtml = await this.fetcher.text(ctx, new URL(url.href.replace('/e/', '/d/')));
const sizeMatch = downloadHtml.match(/([\d.]+ ?[GM]B)/);
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Doodstream', url, headers),
format: Format.mp4,
meta: {
...meta,
title,
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
},
},
];
};
}

View file

@ -1,32 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { Dropload } from './Dropload';
import { ExtractorRegistry } from './ExtractorRegistry';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Dropload(new FetcherMock(`${__dirname}/__fixtures__/Dropload`))]);
const ctx = createTestContext();
describe('Dropload', () => {
test('dropload.io', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
});
test('dropload unknown height and size', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/gf2apij3ql37'))).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
});
test('processing / internal problem', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'))).toMatchSnapshot();
});
test('download URL', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'))).toMatchSnapshot();
});
});

View file

@ -1,58 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class Dropload extends Extractor {
public readonly id = 'dropload';
public readonly label = 'Dropload';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dropload|dr0pstream/);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/d/', '/').replace('/e/', '/').replace('/embed-', '/'));
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('File Not Found') || html.includes('Pending in queue')) {
throw new NotFoundError();
}
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
const playlistHeaders = { Referer: 'https://dr0pstream.com/' };
const heightMatch = html.match(/\d{3,}x(\d{3,}),/);
const height = heightMatch
? parseInt(heightMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
const size = sizeMatch ? bytes.parse(sizeMatch[1] as string) as number : undefined;
const $ = cheerio.load(html);
const title = $('.videoplayer h1').text().trim();
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
title,
...(size && { bytes: size }),
...(height && { height }),
},
requestHeaders: playlistHeaders,
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExternalUrl } from './ExternalUrl';
import { ExtractorRegistry } from './ExtractorRegistry';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new ExternalUrl(new FetcherMock(`${__dirname}/__fixtures__/ExternalUrl`))]);
const ctx = createTestContext({ includeExternalUrls: 'on' });
describe('ExternalUrl', () => {
test('404 - not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'))).toMatchSnapshot();
});
test('netu.fanstream.us', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'))).toMatchSnapshot();
});
});

View file

@ -1,27 +0,0 @@
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { showExternalUrls } from '../utils';
import { Extractor } from './Extractor';
export class ExternalUrl extends Extractor {
public readonly id = 'external';
public readonly label = 'External';
public override readonly ttl = 21600000; // 6h
public supports(ctx: Context, url: URL): boolean {
return showExternalUrls(ctx.config) && null !== url.host.match(/.*/);
}
protected async extractInternal(_ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
return [
{
url: url,
format: Format.unknown,
isExternal: true,
label: `${url.host}`,
meta,
},
];
};
}

View file

@ -1,62 +0,0 @@
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
import { Fetcher } from '../utils';
export abstract class Extractor {
public abstract readonly id: string;
public abstract readonly label: string;
public readonly ttl: number = 900000; // 15m
public readonly cacheVersion: number | undefined = undefined;
public readonly viaMediaFlowProxy: boolean = false;
protected readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
public abstract supports(ctx: Context, url: URL): boolean;
public normalize(url: URL): URL {
return url;
};
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]>;
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
try {
return (await this.extractInternal(ctx, url, meta)).map(
urlResult => ({
...urlResult,
label: this.formatLabel(urlResult.label ?? this.label),
ttl: this.ttl,
}),
);
} catch (error) {
if (error instanceof NotFoundError) {
return [];
}
return [
{
url,
format: Format.unknown,
isExternal: true,
error,
label: this.formatLabel(this.label),
ttl: 0,
meta,
},
];
}
};
private formatLabel(label: string): string {
return this.viaMediaFlowProxy ? `${label} (MFP)` : label;
}
}

View file

@ -1,63 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { createExtractors } from './index';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, createExtractors(new FetcherMock(`${__dirname}/__fixtures__/ExtractorRegistry`)));
describe('ExtractorRegistry', () => {
const ctx = createTestContext();
test('returns error result from extractor', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'));
expect(urlResult).toMatchSnapshot();
});
test('returns external URLs if enabled by config', async () => {
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'));
expect(urlResult).toMatchSnapshot();
});
test('does not return external URLs by default', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'));
expect(urlResult).toStrictEqual([]);
});
test('returns from memory cache if possible', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'));
expect(urlResults1).not.toStrictEqual([]);
expect(urlResults2).not.toStrictEqual([]);
});
test('ignores not found errors but caches them', async () => {
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'));
expect(urlResults1).toStrictEqual([]);
expect(urlResults2).toStrictEqual([]);
});
test('returns external url for error', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'));
expect(urlResults).toMatchSnapshot();
});
test('empty results are cached', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { title: 'title' });
expect(urlResults).toMatchSnapshot();
});
test('stats returns something', async () => {
const stats = extractorRegistry.stats();
expect(stats).toHaveProperty('urlResultCache');
expect(stats.urlResultCache).toBeTruthy();
});
});

View file

@ -1,112 +0,0 @@
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
import winston from 'winston';
import { Context, Format, Meta, UrlResult } from '../types';
import { createKeyvSqlite, envGet, isExtractorDisabled } from '../utils';
import { Extractor } from './Extractor';
export class ExtractorRegistry {
private readonly logger: winston.Logger;
private readonly extractors: Extractor[];
private readonly urlResultCache: Cacheable;
private readonly lazyUrlResultCache: Cacheable;
public constructor(logger: winston.Logger, extractors: Extractor[]) {
this.logger = logger;
this.extractors = extractors;
this.urlResultCache = new Cacheable({
nonBlocking: true,
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
secondary: createKeyvSqlite('extractor-cache'),
stats: true,
});
this.lazyUrlResultCache = new Cacheable({
nonBlocking: true,
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
secondary: createKeyvSqlite('extractor-lazy-cache'),
stats: true,
});
}
public stats() {
return {
urlResultCache: this.urlResultCache.stats,
lazyUrlResultCache: this.lazyUrlResultCache.stats,
};
};
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
if (!extractor) {
return [];
}
const normalizedUrl = extractor.normalize(url);
const cacheKey = this.determineCacheKey(ctx, extractor, normalizedUrl);
const storedDataRaw = await this.urlResultCache.getRaw<UrlResult[]>(cacheKey);
const expires = storedDataRaw?.expires;
if (storedDataRaw && expires) {
const ttl = expires - Date.now();
/* istanbul ignore if */
if (ttl > 0) {
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl, url: new URL(urlResult.url) }));
}
}
const lazyUrlResults = await this.lazyUrlResultCache.get<UrlResult[]>(normalizedUrl.href) ?? [];
/* istanbul ignore next */
if (
lazyUrlResults.length && allowLazy && !extractor.viaMediaFlowProxy
&& lazyUrlResults.every(urlResult => urlResult.format !== Format.hls) // related to Android issues, e.g. https://github.com/Stremio/stremio-bugs/issues/1574 or https://github.com/Stremio/stremio-bugs/issues/1579
) {
// generate lazy extract urls
return lazyUrlResults.map((urlResult, index) => {
const extractUrl = new URL(`${envGet('PROTOCOL')}:${envGet('HOST')}/extract/`);
extractUrl.searchParams.set('index', `${index}`);
extractUrl.searchParams.set('url', url.href);
return { ...urlResult, url: extractUrl };
});
}
this.logger.info(`Extract ${url} using ${extractor.id} extractor`, ctx);
const mergedMeta: Meta = { ...meta, ...lazyUrlResults[0]?.meta };
const urlResults = await extractor.extract(ctx, normalizedUrl, { extractorId: extractor.id, ...mergedMeta });
if (!Object.keys(mergedMeta).length || urlResults.some(urlResult => urlResult.error)) {
await this.urlResultCache.delete(cacheKey);
await this.lazyUrlResultCache.delete(normalizedUrl.href);
return urlResults;
}
const ttl = urlResults.length ? extractor.ttl : 43200000; // 12h
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, ttl);
if (extractor.id !== 'external') {
await this.lazyUrlResultCache.set<UrlResult[]>(normalizedUrl.href, urlResults, 2629800000); // 1 month
}
return urlResults;
};
private determineCacheKey(ctx: Context, extractor: Extractor, url: URL): string {
let suffix = '';
if (extractor.viaMediaFlowProxy) {
suffix += `_${ctx.config.mediaFlowProxyUrl}`;
}
if (extractor.cacheVersion) {
suffix += `_${extractor.cacheVersion}`;
}
return `${extractor.id}_${url}${suffix}`;
}
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fastream } from './Fastream';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Fastream(new FetcherMock(`${__dirname}/__fixtures__/Fastream`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
describe('Fastream', () => {
test('fastream.to embed', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
});
test('expired or deleted', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-a9rp1d1pqom7.html'))).toMatchSnapshot();
});
});

View file

@ -1,50 +0,0 @@
import bytes from 'bytes';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Fastream extends Extractor {
public readonly id = 'fastream';
public readonly label = 'Fastream';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/fastream/) && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
const html = await this.fetcher.text(ctx, downloadUrl, { headers });
if (/No such file/.test(html)) {
throw new NotFoundError();
}
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url, headers);
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
const titleMatch = html.match(/>Download (.*?)</) as string[];
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
title: titleMatch[1],
},
},
];
};
}

View file

@ -1,28 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { FileLions } from './FileLions';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new FileLions(new FetcherMock(`${__dirname}/__fixtures__/FileLions`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('FileLions', () => {
test('filelions f', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filelions.to/f/tyn45apubte2'))).toMatchSnapshot();
});
test('mivalyo v referer lock', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mivalyo.com/v/tah5znapz3e5'), { referer: 'https://kinoger.com' })).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filelions.to/v/ylcp2cu5qanb'))).toMatchSnapshot();
});
test('deleted by administration', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://callistanise.com/f/cy4t5nkerjrt'))).toMatchSnapshot();
});
});

View file

@ -1,99 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy, unpackEval } from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/commits/master/script.module.resolveurl/lib/resolveurl/plugins/filelions.py */
export class FileLions extends Extractor {
public readonly id = 'filelions';
public readonly label = 'FileLions';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/.*lions?/)
|| [
'6sfkrspw4u.sbs',
'ajmidyadfihayh.sbs',
'alhayabambi.sbs',
'anime7u.com',
'azipcdn.com',
'bingezove.com',
'callistanise.com',
'coolciima.online',
'dhtpre.com',
'dingtezuni.com',
'dintezuvio.com',
'e4xb5c2xnz.sbs',
'egsyxutd.sbs',
'fdewsdc.sbs',
'gsfomqu.sbs',
'javplaya.com',
'katomen.online',
'lumiawatch.top',
'minochinos.com',
'mivalyo.com',
'moflix-stream.click',
'motvy55.store',
'movearnpre.com',
'peytonepre.com',
'ryderjet.com',
'smoothpre.com',
'taylorplayer.com',
'techradar.ink',
'videoland.sbs',
'vidhide.com',
'vidhide.fun',
'vidhidefast.com',
'vidhidehub.com',
'vidhideplus.com',
'vidhidepre.com',
'vidhidepro.com',
'vidhidevip.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/v/', '/f/').replace('/download/', '/f/').replace('/file/', '/f/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(url.href.replace('/f/', '/v/')), meta);
}
if (/File Not Found|deleted by administration/.test(html)) {
throw new NotFoundError();
}
const unpacked = unpackEval(html);
const heightMatch = unpacked.match(/(\d{3,})p/) as string[];
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
const $ = cheerio.load(html);
const title = $('meta[name="description"]').attr('content') as string;
return [
{
url: await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', url, headers),
format: Format.hls,
meta: {
...meta,
height: parseInt(heightMatch[1] as string),
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
...(title && { title }),
},
},
];
};
}

View file

@ -1,24 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { FileMoon } from './FileMoon';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new FileMoon(new FetcherMock(`${__dirname}/__fixtures__/FileMoon`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('FileMoon', () => {
test('z1ekv717 d', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://z1ekv717.fun/d/wkhcbggdxf1d'))).toMatchSnapshot();
});
test('page not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.sx/e/n7i8zodwjqr9'))).toMatchSnapshot();
});
test('missing height', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filemoon.to/d/ugj7f1kq94p1'))).toMatchSnapshot();
});
});

View file

@ -1,82 +0,0 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
unpackEval,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/filemoon.py */
export class FileMoon extends Extractor {
public readonly id = 'filemoon';
public readonly label = 'FileMoon';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/filemoon/)
|| [
'1azayf9w.xyz',
'222i8x.lol',
'81u6xl9d.xyz',
'8mhlloqo.fun',
'96ar.com',
'bf0skv.org',
'boosteradx.online',
'c1z39.com',
'cinegrab.com',
'f51rm.com',
'furher.in',
'kerapoxy.cc',
'l1afav.net',
'moonmov.pro',
'smdfs40r.skin',
'xcoic.com',
'z1ekv717.fun',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/d/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta, originalUrl?: URL): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/Page not found/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('h3').text().trim();
const iframeUrlMatches = Array.from(html.matchAll(/iframe.*?src=["'](.*?)["']/g));
if (iframeUrlMatches.length) {
// Use last match because there can be fake adblock catcher urls before
return await this.extractInternal(ctx, new URL((iframeUrlMatches[iframeUrlMatches.length - 1] as RegExpExecArray)[1] as string), { title, ...meta }, url);
}
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileMoon', originalUrl as URL, headers);
const unpacked = unpackEval(html);
const heightMatch = unpacked.match(/(\d{3,})p/);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
...(heightMatch && { height: parseInt(heightMatch[1] as string) }),
},
},
];
};
}

View file

@ -1,16 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fsst } from './Fsst';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Fsst(new FetcherMock(`${__dirname}/__fixtures__/Fsst`))]);
const ctx = createTestContext();
describe('Fsst', () => {
test('Wake up Dead Man', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/948429/'))).toMatchSnapshot();
});
});

View file

@ -1,41 +0,0 @@
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class Fsst extends Extractor {
public readonly id = 'fsst';
public readonly label = 'Fsst';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/fsst/);
};
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers, noProxyHeaders: true });
const $ = cheerio.load(html);
const title = $('title').text().trim();
const filesMatch = html.match(/file:"(.*)"/) as string[];
const lastFile = (filesMatch[1] as string).split(',').pop() as string;
const heightAndUrlMatch = lastFile.match(/\[?([\d]*)p?]?(.*)/) as string[];
const fileHref = heightAndUrlMatch[2] as string;
return [{
url: await this.fetcher.getFinalRedirectUrl(ctx, new URL(fileHref), { headers, noProxyHeaders: true }, 1),
format: Format.mp4,
meta: {
...meta,
height: parseInt(heightAndUrlMatch[1] as string),
title,
},
}];
};
}

View file

@ -1,28 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { HubCloud } from './HubCloud';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new HubCloud(new FetcherMock(`${__dirname}/__fixtures__/HubCloud`))]);
const ctx = createTestContext();
describe('HubCloud', () => {
test('handle dexter original sin 2024 s01e01', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/idt1evqfuviqiei'))).toMatchSnapshot();
});
test('handle crayon shin-chan 1993', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/bffzqlpqfllfcld'))).toMatchSnapshot();
});
test('handle dark 2017 s03e08', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.one/drive/nknlofk8snfnknh'))).toMatchSnapshot();
});
test('handle goat 2026', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubcloud.foo/drive/p94k4dccjwxjcx4'))).toMatchSnapshot();
});
});

View file

@ -1,102 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { findCountryCodes, findHeight } from '../utils';
import { Extractor } from './Extractor';
export class HubCloud extends Extractor {
public readonly id = 'hubcloud';
public readonly label = 'HubCloud';
public override readonly ttl: number = 43200000; // 12h
public override readonly cacheVersion = 1;
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/hubcloud/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const redirectHtml = await this.fetcher.text(ctx, url, { headers });
const redirectUrlMatch = redirectHtml.match(/var url ?= ?'(.*?)'/) as string[];
const linksHtml = await this.fetcher.text(ctx, new URL(redirectUrlMatch[1] as string), { headers: { Referer: url.href } });
const $ = cheerio.load(linksHtml);
const title = $('title').text().trim();
const countryCodes = [...new Set([...meta.countryCodes ?? [], ...findCountryCodes(title)])];
const height = meta.height ?? findHeight(title);
return Promise.all([
...$('a')
.filter((_i, el) => {
const text = $(el).text();
return text.includes('FSL') && !text.includes('FSLv2');
})
.map((_i, el) => {
const url = new URL($(el).attr('href') as string);
return {
url,
format: Format.unknown,
label: `${this.label} (FSL)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_fsl`,
countryCodes,
height,
title,
},
};
}).toArray(),
...$('a')
.filter((_i, el) => {
const text = $(el).text();
return text.includes('FSLv2');
})
.map((_i, el) => {
const url = new URL($(el).attr('href') as string);
return {
url,
format: Format.unknown,
label: `${this.label} (FSLv2)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_fslv2`,
countryCodes,
height,
title,
},
};
}).toArray(),
...$('a')
.filter((_i, el) => $(el).text().includes('PixelServer'))
.map((_i, el) => {
const userUrl = new URL(($(el).attr('href') as string).replace('/api/file/', '/u/'));
const url = new URL(userUrl.href.replace('/u/', '/api/file/'));
url.searchParams.set('download', '');
return {
url,
format: Format.unknown,
label: `${this.label} (PixelServer)`,
meta: {
...meta,
bytes: bytes.parse($('#size').text()) as number,
extractorId: `${this.id}_pixelserver`,
countryCodes,
height,
title,
},
requestHeaders: { Referer: userUrl.href },
};
}).toArray(),
]);
};
}

View file

@ -1,29 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { HubCloud } from './HubCloud';
import { HubDrive } from './HubDrive';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(
logger,
[
new HubDrive(
new FetcherMock(`${__dirname}/__fixtures__/HubDrive`),
new HubCloud(new FetcherMock(`${__dirname}/__fixtures__/HubDrive/HubCloud`)),
),
],
);
const ctx = createTestContext();
describe('HubDrive', () => {
test('handle avatar', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/7283903021'))).toMatchSnapshot();
});
test('handle missing hubcloud', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://hubdrive.space/file/2243124026'))).toMatchSnapshot();
});
});

View file

@ -1,40 +0,0 @@
import * as cheerio from 'cheerio';
import { Context, InternalUrlResult, Meta } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
import { HubCloud } from './HubCloud';
export class HubDrive extends Extractor {
public readonly id = 'hubdrive';
public readonly label = 'HubDrive';
public override readonly ttl: number = 43200000; // 12h
public override readonly cacheVersion = 1;
private readonly hubCloud: HubCloud;
public constructor(fetcher: Fetcher, hubCloud: HubCloud) {
super(fetcher);
this.hubCloud = hubCloud;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/hubdrive/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
const $ = cheerio.load(html);
const hubCloudUrl = $('a:contains("HubCloud")')
.map((_i, el) => new URL($(el).attr('href') as string))
.get(0);
return hubCloudUrl ? this.hubCloud.extract(ctx, hubCloudUrl, meta) : [];
};
}

View file

@ -1,28 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { KinoGer } from './KinoGer';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new KinoGer(new FetcherMock(`${__dirname}/__fixtures__/KinoGer`))]);
const ctx = createTestContext();
describe('KinoGer', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'))).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'))).toMatchSnapshot();
});
test('filma365.strp2p.site', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://filma365.strp2p.site/#vih6bx'))).toMatchSnapshot();
});
test('kinoger.p2pplay.pro', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.p2pplay.pro/#k53k9'))).toMatchSnapshot();
});
});

View file

@ -1,78 +0,0 @@
import crypto from 'crypto';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */
export class KinoGer extends Extractor {
public readonly id = 'kinoger';
public readonly label = 'KinoGer';
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return [
'asianembed.cam',
'disneycdn.net',
'dzo.vidplayer.live',
'filedecrypt.link',
'filma365.strp2p.site',
'flimmer.rpmvip.com',
'flixfilmesonline.strp2p.site',
'kinoger.p2pplay.pro',
'kinoger.re',
'moflix.rpmplay.xyz',
'moflix.upns.xyz',
'player.upn.one',
'securecdn.shop',
'shiid4u.upn.one',
'srbe84.vidplayer.live',
'strp2p.site',
't1.p2pplay.pro',
'tuktuk.rpmvid.com',
'ultrastream.online',
'videoland.cfd',
'videoshar.uns.bio',
'w1tv.xyz',
'wasuytm.store',
].includes(url.host);
}
public override normalize(url: URL): URL {
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = {
'Origin': url.origin,
'Referer': url.origin + '/',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
};
const hexData = await this.fetcher.text(ctx, url, { headers });
const encrypted = Buffer.from(hexData.slice(0, -1), 'hex');
const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex');
const iv = Buffer.from('313233343536373839306f6975797472', 'hex');
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
const { source, title } = JSON.parse(decrypted) as { source: string; title: string };
const m3u8Url = new URL(source);
return [
{
url: m3u8Url,
format: Format.hls,
meta: {
...meta,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
title,
},
requestHeaders: headers,
},
];
};
}

View file

@ -1,24 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { LuluStream } from './LuluStream';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new LuluStream(new FetcherMock(`${__dirname}/__fixtures__/LuluStream`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('LuluStream', () => {
test('streamhihi d', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/d/mk9m58lz8ts6'))).toMatchSnapshot();
});
test('streamhihi', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhihi.com/mk9m58lz8ts6'))).toMatchSnapshot();
});
test('no such file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://lulustream.com/e/uthq0o0sljnx'))).toMatchSnapshot();
});
});

View file

@ -1,67 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/lulustream.py */
export class LuluStream extends Extractor {
public readonly id = 'lulustream';
public readonly label = 'LuluStream';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/lulu/)
|| [
'732eg54de642sa.sbs',
'cdn1.site',
'd00ds.site',
'streamhihi.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
return new URL(`/e/${videoId}`, url);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const fileUrl = new URL(url.href.replace('/e/', '/d/'));
const html = await this.fetcher.text(ctx, fileUrl, { headers });
if (/No such file|File Not Found/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('h1').text().trim();
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'LuluStream', url, headers);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
title,
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
},
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Mixdrop } from './Mixdrop';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Mixdrop(new FetcherMock(`${__dirname}/__fixtures__/Mixdrop`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
describe('Mixdrop', () => {
test('mixdrop.my /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'))).toMatchSnapshot();
});
});

View file

@ -1,46 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Mixdrop extends Extractor {
public readonly id = 'mixdrop';
public readonly label = 'Mixdrop';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
}
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
const html = await this.fetcher.text(ctx, fileUrl);
if (/can't find the (file|video)/.test(html)) {
throw new NotFoundError();
}
const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/) as string[];
const $ = cheerio.load(html);
const title = $('.title b').text().trim();
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
format: Format.mp4,
meta: {
...meta,
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
title,
},
},
];
};
}

View file

@ -1,24 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { RgShows } from './RgShows';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`))]);
const ctx = createTestContext();
describe('RgShows', () => {
test('handle one battle after another', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/1054867'))).toMatchSnapshot();
});
test('handle stranger things s05e01', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/tv/66732/5/1'))).toMatchSnapshot();
});
test('handle parasite 2019 (mp4)', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/496243'))).toMatchSnapshot();
});
});

View file

@ -1,49 +0,0 @@
import { BlockedError } from '../error';
import { BlockedReason, Context, Format, InternalUrlResult, Meta } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
interface RgShowsApiData {
stream: {
url: string;
};
}
export class RgShows extends Extractor {
public readonly id = 'rgshows';
public readonly label = 'RgShows';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/rgshows/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { 'Referer': 'https://www.rgshows.ru/', 'Origin': 'https://www.rgshows.ru', 'User-Agent': 'Mozilla' };
const data = await this.fetcher.json(ctx, url, { headers }) as RgShowsApiData;
const streamUrl = new URL(data.stream.url);
/* istanbul ignore if */
if (streamUrl.host.includes('vidzee')) {
throw new BlockedError(url, BlockedReason.unknown, {});
}
const isMp4 = streamUrl.href.includes('mp4');
const isHls = streamUrl.href.includes('m3u8') || streamUrl.href.includes('txt');
return [
{
url: streamUrl,
format: isMp4 ? Format.mp4 : (isHls ? Format.hls : /* istanbul ignore next */ Format.unknown),
meta: {
...meta,
...(isHls && { height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, streamUrl, { headers }) }),
},
requestHeaders: headers,
},
];
};
}

View file

@ -1,36 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SaveFiles } from './SaveFiles';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new SaveFiles(new FetcherMock(`${__dirname}/__fixtures__/SaveFiles`))]);
const ctx = createTestContext();
describe('SafeFiles', () => {
test('savefiles', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'))).toMatchSnapshot();
});
test('savefiles /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'))).toMatchSnapshot();
});
test('savefiles /d/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'))).toMatchSnapshot();
});
test('savefiles locked file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'))).toMatchSnapshot();
});
test('savefiles deleted file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/bejqk3vlsv02'))).toMatchSnapshot();
});
test('streamhls /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'))).toMatchSnapshot();
});
});

View file

@ -1,48 +0,0 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class SaveFiles extends Extractor {
public readonly id = 'savefiles';
public readonly label = 'SaveFiles';
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/savefiles|streamhls/);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/file was locked|file was deleted/i.test(html)) {
throw new NotFoundError();
}
const fileMatch = html.match(/file:"(.*?)"/) as string[];
const sizeMatch = html.match(/\[\d{3,}x(\d{3,})/) as string[];
const $ = cheerio.load(html);
const title = $('.download-title').text().trim();
return [
{
url: new URL(fileMatch[1] as string),
format: Format.hls,
meta: {
...meta,
title,
height: parseInt(sizeMatch[1] as string),
},
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { StreamEmbed } from './StreamEmbed';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new StreamEmbed(new FetcherMock(`${__dirname}/__fixtures__/StreamEmbed`))]);
const ctx = createTestContext();
describe('StreamEmbed', () => {
test('watch.gxplayer.xyz', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'))).toMatchSnapshot();
});
test('video is not ready', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=PBO90WAS'))).toMatchSnapshot();
});
});

View file

@ -1,39 +0,0 @@
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class StreamEmbed extends Extractor {
public readonly id = 'streamembed';
public readonly label = 'StreamEmbed';
public override readonly ttl: number = 259200000; // 3d
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (/Video is not ready/.test(html)) {
throw new NotFoundError();
}
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
return [
{
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
format: Format.hls,
meta: {
...meta,
height: parseInt(JSON.parse(video.quality)[0]),
title: decodeURIComponent(video.title),
},
},
];
};
}

View file

@ -1,16 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Streamtape } from './Streamtape';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Streamtape(new FetcherMock(`${__dirname}/__fixtures__/Streamtape`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Streamtape', () => {
test('streamtape.com /e', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/DjmZBG0K9gHkJ7z/WALL-E.2008.1080p.BrRip.x264.YIFY.mp4'))).toMatchSnapshot();
});
});

View file

@ -1,70 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Streamtape extends Extractor {
public readonly id = 'streamtape';
public readonly label = 'Streamtape';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/streamtape/)
|| [
'strtape.cloud',
'streamta.pe',
'strcloud.link',
'strcloud.club',
'strtpe.link',
'scloud.online',
'stape.fun',
'streamadblockplus.com',
'shavetape.cash',
'streamta.site',
'streamadblocker.xyz',
'tapewithadblock.org',
'adblocktape.wiki',
'antiadtape.com',
'tapeblocker.com',
'streamnoads.com',
'tapeadvertisement.com',
'tapeadsenjoyer.com',
'watchadsontape.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/v/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
// Only needed to properly find non-existing files via 404 response
await this.fetcher.text(ctx, new URL(url.href.replace('/v/', '/e/')), { headers });
const html = await this.fetcher.text(ctx, url, { headers });
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
const $ = cheerio.load(html);
const title = $('meta[name="og:title"]').attr('content') as string;
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers),
format: Format.mp4,
meta: {
...meta,
title,
bytes: bytes.parse(sizeMatch[1] as string) as number,
},
},
];
};
}

View file

@ -1,32 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SuperVideo } from './SuperVideo';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new SuperVideo(new FetcherMock(`${__dirname}/__fixtures__/SuperVideo`))]);
const ctx = createTestContext();
describe('SuperVideo', () => {
test('supervideo.cc /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'))).toMatchSnapshot();
});
test('supervideo.tv /embed-/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'))).toMatchSnapshot();
});
test('embed only', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'))).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'))).toMatchSnapshot();
});
test('processing video', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'))).toMatchSnapshot();
});
});

View file

@ -1,62 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class SuperVideo extends Extractor {
public readonly id = 'supervideo';
public readonly label = 'SuperVideo';
public override readonly ttl: number = 10800000; // 3h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/supervideo/);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/e/', '/').replace('/k/', '/').replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
}
if (/'The file was deleted|The file expired|Video is processing/.test(html)) {
throw new NotFoundError();
}
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
const playlistHeaders = { Referer: 'https://supervideo.cc/' };
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/);
const size = heightAndSizeMatch ? bytes.parse(heightAndSizeMatch[2] as string) as number : undefined;
const height = heightAndSizeMatch
? parseInt(heightAndSizeMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: playlistHeaders });
const $ = cheerio.load(html);
const title = $('.download__title').text().trim();
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
title,
...(size && { bytes: size }),
...(height && { height }),
},
requestHeaders: playlistHeaders,
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Uqload } from './Uqload';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Uqload(new FetcherMock(`${__dirname}/__fixtures__/Uqload`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Uqload', () => {
test('uqload.net /embed-', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.cx/2ua47fdfkjgd.html'))).toMatchSnapshot();
});
});

View file

@ -1,48 +0,0 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Uqload extends Extractor {
public readonly id = 'uqload';
public readonly label = 'Uqload';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/uqload/) && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (/File Not Found/.test(html)) {
throw new NotFoundError();
}
const heightMatch = html.match(/\d{3,}x(\d{3,})/);
const $ = cheerio.load(html);
const title = $('h1').text().trim();
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url),
format: Format.mp4,
meta: {
...meta,
title,
...(heightMatch && {
height: parseInt(heightMatch[1] as string),
}),
},
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VidSrc } from './VidSrc';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]);
const ctx = createTestContext();
describe('VidSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/movie/tt0093058'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/tv/tt2085059/4-2'))).toMatchSnapshot();
});
});

View file

@ -1,86 +0,0 @@
import * as cheerio from 'cheerio';
import { BlockedError, TooManyRequestsError } from '../error';
import { Context, Format, InternalUrlResult, Meta, NonEmptyArray } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class VidSrc extends Extractor {
public readonly id = 'vidsrc';
public readonly label = 'VidSrc';
public override readonly ttl: number = 10800000; // 3h
private readonly domains: NonEmptyArray<string>;
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
super(fetcher);
this.domains = domains;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidsrc|vsrc/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
// While this is a crappy thing to do, they seem to be blocking overly strict IMO
const randomIp = `${Math.floor(Math.random() * 223) + 1}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
const newCtx = { ...ctx, ip: randomIp };
return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]);
};
private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise<InternalUrlResult[]> {
const domainIndex = Math.floor(Math.random() * domains.length);
const [domain] = domains.splice(domainIndex, 1) as [string];
const newUrl = new URL(url);
newUrl.hostname = domain;
let html: string;
try {
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
} catch (error) {
/* istanbul ignore next */
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
return this.extractUsingRandomDomain(ctx, url, meta, domains);
}
/* istanbul ignore next */
throw error;
}
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
const title = $('title').text().trim();
return Promise.all(
$('.server')
.map((_i, el) => ({ serverName: $(el).text(), dataHash: $(el).data('hash') }))
.toArray()
.filter(({ serverName }) => serverName === 'CloudStream Pro')
.map(async ({ serverName, dataHash }) => {
const rcpUrl = new URL(`/rcp/${dataHash}`, iframeUrl.origin);
const iframeHtml = await this.fetcher.text(ctx, rcpUrl, { headers: { Referer: newUrl.origin } });
const srcMatch = iframeHtml.match(`src:\\s?'(.*)'`) as string[];
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: rcpUrl.href } });
const fileMatch = playerHtml.match(`(https:\\/\\/.*?{v\\d}.*?) or`) as string[];
const m3u8Url = new URL((fileMatch[1] as string).replace(/{v\d}/, iframeUrl.host));
return {
url: m3u8Url,
format: Format.hls,
label: serverName,
meta: {
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }),
title,
},
};
}),
);
}
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Vidora } from './Vidora';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Vidora(new FetcherMock(`${__dirname}/__fixtures__/Vidora`))]);
const ctx = createTestContext();
describe('Vidora', () => {
test('vidora.stream', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/5iz6c1elaqgq'))).toMatchSnapshot();
});
test('vidora.stream /embed/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidora.stream/embed/5iz6c1elaqgq'))).toMatchSnapshot();
});
});

View file

@ -1,43 +0,0 @@
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
export class Vidora extends Extractor {
public readonly id = 'vidora';
public readonly label = 'Vidora';
public override readonly ttl: number = 43200000; // 12h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidora/);
}
public override normalize(url: URL): URL {
return new URL(url.href.replace('/embed/', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);
const title = $('title').text().trim().replace(/^Watch /, '').trim();
const m3u8Url = extractUrlFromPacked(html, [/file: ?"(.*?)"/]);
const headers = { Origin: url.origin };
return [
{
url: m3u8Url,
format: Format.hls,
meta: {
...meta,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
title,
},
requestHeaders: headers,
},
];
};
}

View file

@ -1,30 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VixSrc } from './VixSrc';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new VixSrc(new FetcherMock(`${__dirname}/__fixtures__/VixSrc`))]);
const ctx = createTestContext();
describe('VixSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'))).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
});
test('Rental Family', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/1208348'))).toMatchSnapshot();
});
test('Black Mirror is excluded if no matching language was found', async () => {
const ctx = createTestContext({ de: 'on' });
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/3'))).toMatchSnapshot();
});
});

View file

@ -1,65 +0,0 @@
import { Context, CountryCode, Format, InternalUrlResult, Meta } from '../types';
import { CustomRequestConfig, guessHeightFromPlaylist, hasMultiEnabled, iso639FromCountryCode } from '../utils';
import { Extractor } from './Extractor';
export class VixSrc extends Extractor {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc';
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vixsrc/);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: url.href };
const html = await this.fetcher.text(ctx, url);
const tokenMatch = html.match(/['"]token['"]: ?['"](.*?)['"]/) as string[];
const expiresMatch = html.match(/['"]expires['"]: ?['"](.*?)['"]/) as string[];
const urlMatch = html.match(/url: ?['"](.*?)['"]/) as string[];
const baseUrl = new URL(`${urlMatch[1]}`);
const playlistUrl = new URL(`${baseUrl.origin}${baseUrl.pathname}.m3u8?${baseUrl.searchParams}`);
playlistUrl.searchParams.append('token', tokenMatch[1] as string);
playlistUrl.searchParams.append('expires', expiresMatch[1] as string);
playlistUrl.searchParams.append('h', '1');
const countryCodes = meta.countryCodes ?? [CountryCode.multi, ...(await this.determineCountryCodesFromPlaylist(ctx, playlistUrl, { headers }))];
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
return [];
}
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
countryCodes,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
},
},
];
};
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestConfig): Promise<CountryCode[]> {
const playlist = await this.fetcher.text(ctx, playlistUrl, init);
const countryCodes: CountryCode[] = [];
(Object.keys(CountryCode) as CountryCode[]).forEach((countryCode) => {
const iso639 = iso639FromCountryCode(countryCode);
if (!countryCodes.includes(countryCode) && (new RegExp(`#EXT-X-MEDIA:TYPE=AUDIO.*LANGUAGE="${iso639}"`)).test(playlist)) {
countryCodes.push(countryCode);
}
});
return countryCodes;
}
}

View file

@ -1,28 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Voe } from './Voe';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Voe(new FetcherMock(`${__dirname}/__fixtures__/Voe`))]);
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow.test.org', mediaFlowProxyPassword: 'test' });
describe('Voe', () => {
test('jilliandescribecompany', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/e/ea21l02gcygw'))).toMatchSnapshot();
});
test('premium only without resolution', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://jilliandescribecompany.com/qqfyi04w52mj'))).toMatchSnapshot();
});
test('encoding error', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://voe.sx/e/c2yxvit4f6bz'))).toMatchSnapshot();
});
test('embed only urls which otherwise lead to 404', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mikaylaarealike.com/e/gqlhm9hbobwu'))).toMatchSnapshot();
});
});

View file

@ -1,180 +0,0 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/voesx.py */
export class Voe extends Extractor {
public readonly id = 'voe';
public readonly label = 'VOE';
public override viaMediaFlowProxy = true;
public supports(ctx: Context, url: URL): boolean {
const supportedDomain = null !== url.host.match(/voe/)
|| [
'19turanosephantasia.com',
'20demidistance9elongations.com',
'30sensualizeexpression.com',
'321naturelikefurfuroid.com',
'35volitantplimsoles5.com',
'449unceremoniousnasoseptal.com',
'745mingiestblissfully.com',
'adrianmissionminute.com',
'alleneconomicmatter.com',
'antecoxalbobbing1010.com',
'apinchcaseation.com',
'audaciousdefaulthouse.com',
'availedsmallest.com',
'bigclatterhomesguideservice.com',
'boonlessbestselling244.com',
'bradleyviewdoctor.com',
'brittneystandardwestern.com',
'brucevotewithin.com',
'christopheruntilpoint.com',
'chromotypic.com',
'chuckle-tube.com',
'cindyeyefinal.com',
'counterclockwisejacky.com',
'crownmakermacaronicism.com',
'crystaltreatmenteast.com',
'cyamidpulverulence530.com',
'diananatureforeign.com',
'donaldlineelse.com',
'edwardarriveoften.com',
'erikcoldperson.com',
'figeterpiazine.com',
'fittingcentermondaysunday.com',
'fraudclatterflyingcar.com',
'gamoneinterrupted.com',
'generatesnitrosate.com',
'goofy-banana.com',
'graceaddresscommunity.com',
'greaseball6eventual20.com',
'guidon40hyporadius9.com',
'heatherdiscussionwhen.com',
'housecardsummerbutton.com',
'jamessoundcost.com',
'jamiesamewalk.com',
'jasminetesttry.com',
'jayservicestuff.com',
'jennifercertaindevelopment.com',
'jilliandescribecompany.com',
'johnalwayssame.com',
'jonathansociallike.com',
'josephseveralconcern.com',
'kathleenmemberhistory.com',
'kellywhatcould.com',
'kennethofficialitem.com',
'kinoger.ru',
'kristiesoundsimply.com',
'lancewhosedifficult.com',
'launchreliantcleaverriver.com',
'lauradaydo.com',
'lisatrialidea.com',
'loriwithinfamily.com',
'lukecomparetwo.com',
'lukesitturn.com',
'mariatheserepublican.com',
'matriculant401merited.com',
'maxfinishseveral.com',
'metagnathtuggers.com',
'michaelapplysome.com',
'mikaylaarealike.com',
'nathanfromsubject.com',
'nectareousoverelate.com',
'nonesnanking.com',
'paulkitchendark.com',
'realfinanceblogcenter.com',
'rebeccaneverbase.com',
'reputationsheriffkennethsand.com',
'richardsignfish.com',
'roberteachfinal.com',
'robertordercharacter.com',
'robertplacespace.com',
'sandratableother.com',
'sandrataxeight.com',
'scatch176duplicities.com',
'sethniceletter.com',
'shannonpersonalcost.com',
'simpulumlamerop.com',
'smoki.cc',
'stevenimaginelittle.com',
'strawberriesporail.com',
'telyn610zoanthropy.com',
'timberwoodanotia.com',
'toddpartneranimal.com',
'toxitabellaeatrebates306.com',
'uptodatefinishconferenceroom.com',
'v-o-e-unblock.com',
'valeronevijao.com',
'walterprettytheir.com',
'wolfdyslectic.com',
'yodelswartlike.com',
].includes(url.host);
return supportedDomain && supportsMediaFlowProxy(ctx);
}
public override normalize(url: URL): URL {
return new URL(`/${url.pathname.replace(/\/+$/, '').split('/').at(-1)}`, url);
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
let html: string;
try {
html = await this.fetcher.text(ctx, url, { headers });
} catch (error) {
/* istanbul ignore next */
if (error instanceof NotFoundError && !url.href.includes('/e/')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
}
/* istanbul ignore next */
throw error;
}
const redirectMatch = html.match(/window\.location\.href\s*=\s*'([^']+)/);
if (redirectMatch && redirectMatch[1]) {
return await this.extractInternal(ctx, new URL(redirectMatch[1]), meta);
}
if (/An error occurred during encoding/.test(html)) {
throw new NotFoundError();
}
const $ = cheerio.load(html);
const title = $('meta[name="description"]').attr('content')?.trim().replace(/^Watch /, '').replace(/ at VOE$/, '').trim();
const sizeMatch = html.matchAll(/[\d.]+ ?[GM]B/g).toArray().at(-1);
const size = sizeMatch ? bytes.parse(sizeMatch[0] as string) as number : /* istanbul ignore next */null;
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Voe', url, headers);
const heightMatch = html.match(/<b>(\d{3,})p<\/b>/);
const height = heightMatch
? parseInt(heightMatch[1] as string)
: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl);
return [
{
url: playlistUrl,
format: Format.hls,
meta: {
...meta,
height,
title,
...(size && size > 16777216 && { bytes: size }),
},
},
];
};
}

View file

@ -1,20 +0,0 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { YouTube } from './YouTube';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new YouTube(new FetcherMock(`${__dirname}/__fixtures__/YouTube`))]);
const ctx = createTestContext();
describe('YouTube', () => {
test('Solaris', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'))).toMatchSnapshot();
});
test('unsupported format', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'))).toMatchSnapshot();
});
});

View file

@ -1,34 +0,0 @@
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Extractor } from './Extractor';
export class YouTube extends Extractor {
public readonly id = 'youtube';
public readonly label = 'YouTube';
public override readonly ttl: number = 21600000; // 6h
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
const html = await this.fetcher.text(ctx, url, { headers });
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
return [
{
url,
format: Format.unknown,
ytId: url.searchParams.get('v') as string,
meta: {
...meta,
title: titleMatch[1] as string,
},
},
];
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <title>Video not found | DoodStream</title> <link rel="preconnect" href="//i.doodcdn.io"><link rel="preconnect" href="//cdnjs.cloudflare.com"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <link rel="stylesheet" href="//i.doodcdn.io/theme_2/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="//i.doodcdn.io/theme_2/css/style.css" /></head><body> <style type="text/css"> .not_found img {height:250px !important;} @media (max-width: 768px){.not_found h1{font-size:1.8rem;}.not_found img {height:200px !important;margin: 0 auto 15px;}} @media (max-width: 480px){.not_found h1{font-size:1.4rem;}.not_found img {height:120px !important;margin: 0 auto 15px;}.not_found p{margin-top: -5px;}} </style> <div class="container pt-5 not_found"> <div class="row"> <div class="col-md-12 pt-2 text-center"> <img src="//i.doodcdn.io/img/no_video_3.svg"> <h1>Not Found</h1> <p>video you are looking for is not found.</p> </div> </div> </div><script data-cfasync="false" src="//d1f05vr3sjsuy7.cloudfront.net/?srvfd=908056"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/gHzOaAdOhbZ/71405"></script><script data-cfasync="false" async type="text/javascript" src="//faqirsgoliard.top/r67c0fc81985e5/70849"></script><script type="text/javascript" data-cfasync="false">/*<![CDATA[/* */(function(){var g=window,r="ca0967284e67bc53cb5b626985619403",w=[["siteId",317+185*82*507-3600486],["minBid",0],["popundersPerIP","0"],["delayBetween",300],["default","https://strettechoco.com/iYa7dlULceKw/27615"],["defaultPerDay",0],["topmostLayer","never"]],n=["d3d3LmJsb2NrYWRzbm90LmNvbS9iYWphLm1pbi5jc3M=","ZG5oZmk1bm4yZHQ2Ny5jbG91ZGZyb250Lm5ldC9RVHd0eC91YWxnZWJyYS5taW4uanM=","d3d3LmJibHZwbGd1b21hLmNvbS9mYWphLm1pbi5jc3M=","d3d3LndqZ3BzcGt4empiZGd1LmNvbS9NTmlIWS9rYWxnZWJyYS5taW4uanM="],f=-1,v,q,h=function(){clearTimeout(q);f++;if(n[f]&&!(1766620801000<(new Date).getTime()&&1<f)){v=g.document.createElement("script");v.type="text/javascript";v.async=!0;var o=g.document.getElementsByTagName("script")[0];v.src="https://"+atob(n[f]);v.crossOrigin="anonymous";v.onerror=h;v.onload=function(){clearTimeout(q);g[r.slice(0,16)+r.slice(0,16)]||h()};q=setTimeout(h,5E3);o.parentNode.insertBefore(v,o)}};if(!g[r]){try{Object.freeze(g[r]=w)}catch(e){}h()}})();/*]]>/* */</script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015" integrity="sha512-ZpsOmlRQV6y907TI0dKBHq9Md29nnaEIPlkf84rnaERnq6zvWvPUqr2ft8M1aS28oN72PdrCzSjY4U6VaAw1EQ==" data-cf-beacon='{"version":"2024.11.0","token":"1c76230f8ab248eab0b0f2bf7c205c51","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,431 +0,0 @@
<!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="1198ec03089106bda1a7dcfc-text/javascript"></script>
<script src="https://dropload.io/assets2/js/bootstrap.bundle.min.js" type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<script src="https://dropload.io/assets2/js/xupload.js?v=13" type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<script src="https://dropload.io/assets2/js/app.js" type="1198ec03089106bda1a7dcfc-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 src="https://dropload.io/js/jquery.cookie.js" type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<script type="1198ec03089106bda1a7dcfc-text/javascript">
$.cookie('file_id', '1062326', { expires: 10 });
$.cookie('aff', '1805', { expires: 10 });
</script>
<section class="mb-5 mb-lg-7">
<div class="container">
<div class="videoplayer">
<div class="videoplayer-embed">
<img src="https://img.dropload.io/7xtxuac84xyh.jpg" alt="">
<div class="encoding">
<div class="encoding-body">
<div class="row align-items-center">
<div class="col-auto">
<div class="percent">
75%
</div>
</div>
<div class="col-auto">
<h6>Pending in queue</h6>
<small class="text-light">Video is processing now.<br>Conversion stage: <span id='enc_pp'>...</span></small>
</div>
</div>
</div>
</div>
</div>
<div class="lightdown-backdrop"></div>
<h1>Bates Motel 2x3 </h1>
<div class="videoplayer-controlbar">
<div class="row g-1">
<div class="col-auto flex-grow-1 flex-shrink-1 mw-0">
<div class="videoplayer-details">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-shrink-1 mw-0">
<div class="row flex-lg-nowrap">
<div class="col-auto d-flex align-items-center">
<i class="icon icon-clock icon-size-16 me-2 opacity-50"></i>
on Aug 6, 2024
</div>
<div class="col-auto d-flex align-items-center">
<i class="icon icon-download2 icon-size-16 me-2 opacity-50"></i>
<span id="size"></span>
</div>
<div class="col-auto d-flex align-items-center">
<i class="icon icon-clock icon-size-16 me-2 opacity-50"></i>
00:42:55
</div>
</div>
</div>
<div class="col-auto d-flex align-items-center">
<button class="btn icon-btn me-4" data-bs-toggle="modal" data-bs-target="#modal_addplaylist">
<i class="icon icon-addlist icon-size-16"></i>
</button>
<button type="button" class="btn icon-btn text-danger" data-bs-toggle="modal" data-bs-target="#modal_flag">
<i class="icon icon-flag icon-size-16"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-auto">
<button class="btn btn-like border-0" onclick="if (!window.__cfRLUnblockHandlers) return false; return jah('/?op=vote&file_code=7xtxuac84xyh&v=up')" title="Like" data-cf-modified-1198ec03089106bda1a7dcfc-="">
<i class="icon icon-like icon-size-16 me-2"></i>
<span id="likes_num">0</span>
</button>
</div>
<div class="col-auto">
<button class="btn btn-dislike border-0" onclick="if (!window.__cfRLUnblockHandlers) return false; return jah('/?op=vote&file_code=7xtxuac84xyh&v=down')" title="Don&#39;t Like" data-cf-modified-1198ec03089106bda1a7dcfc-="">
<i class="icon icon-dislike icon-size-16 me-2"></i>
<span id="dislikes_num">0</span>
</button>
</div>
</div>
</div>
<div class="mb-5">
<div class="row justify-content-center justify-content-lg-between">
<div class="col-12 col-lg-7 mb-3">
<div class="copybox">
<textarea class="form-control copy-source" rows="6"><IFRAME SRC="https://dropload.io/e/7xtxuac84xyh" FRAMEBORDER=0 MARGINWIDTH=0 MARGINHEIGHT=0 SCROLLING=NO WIDTH=640 HEIGHT=360 allowfullscreen></IFRAME></textarea>
<button class="btn btn-primary" onclick="if (!window.__cfRLUnblockHandlers) return false; copy(this);" data-cf-modified-1198ec03089106bda1a7dcfc-="">Copy</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="modal fade" id="modal_flag" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Flag</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-0">
<form method="POST" action="/" onsubmit="if (!window.__cfRLUnblockHandlers) return false; $.post('/',$(this).serialize(),function(code){ eval(code); } );return false;" data-cf-modified-1198ec03089106bda1a7dcfc-="">
<input type="hidden" name="op" value="report_file">
<input type="hidden" name="file_code" value="7xtxuac84xyh">
<div class="mb-3">
<label class="form-label">Reason:</label>
<select class="form-select" name="report_type">
<option>Broken video</option>
<option>Wrong title/description</option>
<option>Copyright restriction</option>
<option>Other</option>
</select>
</div>
<div class="mb-3">
<textarea name="report_info" class="form-control" rows="2" placeholder="Details"></textarea>
</div>
<div class="text-center">
<input type="submit" name="add" value="Send Report" class="btn btn-gradient submit-btn">
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modal_addplaylist" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Add to</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-0">
</div>
</div>
</div>
</div>
<div class="modal fade" id="modal_share" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Share</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-0">
Share code
</div>
</div>
</div>
</div>
<style media="screen">
.lightdown {
position: absolute;
top: 20px;
right: 20px;
width: 40px;
height: 40px;
border: 0;
padding: 0;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill-rule='evenodd' fill='%23FFF' d='M10 20c-1.654 0-3-1.346-3-3v-.531c0-.625-.254-1.237-.695-1.679l-.548-.548A5.96 5.96 0 0 1 4 10c0-1.603.624-3.109 1.757-4.243A5.961 5.961 0 0 1 10 4a5.96 5.96 0 0 1 4.242 1.757A5.96 5.96 0 0 1 16 10a5.957 5.957 0 0 1-1.758 4.242l-.546.547a2.396 2.396 0 0 0-.696 1.68V17c0 1.654-1.346 3-3 3Zm-1.025-4c.017.154.025.311.025.469V17a1.001 1.001 0 0 0 2 0v-.531c0-.158.008-.315.024-.469H8.975Zm-.737-2h3.524c.152-.222.325-.431.519-.624l.546-.547A3.974 3.974 0 0 0 14 10a3.975 3.975 0 0 0-1.172-2.829c-1.512-1.51-4.146-1.51-5.657 0A3.978 3.978 0 0 0 6 10c0 1.068.416 2.072 1.171 2.828h.001l.547.548c.194.194.367.402.519.624ZM19 11h-1a1 1 0 1 1 0-2h1a1 1 0 1 1 0 2ZM2 11H1a1 1 0 1 1 0-2h1a1 1 0 0 1 0 2Zm13.657-5.657a.999.999 0 0 1-.707-1.707l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a.993.993 0 0 1-.707.293Zm-11.314 0a.993.993 0 0 1-.707-.293l-.707-.707a.999.999 0 1 1 1.414-1.414l.707.707a.999.999 0 0 1-.707 1.707ZM10 3a1 1 0 0 1-1-1V1a1 1 0 0 1 2 0v1a1 1 0 0 1-1 1Z'/%3E%3C/svg%3E") no-repeat center;
background-color: rgba(28,37,52,.75);
transition: .25s;
opacity: 0;
}
.videoplayer-embed:hover .lightdown {
opacity: 1;
}
.body-lightdown .lightdown {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill-rule='evenodd' fill='%233DB6D4' d='M10 20c-1.654 0-3-1.346-3-3v-.531c0-.625-.254-1.237-.695-1.679l-.548-.548A5.96 5.96 0 0 1 4 10c0-1.603.624-3.109 1.757-4.243A5.961 5.961 0 0 1 10 4a5.96 5.96 0 0 1 4.242 1.757A5.96 5.96 0 0 1 16 10a5.957 5.957 0 0 1-1.758 4.242l-.546.547a2.396 2.396 0 0 0-.696 1.68V17c0 1.654-1.346 3-3 3Zm-1.025-4c.017.154.025.311.025.469V17a1.001 1.001 0 0 0 2 0v-.531c0-.158.008-.315.024-.469H8.975Zm-.737-2h3.524c.152-.222.325-.431.519-.624l.546-.547A3.974 3.974 0 0 0 14 10a3.975 3.975 0 0 0-1.172-2.829c-1.512-1.51-4.146-1.51-5.657 0A3.978 3.978 0 0 0 6 10c0 1.068.416 2.072 1.171 2.828h.001l.547.548c.194.194.367.402.519.624ZM19 11h-1a1 1 0 1 1 0-2h1a1 1 0 1 1 0 2ZM2 11H1a1 1 0 1 1 0-2h1a1 1 0 0 1 0 2Zm13.657-5.657a.999.999 0 0 1-.707-1.707l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707a.993.993 0 0 1-.707.293Zm-11.314 0a.993.993 0 0 1-.707-.293l-.707-.707a.999.999 0 1 1 1.414-1.414l.707.707a.999.999 0 0 1-.707 1.707ZM10 3a1 1 0 0 1-1-1V1a1 1 0 0 1 2 0v1a1 1 0 0 1-1 1Z'/%3E%3C/svg%3E");
}
.lightdown-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,.9);
display: none;
z-index: 1999;
}
.body-lightdown .lightdown-backdrop {
display: block;
}
.body-lightdown .videoplayer-embed {
z-index: 2000;
}
</style>
<script type="1198ec03089106bda1a7dcfc-text/javascript">
$('.lightdown').on('click', function(){
$('body').toggleClass('body-lightdown');
})
</script>
<Script type="1198ec03089106bda1a7dcfc-text/javascript">
$("#embed_div textarea").focus(function (){ $(this).select(); })
var tab_cookie='tab_down';
$('ul.tabs').on('click', 'li:not(.current)', function() {
$(this).addClass('current').siblings().removeClass('current')
.parents('div.section').find('div.box').eq($(this).index()).fadeIn(150).siblings('div.box').hide();
});
$("#iew,#ieh").change(function (){
var ww = $("#iew").val();
var hh = $("#ieh").val();
ww = ww.replace(/\D/g,'');
hh = hh.replace(/\D/g,'');
if(!ww)ww=640;
if(!hh)hh=360;
tt = $("#iet").val();
tt = tt.replace(/ WIDTH=\d+ HEIGHT=\d+ /,' WIDTH='+ww+' HEIGHT='+hh+' ');
$("#iet").val(tt);
});
encStatus('1062326',1);
</Script>
<script src="https://dropload.io/js/tabber.js" type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<Script type="1198ec03089106bda1a7dcfc-text/javascript">
$(function() {
});
</Script>
<script data-cfasync="false" async type="text/javascript" src="//en.almanpureed.com/rWm0dvLNb29/111561"></script>
<script src='https://dropload.io/tag2.js' type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<script src='https://dropload.io/tag1.js' type="1198ec03089106bda1a7dcfc-text/javascript"></script>
<!-- Javascript Alt Ads code start -->
<!-- Javascript Alt Ads code end -->
</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="1198ec03089106bda1a7dcfc-|49" defer></script></body>
</html>

View file

@ -1,160 +0,0 @@
<!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="ad75555b092545103e6c43f1-text/javascript"></script>
<script src="https://dropload.io/assets2/js/bootstrap.bundle.min.js" type="ad75555b092545103e6c43f1-text/javascript"></script>
<script src="https://dropload.io/assets2/js/xupload.js?v=13" type="ad75555b092545103e6c43f1-text/javascript"></script>
<script src="https://dropload.io/assets2/js/app.js" type="ad75555b092545103e6c43f1-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="//en.almanpureed.com/rWm0dvLNb29/111561"></script>
<script src='https://dropload.io/tag2.js' type="ad75555b092545103e6c43f1-text/javascript"></script>
<script src='https://dropload.io/tag1.js' type="ad75555b092545103e6c43f1-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="ad75555b092545103e6c43f1-|49" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3955353,RESOLUTION=1920x800,FRAME-RATE=23.974,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR
index-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=68483,RESOLUTION=1920x800,CODECS="avc1.640028",URI="iframes-v1-a1.m3u8?t=jq0VCV2dOyLIIH07IGIpzruCXvbfdVqzGLWfMeo00AM&s=1759845990&e=14400&f=1571895&srv=ds3&i=0.0&sp=0&ii=2.184.0.222&p1=ds3&p2=ds3",VIDEO-RANGE=SDR

View file

@ -1 +0,0 @@
Fetcher error: 404: Not Found

View file

@ -1 +0,0 @@
TypeError: fetch failed

View file

@ -1,160 +0,0 @@
<!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="67d4e7c92cd803f904f519b6-text/javascript"></script>
<script src="https://dropload.io/assets2/js/bootstrap.bundle.min.js" type="67d4e7c92cd803f904f519b6-text/javascript"></script>
<script src="https://dropload.io/assets2/js/xupload.js?v=13" type="67d4e7c92cd803f904f519b6-text/javascript"></script>
<script src="https://dropload.io/assets2/js/app.js" type="67d4e7c92cd803f904f519b6-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="//en.almanpureed.com/rWm0dvLNb29/111561"></script>
<script src='https://dropload.io/tag2.js' type="67d4e7c92cd803f904f519b6-text/javascript"></script>
<script src='https://dropload.io/tag1.js' type="67d4e7c92cd803f904f519b6-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="67d4e7c92cd803f904f519b6-|49" defer></script></body>
</html>

View file

@ -1 +0,0 @@
Fetcher error: 403: Forbidden

View file

@ -1 +0,0 @@
Fetcher error: 403: Forbidden

Some files were not shown because too many files have changed in this diff Show more