Compare commits

..

2 commits

Author SHA1 Message Date
WebStreamr
5c2d3bba01
WIP: refactor doodstream 2025-08-25 13:05:13 +00:00
WebStreamr
e8ad568327
hore(extractor): add more KinoGer domains 2025-08-24 13:40:47 +00:00
682 changed files with 35771 additions and 126960 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
on:
push:
pull_request:
workflow_dispatch:
on: push
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v7
uses: docker/build-push-action@v6
with:
push: false
tags: webstreamr/webstreamr:latest

View file

@ -1,17 +1,26 @@
name: Tests
on:
push:
pull_request:
workflow_dispatch:
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
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
run: npm install

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,47 +1,15 @@
# WebStreamr
[![Tests](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml/badge.svg)](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
[![Tests](https://github.com/webstreamr/webstreamr/workflows/Tests/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!
@ -54,64 +22,12 @@ Self-hosting to stressful? ElfHosted offer [ready-to-go, turn-key WebStreamr ins
### Self-Hosting
You can run the latest WebStreamr via Docker. E.g.
You can build an image using the [Dockerfile](./Dockerfile) and run it.
Alternatively, you can also start WebStreamr directly via
```shell
docker run \
--detach=true \
--name webstreamr \
--rm \
--pull always \
--publish 51546:51546 \
--env TMDB_ACCESS_TOKEN="YOUR_TOKEN" \
--volume /tmp:/tmp \
webstreamr/webstreamr
npm install
npm run build
npm start
```
### 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

@ -13,7 +13,6 @@ const config: Config = {
coverageDirectory: '<rootDir>/coverage',
coveragePathIgnorePatterns: [
'/src/controller/',
'/src/utils/dispatcher.ts',
],
coverageProvider: 'babel',
coverageThreshold: {

View file

@ -1,10 +1,5 @@
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';
}
import { socksDispatcher } from 'fetch-socks';
import { Agent, Dispatcher, interceptors, ProxyAgent, setGlobalDispatcher } from 'undici';
jest.mock('randomstring', () => ({
generate: jest.fn(() => 'mocked-random-string'),
@ -14,4 +9,19 @@ beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 639837296000);
});
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };
let dispatcher: Dispatcher;
if (process.env['ALL_PROXY']) {
const proxyUrl = new URL(process.env['ALL_PROXY']);
if (proxyUrl.protocol === 'socks5:') {
dispatcher = socksDispatcher({ type: 5, host: proxyUrl.hostname, port: parseInt(proxyUrl.port) }, { allowH2: true });
} else {
dispatcher = new ProxyAgent({ uri: proxyUrl.href, allowH2: true });
}
} else {
dispatcher = new Agent({ allowH2: true });
}
dispatcher.compose(
interceptors.dns(),
interceptors.retry({ maxRetries: 3 }),
);
setGlobalDispatcher(dispatcher);

5520
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "webstreamr",
"description": "Provides HTTP URLs from streaming websites.",
"version": "0.69.1",
"version": "0.44.1",
"type": "commonjs",
"scripts": {
"analyse": "tsc --noEmit --project ./tsconfig.dev.json",
@ -28,55 +28,37 @@
"url": "https://github.com/webstreamr/webstreamr/issues"
},
"engines": {
"node": "^22.0.0 || ^24.0.0"
},
"devEngines": {
"runtime": {
"name": "node",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"onFail": "error"
}
"node": "^22.0.0"
},
"dependencies": {
"@keyv/sqlite": "^4.0.5",
"@isaacs/ttlcache": "^1.4.1",
"@types/jsdom": "^21.1.7",
"async-mutex": "^0.5.0",
"axios": "^1.13.2",
"axios-cache-interceptor": "^1.8.3",
"axios-mock-adapter": "^2.1.0",
"axios-retry": "^4.5.0",
"bytes": "^3.1.2",
"cacheable": "^2.0.0",
"cheerio": "^1.0.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",
"fetch-socks": "^1.3.2",
"http-cache-semantics": "^4.2.0",
"jsdom": "^26.1.0",
"lru-cache": "^11.1.0",
"randomstring": "^1.3.1",
"rot13-cipher": "^1.0.0",
"slugify": "^1.6.6",
"socks-proxy-agent": "^8.0.5",
"sqlite3": "^5.1.7",
"tough-cookie": "^6.0.0",
"undici": "^7.10.0",
"unpacker": "^1.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@stylistic/eslint-plugin": "^5.0.0",
"@tsconfig/node24": "^24.0.0",
"@tsconfig/node22": "^22.0.2",
"@types/bytes": "^3.1.5",
"@types/express": "^5.0.1",
"@types/fast-levenshtein": "^0.0.4",
"@types/http-cache-semantics": "^4.0.4",
"@types/jest": "^30.0.0",
"@types/memoizee": "^0.4.12",
"@types/node": "^24.0.0",
"@types/make-fetch-happen": "^10.0.4",
"@types/node": "^22.15.3",
"@types/randomstring": "^1.3.0",
"@types/stremio-addon-sdk": "^1.6.11",
"babel-jest": "^30.0.0",

View file

@ -22,7 +22,7 @@ export class ConfigureController {
}
private 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'] || JSON.stringify(getDefaultConfig()));
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy
if (!req.params['config'] && isElfHostedInstance(req)) {

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

@ -21,7 +21,7 @@ export class ManifestController {
}
private 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'] || JSON.stringify(getDefaultConfig()));
const manifest = buildManifest(this.sources, this.extractors, config);

View file

@ -5,6 +5,8 @@ import winston from 'winston';
import { Source } from '../source';
import { contextFromRequestAndResponse, envIsProd, Id, ImdbId, StreamResolver, TmdbId } from '../utils';
const locks = new Map<string, Mutex>();
export class StreamController {
public readonly router: Router;
@ -12,8 +14,6 @@ export class StreamController {
private readonly sources: Source[];
private readonly streamResolver: StreamResolver;
private readonly locks = new Map<string, Mutex>();
public constructor(logger: winston.Logger, sources: Source[], streams: StreamResolver) {
this.router = Router();
@ -27,7 +27,7 @@ export class StreamController {
private async getStream(req: Request, res: Response) {
const type: ContentType = (req.params['type'] || '') as ContentType;
const rawId: string = req.params['id'] as string || '';
const rawId: string = req.params['id'] || '';
let id: Id;
if (rawId.startsWith('tmdb:')) {
@ -35,9 +35,7 @@ export class StreamController {
} else if (rawId.startsWith('tt')) {
id = ImdbId.fromString(rawId);
} else {
res.status(400).send(`Unsupported ID: ${rawId}`);
return;
throw new Error(`Unsupported ID: ${rawId}`);
}
const ctx = contextFromRequestAndResponse(req, res);
@ -46,17 +44,17 @@ export class StreamController {
const sources = this.sources.filter(source => source.countryCodes.filter(countryCode => countryCode in ctx.config).length);
let mutex = this.locks.get(rawId);
let mutex = locks.get(rawId);
if (!mutex) {
mutex = new Mutex();
this.locks.set(rawId, mutex);
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('Cache-Control', `max-age=${ttl / 1000}, public`);
}
res.setHeader('Content-Type', 'application/json');
@ -64,7 +62,7 @@ export class StreamController {
});
if (!mutex.isLocked()) {
this.locks.delete(rawId);
locks.delete(rawId);
}
};
}

View file

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

View file

@ -1,15 +1,12 @@
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 readonly headers: Record<string, string[] | string | undefined>;
public constructor(url: URL, reason: BlockedReason, headers: AxiosResponse['headers']) {
public constructor(reason: BlockedReason, headers: Record<string, string[] | string | undefined>) {
super();
this.url = url;
this.reason = reason;
this.headers = headers;
}

View file

@ -1,15 +1,11 @@
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 readonly headers: Record<string, string[] | string | undefined>;
public constructor(url: URL, status: number, statusText: string, headers: AxiosResponse['headers']) {
public constructor(status: number, statusText: string, headers: Record<string, string[] | string | undefined>) {
super();
this.url = url;
this.status = status;
this.statusText = statusText;
this.headers = headers;

View file

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

View file

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

View file

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

View file

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

View file

@ -17,54 +17,51 @@ 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) {
if (error.reason === BlockedReason.cloudflare_challenge) {
logger.warn(`${source}: Request was blocked via Cloudflare challenge.`, ctx);
} else if (error.reason === BlockedReason.cloudflare_censor) {
logger.warn(`${source}: Request was censored by Cloudflare.`, ctx);
} else if (error.reason === BlockedReason.media_flow_proxy_auth) {
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
} else {
logger.warn(`${source}: Request was blocked, headers: ${JSON.stringify(error.headers)}.`, ctx);
}
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}`;
return `⚠️ Request was blocked. Reason: ${error.reason}`;
}
if (error instanceof TooManyRequestsError) {
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
logger.warn(`${source}: Rate limited for ${error.retryAfter} seconds.`, ctx);
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
return '🚦 Request 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);
logger.warn(`${source}: Too many timeouts.`, ctx);
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
return '🚦 Too many recent timeouts. 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);
logger.warn(`${source}: Request queue is full.`, ctx);
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
return '⏳ Request queue 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);
logger.error(`${source}: 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 `❌ Remote server 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}.`;
return `❌ Request failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
}
const cause = (error as Error & { cause?: unknown }).cause;

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { DoodStream } from './DoodStream';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -7,18 +8,26 @@ 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' });
const ctx = createTestContext();
describe('DoodStream', () => {
test('dood.to', async () => {
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('http://dood.to/e/sk1m9eumzyjj'), CountryCode.de)).toMatchSnapshot();
});
test('doodster', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'), CountryCode.de)).toMatchSnapshot();
});
test('missing pass_md5 -> not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'), CountryCode.mx)).toMatchSnapshot();
});
test('can guess height from title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'), CountryCode.fr)).toMatchSnapshot();
});
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://myvidplay.com/e/cz7cus0bvlzr'))).toMatchSnapshot();
test('cloudflare storage', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'), CountryCode.mx)).toMatchSnapshot();
});
});

View file

@ -1,8 +1,9 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import randomstring from 'randomstring';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class DoodStream extends Extractor {
@ -10,44 +11,102 @@ export class DoodStream extends Extractor {
public readonly label = 'DoodStream';
public override readonly ttl: number = 21600000; // 6h
private readonly fetcher: Fetcher;
public override viaMediaFlowProxy = true;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
/** @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 supports(_ctx: Context, url: URL): boolean {
return [
'all3do.com',
'd-s.io',
'd0000d.com',
'd000d.com',
'd0o0d.com',
'do0od.com',
'do7go.com',
'dood.cx',
'dood.la',
'dood.li',
'dood.pm',
'dood.re',
'dood.sh',
'dood.so',
'dood.stream',
'dood.to',
'dood.watch',
'dood.wf',
'dood.work',
'dood.ws',
'dood.yt',
'doodcdn.io',
'doods.pro',
'doodstream.co',
'doodstream.com',
'dooodster.com',
'dooood.com',
'doply.net',
'ds2play.com',
'ds2video.com',
'vide0.net',
'vidply.com',
'vvide0.com',
].includes(url.host);
};
public override normalize(url: URL): URL {
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
const supportedHosts = [
'all3do.com',
'd-s.io',
'doodstream.com',
'vide0.net',
'vidply.com',
];
const host = supportedHosts.includes(url.host) ? url.host : 'd-s.io';
return new URL(`http://dood.to/e/${videoId}`);
const videoId = url.pathname.split('/').slice(-1)[0] as string;
return new URL(`/d/${videoId}`, `${url.protocol}//${host}`);
};
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: meta.referer ?? url.href };
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const html = await this.fetcher.text(ctx, url, { headers });
if (/Video not found/.test(html)) {
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);
if (!passMd5Match) {
throw new NotFoundError();
}
const token = passMd5Match[1] as string;
const baseUrl = await this.fetcher.text(ctx, new URL(passMd5Match[0], url.origin));
const $ = cheerio.load(html);
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
const downloadHtml = await this.fetcher.text(ctx, new URL(url.href.replace('/e/', '/d/')));
const sizeMatch = downloadHtml.match(/([\d.]+ ?[GM]B)/);
const mp4Url = baseUrl.includes('cloudflarestorage.')
? new URL(baseUrl) // TODO: headers needed here? compare with resolveurl
: new URL(`${baseUrl}${randomstring.generate(10)}?token=${token}&expiry=${Date.now()}`);
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Doodstream', url, headers),
url: mp4Url,
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
title,
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
bytes: bytes.parse(sizeMatch[1] as string) as number,
},
requestHeaders: {
Referer: url.origin,
},
},
];

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { Dropload } from './Dropload';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -11,22 +12,18 @@ 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();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'), CountryCode.de)).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de)).toMatchSnapshot();
});
test('processing / internal problem', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/7xtxuac84xyh.html'), CountryCode.de)).toMatchSnapshot();
});
test('download URL', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/d/xhcmgcc2txnv'), CountryCode.en)).toMatchSnapshot();
});
});

View file

@ -1,8 +1,8 @@
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 { Context, CountryCode, Format, UrlResult } from '../types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class Dropload extends Extractor {
@ -10,48 +10,47 @@ export class Dropload extends Extractor {
public readonly label = 'Dropload';
public override readonly ttl: number = 10800000; // 3h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/dropload|dr0pstream/);
return null !== url.host.match(/dropload/);
}
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 });
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
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,}),/) as string[];
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 sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
const $ = cheerio.load(html);
const title = $('.videoplayer h1').text().trim();
return [
{
url: playlistUrl,
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
bytes: bytes.parse(sizeMatch[1] as string) as number,
countryCodes: [countryCode],
height: parseInt(heightMatch[1] as string),
title,
...(size && { bytes: size }),
...(height && { height }),
},
requestHeaders: playlistHeaders,
},
];
};

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExternalUrl } from './ExternalUrl';
import { ExtractorRegistry } from './ExtractorRegistry';
@ -11,10 +12,14 @@ 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();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/gjA1OQ4klyHxgJ'), CountryCode.fr)).toMatchSnapshot();
});
test('netu.fanstream.us', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://netu.fanstream.us/e/0DFgfkcXOsDP'), CountryCode.fr)).toMatchSnapshot();
});
test('johnalwayssame.com with title', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://johnalwayssame.com/e/cqy9oue7sv0g'), CountryCode.fr, 'Black Mirror 4x2')).toMatchSnapshot();
});
});

View file

@ -1,5 +1,6 @@
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { showExternalUrls } from '../utils';
import { BlockedError } from '../error';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, showExternalUrls } from '../utils';
import { Extractor } from './Extractor';
export class ExternalUrl extends Extractor {
@ -7,20 +8,43 @@ export class ExternalUrl extends Extractor {
public readonly label = 'External';
public override readonly ttl = 21600000; // 6h
public override readonly ttl = 3600000; // 1h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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[]> {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
try {
// Make sure the URL is accessible, but avoid causing noise and delays doing this
await this.fetcher.head(ctx, url, { noFlareSolverr: true, timeout: 1000, headers: { Referer: url.origin } });
} catch (error) {
/* istanbul ignore if */
if (!(error instanceof BlockedError)) {
return [];
}
}
return [
{
url: url,
format: Format.unknown,
isExternal: true,
label: `${url.host}`,
meta,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
title,
},
},
];
};

View file

@ -1,6 +1,5 @@
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
export abstract class Extractor {
public abstract readonly id: string;
@ -9,34 +8,19 @@ export abstract class Extractor {
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[]>;
protected abstract extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]>;
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
public async extract(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
try {
return (await this.extractInternal(ctx, url, meta)).map(
urlResult => ({
...urlResult,
label: this.formatLabel(urlResult.label ?? this.label),
ttl: this.ttl,
}),
);
return await this.extractInternal(ctx, url, countryCode, title);
} catch (error) {
if (error instanceof NotFoundError) {
return [];
@ -48,15 +32,14 @@ export abstract class Extractor {
format: Format.unknown,
isExternal: true,
error,
label: this.formatLabel(this.label),
ttl: 0,
meta,
label: this.label,
sourceId: `${this.id}`,
meta: {
countryCodes: [countryCode],
...(title && { title }),
},
},
];
}
};
private formatLabel(label: string): string {
return this.viaMediaFlowProxy ? `${label} (MFP)` : label;
}
}

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { createExtractors } from './index';
@ -11,53 +12,46 @@ describe('ExtractorRegistry', () => {
const ctx = createTestContext();
test('returns error result from extractor', async () => {
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'));
const urlResult = await extractorRegistry.handle(ctx, new URL('https://some-url.test'), CountryCode.de);
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'));
const urlResult = await extractorRegistry.handle({ ...ctx, config: { ...ctx.config, includeExternalUrls: 'on' } }, new URL('https://mixdrop.ag/e/3nzwveprim63or6'), CountryCode.de);
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'));
const urlResult = await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/l7v73zqrfdj19z'), CountryCode.de);
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'));
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/lyo2h1snpe5c.html'), CountryCode.de);
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'));
const urlResults1 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
const urlResults2 = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), CountryCode.de);
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'));
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked.html'), CountryCode.de);
expect(urlResults).toMatchSnapshot();
});
test('empty results are cached', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { title: 'title' });
test('returns external url for error with title', async () => {
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/mocked-blocked-2.html'), CountryCode.de, 'a 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 +1,42 @@
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
import TTLCache from '@isaacs/ttlcache';
import winston from 'winston';
import { Context, Format, Meta, UrlResult } from '../types';
import { createKeyvSqlite, envGet, isExtractorDisabled } from '../utils';
import { Context, CountryCode, UrlResult } from '../types';
import { 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;
private readonly urlResultCache: TTLCache<string, UrlResult[]>;
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,
});
this.urlResultCache = new TTLCache({ max: 4096 });
}
public stats() {
return {
urlResultCache: this.urlResultCache.stats,
lazyUrlResultCache: this.lazyUrlResultCache.stats,
};
};
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): 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 cacheKey = `${extractor.id}_${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) }));
}
let urlResults = this.urlResultCache.get(cacheKey) ?? [];
if (this.urlResultCache.has(cacheKey)) {
return urlResults.map(urlResult => ({ ...urlResult, ttl: this.urlResultCache.getRemainingTTL(cacheKey) }));
}
const lazyUrlResults = await this.lazyUrlResultCache.get<UrlResult[]>(normalizedUrl.href) ?? [];
this.logger.info(`Extract stream URL using ${extractor.id} extractor from ${url}`, ctx);
/* 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/`);
urlResults = await extractor.extract(ctx, normalizedUrl, countryCode, title);
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
if (!urlResults.some(urlResult => urlResult.error) && extractor.ttl) {
this.urlResultCache.set(cacheKey, urlResults, { ttl: extractor.ttl });
}
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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fastream } from './Fastream';
@ -11,10 +12,6 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
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();
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'), CountryCode.es)).toMatchSnapshot();
});
});

View file

@ -1,16 +1,25 @@
import bytes from 'bytes';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorStreamUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Fastream extends Extractor {
public readonly id = 'fastream';
public readonly label = 'Fastream';
public readonly label = 'Fastream (via MediaFlow Proxy)';
public override readonly ttl = 0;
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/fastream/) && supportsMediaFlowProxy(ctx);
}
@ -19,17 +28,11 @@ export class Fastream extends Extractor {
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 };
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url);
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 html = await this.fetcher.text(ctx, downloadUrl);
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
const titleMatch = html.match(/>Download (.*?)</) as string[];
@ -38,8 +41,11 @@ export class Fastream extends Extractor {
{
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Fsst } from './Fsst';
@ -10,7 +11,15 @@ const extractorRegistry = new ExtractorRegistry(logger, [new Fsst(new FetcherMoc
const ctx = createTestContext();
describe('Fsst', () => {
test('Wake up Dead Man', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/948429/'))).toMatchSnapshot();
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/900576/'), CountryCode.de)).toMatchSnapshot();
});
test('Dead City', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'), CountryCode.de)).toMatchSnapshot();
});
test('How to Train Your Dragon', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'), CountryCode.de)).toMatchSnapshot();
});
});

View file

@ -1,5 +1,6 @@
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class Fsst extends Extractor {
@ -7,35 +8,44 @@ export class Fsst extends Extractor {
public readonly label = 'Fsst';
public override readonly ttl: number = 10800000; // 3h
public override readonly ttl = 0;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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 });
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
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;
return (filesMatch[1] as string).split(',').map((fileString, index) => {
const heightAndUrlMatch = fileString.match(/\[?([\d]*)p?]?(.*)/) as string[];
const fileHref = heightAndUrlMatch[2] as string;
const heightFromFileHrefMatch = fileHref.match(/([\d]+)p/) 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,
},
}];
return {
url: new URL(fileHref),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}_${index}`,
meta: {
countryCodes: [countryCode],
height: parseInt(heightAndUrlMatch[1] || heightFromFileHrefMatch[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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { KinoGer } from './KinoGer';
@ -11,18 +12,10 @@ const ctx = createTestContext();
describe('KinoGer', () => {
test('Blood & Sinners', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#ge5fhb'), CountryCode.de)).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();
expect(await extractorRegistry.handle(ctx, new URL('https://kinoger.re/#x6tsx9'), CountryCode.de)).toMatchSnapshot();
});
});

View file

@ -1,6 +1,6 @@
import crypto from 'crypto';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { guessHeightFromPlaylist } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */
@ -9,7 +9,13 @@ export class KinoGer extends Extractor {
public readonly label = 'KinoGer';
public override readonly ttl: number = 21600000; // 6h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return [
@ -17,19 +23,11 @@ export class KinoGer extends Extractor {
'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',
@ -43,35 +41,34 @@ export class KinoGer extends Extractor {
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',
};
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const hexData = await this.fetcher.text(ctx, url, { headers: { '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 encrypted = Buffer.from(hexData, '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 { cf, title } = JSON.parse(decrypted) as { cf: string; title: string };
const m3u8Url = new URL(source);
const m3u8Url = new URL(cf);
return [
{
url: m3u8Url,
url: new URL(cf),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.origin } }),
title,
},
requestHeaders: headers,
requestHeaders: {
Referer: url.origin,
},
},
];
};

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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Mixdrop } from './Mixdrop';
@ -11,10 +12,14 @@ const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test
describe('Mixdrop', () => {
test('mixdrop.my /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/e/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot();
});
test('mixdrop.my /f/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.my/f/knq0kj8waq44l8'), CountryCode.de)).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://mixdrop.ag/e/123456789'), CountryCode.de)).toMatchSnapshot();
});
});

View file

@ -1,24 +1,34 @@
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 { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Mixdrop extends Extractor {
public readonly id = 'mixdrop';
public readonly label = 'Mixdrop';
public readonly label = 'Mixdrop (via MediaFlow Proxy)';
public override readonly ttl = 0;
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
return null !== url.host.match(/mixdrop/) && 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[]> {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
const html = await this.fetcher.text(ctx, fileUrl);
@ -26,7 +36,7 @@ export class Mixdrop extends Extractor {
throw new NotFoundError();
}
const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/) as string[];
const sizeMatch = html.match(/([\d.,]+ ?[GM]B)/);
const $ = cheerio.load(html);
const title = $('.title b').text().trim();
@ -35,10 +45,15 @@ export class Mixdrop extends Extractor {
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
countryCodes: [countryCode],
title,
...(sizeMatch && {
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
}),
},
},
];

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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SaveFiles } from './SaveFiles';
@ -11,26 +12,22 @@ const ctx = createTestContext();
describe('SafeFiles', () => {
test('savefiles', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
});
test('savefiles /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'), CountryCode.en)).toMatchSnapshot();
});
test('savefiles /d/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'), CountryCode.en)).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();
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'), CountryCode.en)).toMatchSnapshot();
});
test('streamhls /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'), CountryCode.en)).toMatchSnapshot();
});
});

View file

@ -1,6 +1,7 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class SaveFiles extends Extractor {
@ -8,7 +9,13 @@ export class SaveFiles extends Extractor {
public readonly label = 'SaveFiles';
public override readonly ttl: number = 21600000; // 6h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/savefiles|streamhls/);
@ -18,12 +25,10 @@ export class SaveFiles extends Extractor {
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 };
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const html = await this.fetcher.text(ctx, url, { headers });
if (/file was locked|file was deleted/i.test(html)) {
if (/File was locked by administrator/.test(html)) {
throw new NotFoundError();
}
@ -37,8 +42,11 @@ export class SaveFiles extends Extractor {
{
url: new URL(fileMatch[1] as string),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
title,
height: parseInt(sizeMatch[1] as string),
},

View file

@ -0,0 +1,25 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Soaper } from './Soaper';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new Soaper(new FetcherMock(`${__dirname}/__fixtures__/Soaper`))]);
const ctx = createTestContext();
describe('Soaper', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
});
test('last of us s2e3', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'), CountryCode.de, 'The Last of Us 2x3')).toMatchSnapshot();
});
});

67
src/extractor/Soaper.ts Normal file
View file

@ -0,0 +1,67 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
interface SoaperInfoResponsePartial {
val: string;
val_bak: string;
}
export class Soaper extends Extractor {
public readonly id = 'soaper';
public readonly label = 'Soaper';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode, title: string | undefined): Promise<UrlResult[]> {
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
const form = new URLSearchParams();
form.append('pass', movieOrEpisodeId);
form.append('param', '');
form.append('extra', '');
form.append('e2', '');
form.append('server', '0');
const response = await this.fetcher.textPost(
ctx,
new URL(url.pathname.includes('episode') ? '/home/index/GetEInfoAjax' : '/home/index/GetMInfoAjax', url.origin),
form.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': url.origin,
},
},
);
const jsonResponse = JSON.parse(response) as SoaperInfoResponsePartial;
const m3u8Url = new URL(jsonResponse['val'], url.origin);
return [
{
url: m3u8Url,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
title: `${title}`,
},
},
];
};
}

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { StreamEmbed } from './StreamEmbed';
@ -11,10 +12,6 @@ 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();
expect(await extractorRegistry.handle(ctx, new URL('https://watch.gxplayer.xyz/watch?v=MEKI92PU'), CountryCode.de)).toMatchSnapshot();
});
});

View file

@ -1,5 +1,5 @@
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class StreamEmbed extends Extractor {
@ -7,20 +7,20 @@ export class StreamEmbed extends Extractor {
public readonly label = 'StreamEmbed';
public override readonly ttl: number = 259200000; // 3d
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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();
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
@ -28,8 +28,11 @@ export class StreamEmbed extends Extractor {
{
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
height: parseInt(JSON.parse(video.quality)[0]),
title: decodeURIComponent(video.title),
},

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Streamtape } from './Streamtape';
@ -7,10 +8,10 @@ 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' });
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
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();
expect(await extractorRegistry.handle(ctx, new URL('https://streamtape.com/e/84Kb3mkoYAiokmW'), CountryCode.es)).toMatchSnapshot();
});
});

View file

@ -1,68 +1,47 @@
import bytes from 'bytes';
import * as cheerio from 'cheerio';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorRedirectUrl,
Fetcher,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
export class Streamtape extends Extractor {
public readonly id = 'streamtape';
public readonly label = 'Streamtape';
public readonly label = 'Streamtape (via MediaFlow Proxy)';
public override viaMediaFlowProxy = true;
public override readonly ttl = 0;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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);
return null !== url.host.match(/streamtape/) && 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[];
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const $ = cheerio.load(html);
const title = $('meta[name="og:title"]').attr('content') as string;
return [
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers),
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
title,
bytes: bytes.parse(sizeMatch[1] as string) as number,
},
},
];

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { SuperVideo } from './SuperVideo';
@ -11,22 +12,22 @@ const ctx = createTestContext();
describe('SuperVideo', () => {
test('supervideo.cc /e/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/q7i0sw1oytw3'), CountryCode.de)).toMatchSnapshot();
});
test('supervideo.tv /embed-/', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.tv/embed-1p0m1fi9mok8.html'), CountryCode.it)).toMatchSnapshot();
});
test('embed only', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/e/bj6szat1pval'), CountryCode.it)).toMatchSnapshot();
});
test('deleted or expired file', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/ndf5shmy9lpt'), CountryCode.it)).toMatchSnapshot();
});
test('processing video', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://supervideo.cc/3h1qqoqtldo8'), CountryCode.it)).toMatchSnapshot();
});
});

View file

@ -1,8 +1,8 @@
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 { Context, CountryCode, Format, UrlResult } from '../types';
import { extractUrlFromPacked, Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class SuperVideo extends Extractor {
@ -10,52 +10,53 @@ export class SuperVideo extends Extractor {
public readonly label = 'SuperVideo';
public override readonly ttl: number = 10800000; // 3h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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-', '/'));
return new URL(url.href.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 });
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
if (html.includes('This video can be watched as embed only')) {
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), meta);
return await this.extractInternal(ctx, new URL(`/e${url.pathname}`, url.origin), countryCode);
}
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,
url: extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]),
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
title,
...(size && { bytes: size }),
...(height && { height }),
...(heightAndSizeMatch && {
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
height: parseInt(heightAndSizeMatch[1] as string),
}),
},
requestHeaders: playlistHeaders,
},
];
};

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { Uqload } from './Uqload';
@ -7,14 +8,14 @@ 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' });
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
describe('Uqload', () => {
test('uqload.net /embed-', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/embed-z0xbr87oz637.html'), CountryCode.fr)).toMatchSnapshot();
});
test('file not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.cx/2ua47fdfkjgd.html'))).toMatchSnapshot();
test('uqload.net /', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://uqload.net/z0xbr87oz637.html'), CountryCode.fr)).toMatchSnapshot();
});
});

View file

@ -1,16 +1,25 @@
import * as cheerio from 'cheerio';
import { NotFoundError } from '../error';
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { buildMediaFlowProxyExtractorRedirectUrl, Fetcher, supportsMediaFlowProxy } from '../utils';
import { Extractor } from './Extractor';
export class Uqload extends Extractor {
public readonly id = 'uqload';
public readonly label = 'Uqload';
public readonly label = 'Uqload (via MediaFlow Proxy)';
public override readonly ttl = 0;
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/uqload/) && supportsMediaFlowProxy(ctx);
}
@ -19,13 +28,9 @@ export class Uqload extends Extractor {
return new URL(url.href.replace('/embed-', '/'));
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
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);
@ -35,8 +40,11 @@ export class Uqload extends Extractor {
{
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url),
format: Format.mp4,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
title,
...(heightMatch && {
height: parseInt(heightMatch[1] as string),

View file

@ -1,20 +1,42 @@
import { MockAgent, setGlobalDispatcher } from 'undici';
import winston from 'winston';
import { createTestContext } from '../test';
import { FetcherMock } from '../utils';
import { CountryCode } from '../types';
import { Fetcher, 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 extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['xyz'])]);
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();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'), CountryCode.en, 'Full Metal Jacket (1987)')).toMatchSnapshot();
});
test('Black Mirror', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/tv/tt2085059/4-2'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/tv/tt2085059/4/2'), CountryCode.en, 'Black Mirror 4x2')).toMatchSnapshot();
});
test('not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'), CountryCode.en, 'Titan: The OceanGate Disaster')).toMatchSnapshot();
});
test('rate limit issues are retried and fail if no tlds are left', async () => {
const mockAgent = new MockAgent({ enableCallHistory: true });
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
mockAgent.get('https://vidsrc.xyz')
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
mockAgent.get('https://vidsrc.net')
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), CountryCode.en, 'Dexter: Resurrection 1x1')).toMatchSnapshot();
});
});

View file

@ -1,6 +1,7 @@
import * as cheerio from 'cheerio';
import { BlockedError, TooManyRequestsError } from '../error';
import { Context, Format, InternalUrlResult, Meta, NonEmptyArray } from '../types';
import slugify from 'slugify';
import { NotFoundError, TooManyRequestsError } from '../error';
import { Context, CountryCode, Format, NonEmptyArray, UrlResult } from '../types';
import { Fetcher, guessHeightFromPlaylist } from '../utils';
import { Extractor } from './Extractor';
@ -9,49 +10,45 @@ export class VidSrc extends Extractor {
public readonly label = 'VidSrc';
public override readonly ttl: number = 10800000; // 3h
private readonly fetcher: Fetcher;
private readonly tlds: NonEmptyArray<string>;
private readonly domains: NonEmptyArray<string>;
public constructor(fetcher: Fetcher, tlds: NonEmptyArray<string>) {
super();
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
super(fetcher);
this.domains = domains;
this.fetcher = fetcher;
this.tlds = tlds;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vidsrc|vsrc/);
return null !== url.host.match(/vidsrc/);
}
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]);
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
return this.extractUsingRandomTld(ctx, url, countryCode, [...this.tlds]);
};
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];
private async extractUsingRandomTld(ctx: Context, url: URL, countryCode: CountryCode, tlds: string[]): Promise<UrlResult[]> {
const tldIndex = Math.floor(Math.random() * tlds.length);
const [tld] = tlds.splice(tldIndex, 1) as [string];
const newUrl = new URL(url);
newUrl.hostname = domain;
const hostnameParts = newUrl.hostname.split('.');
hostnameParts[hostnameParts.length - 1] = tld;
newUrl.hostname = hostnameParts.join('.');
let html: string;
try {
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
html = await this.fetcher.text(ctx, newUrl);
} catch (error) {
/* istanbul ignore next */
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
return this.extractUsingRandomDomain(ctx, url, meta, domains);
if (error instanceof TooManyRequestsError && tlds.length) {
return this.extractUsingRandomTld(ctx, url, countryCode, tlds);
}
/* istanbul ignore next */
throw error;
}
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
const $ = cheerio.load(html);
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
const title = $('title').text().trim();
@ -62,21 +59,26 @@ export class VidSrc extends Extractor {
.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 iframeHtml = await this.fetcher.text(ctx, new URL(`/rcp/${dataHash}`, iframeUrl.origin), { headers: { Referer: iframeUrl.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));
const playerHtml = await this.fetcher.text(ctx, new URL(srcMatch[1] as string, iframeUrl.origin), { headers: { Referer: iframeUrl.origin } });
const fileMatch = playerHtml.match(`file:\\s?'(.*)'`);
if (!fileMatch) {
throw new NotFoundError();
}
const m3u8Url = new URL(fileMatch[1] as string);
return {
url: m3u8Url,
format: Format.hls,
label: serverName,
label: `${this.label} (${serverName})`,
sourceId: `${this.id}_${slugify(serverName)}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }),
countryCodes: [countryCode],
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url),
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,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { VixSrc } from './VixSrc';
@ -7,24 +8,20 @@ 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();
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg', de: 'on', en: 'on', it: 'on' });
describe('VixSrc', () => {
test('Full Metal Jacket', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/600'), CountryCode.it, 'Full Metal Jacket (1987)')).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();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
});
test('Black Mirror is excluded if no matching language was found', async () => {
const ctx = createTestContext({ de: 'on' });
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg', de: 'on' });
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/3'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'), CountryCode.it, 'Black Mirror 4x2')).toMatchSnapshot();
});
});

View file

@ -1,34 +1,44 @@
import { Context, CountryCode, Format, InternalUrlResult, Meta } from '../types';
import { CustomRequestConfig, guessHeightFromPlaylist, hasMultiEnabled, iso639FromCountryCode } from '../utils';
import bytes from 'bytes';
import { Context, CountryCode, Format, UrlResult } from '../types';
import {
buildMediaFlowProxyExtractorStreamUrl,
Fetcher,
hasMultiEnabled,
iso639FromCountryCode,
supportsMediaFlowProxy,
} from '../utils';
import { Extractor } from './Extractor';
export class VixSrc extends Extractor {
public readonly id = 'vixsrc';
public readonly label = 'VixSrc';
public readonly label = 'VixSrc (via MediaFlow Proxy)';
public override readonly ttl: number = 21600000; // 6h
public override readonly ttl = 0;
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/vixsrc/);
public override viaMediaFlowProxy = true;
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
const headers = { Referer: url.href };
public supports(ctx: Context, url: URL): boolean {
return null !== url.host.match(/vixsrc/) && supportsMediaFlowProxy(ctx);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
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 filenameMatch = html.match(/"filename":"(.*?)"/);
const sizeMatch = html.match(/"size":(\d+)/);
const qualityMatch = html.match(/"quality":(\d+)/);
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 }))];
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'VixCloud', url);
const countryCodes = await this.determineCountryCodesFromPlaylist(ctx, playlistUrl);
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
return [];
@ -38,19 +48,23 @@ export class VixSrc extends Extractor {
{
url: playlistUrl,
format: Format.hls,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes,
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
...(filenameMatch && { title: filenameMatch[1] }),
...(sizeMatch && { bytes: bytes.parse(`${sizeMatch[1]} kb`) as number }),
...(qualityMatch && { height: parseInt(qualityMatch[1] as string) }),
},
},
];
};
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestConfig): Promise<CountryCode[]> {
const playlist = await this.fetcher.text(ctx, playlistUrl, init);
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL): Promise<CountryCode[]> {
const playlist = await this.fetcher.text(ctx, playlistUrl);
const countryCodes: CountryCode[] = [];
const countryCodes: CountryCode[] = [CountryCode.it];
(Object.keys(CountryCode) as CountryCode[]).forEach((countryCode) => {
const iso639 = iso639FromCountryCode(countryCode);

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

@ -0,0 +1,29 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { XPrime } from './XPrime';
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
const extractorRegistry = new ExtractorRegistry(logger, [new XPrime(new FetcherMock(`${__dirname}/__fixtures__/XPrime`))]);
const ctx = createTestContext();
describe('XPrime', () => {
test('unknown backend is ignored', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/unknown?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
});
test('primebox Superman', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Superman&year=2025'), CountryCode.en, 'Superman (2025)')).toMatchSnapshot();
});
test('primebox Alien: Earth', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Alien%3A%20Earth&year=2025&season=1&episode=1'), CountryCode.en, 'Alien: Earth 1x1')).toMatchSnapshot();
});
test('primebox not found', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://backend.xprime.tv/primebox?name=Jurassic%20World%20Rebirth&year=2025'), CountryCode.en, 'Jurassic World Rebirth (2025)')).toMatchSnapshot();
});
});

68
src/extractor/XPrime.ts Normal file
View file

@ -0,0 +1,68 @@
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
interface XPrimePrimeboxResponsePartial {
streams: Record<string, string>;
series_info?: {
episode: number;
season: number;
title: string;
year: string;
};
title?: string;
}
export class XPrime extends Extractor {
public readonly id = 'xprime';
public readonly label = 'XPrime';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
public supports(_ctx: Context, url: URL): boolean {
return null !== url.host.match(/xprime/);
}
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const urlResults: UrlResult[] = [];
const referer = url.protocol + '//' + url.hostname.split('.').slice(-2).join('.'); // Strip subdomains
if (url.href.includes('primebox')) {
const jsonResponse = JSON.parse(await this.fetcher.text(ctx, url)) as XPrimePrimeboxResponsePartial;
for (const [resolution, stream] of Object.entries(jsonResponse.streams)) {
const url = new URL(stream);
const title = jsonResponse.series_info
? `${jsonResponse.series_info.title} ${jsonResponse.series_info.season}x${jsonResponse.series_info.episode} (${jsonResponse.series_info.year})`
: jsonResponse.title;
urlResults.push({
url,
format: Format.mp4,
label: `${this.label} Primebox`,
sourceId: `${this.id}_${countryCode}_primebox`,
ttl: this.ttl,
meta: {
countryCodes: [countryCode],
bytes: parseInt((await this.fetcher.head(ctx, url, { headers: { Referer: referer }, minCacheTtl: this.ttl }))['content-length'] as string),
height: parseInt(resolution),
title,
},
requestHeaders: {
Referer: referer,
},
});
}
}
return urlResults;
};
}

View file

@ -1,5 +1,6 @@
import winston from 'winston';
import { createTestContext } from '../test';
import { CountryCode } from '../types';
import { FetcherMock } from '../utils';
import { ExtractorRegistry } from './ExtractorRegistry';
import { YouTube } from './YouTube';
@ -11,10 +12,10 @@ const ctx = createTestContext();
describe('YouTube', () => {
test('Solaris', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'), CountryCode.en)).toMatchSnapshot();
});
test('unsupported format', async () => {
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'))).toMatchSnapshot();
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'), CountryCode.en)).toMatchSnapshot();
});
});

View file

@ -1,4 +1,5 @@
import { Context, Format, InternalUrlResult, Meta } from '../types';
import { Context, CountryCode, Format, UrlResult } from '../types';
import { Fetcher } from '../utils';
import { Extractor } from './Extractor';
export class YouTube extends Extractor {
@ -6,16 +7,20 @@ export class YouTube extends Extractor {
public readonly label = 'YouTube';
public override readonly ttl: number = 21600000; // 6h
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
super();
this.fetcher = fetcher;
}
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 });
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
const html = await this.fetcher.text(ctx, url);
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
@ -24,8 +29,11 @@ export class YouTube extends Extractor {
url,
format: Format.unknown,
ytId: url.searchParams.get('v') as string,
label: this.label,
sourceId: `${this.id}_${countryCode}`,
ttl: this.ttl,
meta: {
...meta,
countryCodes: [countryCode],
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

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,230 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fastream</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">
<link rel="apple-touch-icon" href="https://fastream.to/images2/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="https://fastream.to/images2/favicon/site.webmanifest" crossorigin="use-credentials">
<link rel="icon" href="https://fastream.to/images2/favicon/favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap" rel="stylesheet">
<link href="https://fastream.to/css2/bootstrap.css?" rel="stylesheet">
<link href="https://fastream.to/css2/style.css?" rel="stylesheet">
<script src="https://fastream.to/js2/jquery.min.js?"></script>
<script src="https://fastream.to/js2/bootstrap.bundle.min.js?"></script>
<script src="https://fastream.to/js2/app.js?aс1"></script>
<script src="https://fastream.to/js2/xupload.js?aс1"></script>
</head>
<body class="">
<header class="header">
<div class="container">
<div class="row header-row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto d-lg-none">
<div class="dropdown">
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="16" viewBox="0 0 22 16">
<path d="M0 0h22v2H0V0zm0 7h22v2H0V7zm0 7h22v2H0v-2z"></path>
</svg>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<a href="/premium" class="dropdown-item">Premium</a>
<a href="/make_money" class="dropdown-item">Make Money</a>
<a href="/check_files" class="dropdown-item">Link Checker</a>
<a href="/api.html" class="dropdown-item">API</a>
<a href="/tos" class="dropdown-item">Terms of service</a>
<a href="/contact" class="dropdown-item">Contact Us</a>
<a href="/faq" class="dropdown-item">FAQ</a>
</ul>
</div>
</div>
<div class="col-auto me-lg-4">
<a href="/" class="logo">
<img src="https://fastream.to/images2/logo.svg?" alt="">
</a>
</div>
<div class="col-auto d-none d-lg-block">
<nav class="nav">
<a class="nav-link link-body-emphasis" href="/premium">Premium</a>
<a class="nav-link link-body-emphasis" href="/make_money">Make Money</a>
</nav>
</div>
</div>
</div>
<div class="col-auto">
<div class="dropdown header-lang">
<button type="button" class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<img src="https://fastream.to/images2/flags/english.svg" alt="">
</button>
<div class="dropdown-menu">
<a href="/?op=change_lang&lang=English" class="dropdown-item"><img src="https://fastream.to/images2/flags/english.svg" alt="">English</a>
<a href="/?op=change_lang&lang=Russian" class="dropdown-item"><img src="https://fastream.to/images2/flags/russian.svg" alt="">Russian</a>
</div>
</div>
</div>
<div class="col-auto d-none d-lg-block">
<a href="/login.html" class="btn me-2 login-btn link-body-emphasis">Login</a>
<a href="/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
</div>
<div class="col-auto d-lg-none">
<div class="dropdown">
<button class="btn icon-btn" data-bs-toggle="dropdown" data-bs-offset="0, 16">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="22" height="22" viewBox="0 0 22 22">
<path d="M11 22C4.9 22 0 17.1 0 11S4.9 0 11 0s11 4.9 11 11-4.9 11-11 11zm0-20c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9z"></path>
<path d="M11 15.3c-3 0-5.4-2.4-5.4-5.4S8 4.4 11 4.4s5.4 2.4 5.4 5.4-2.4 5.5-5.4 5.5zm0-8.9c-1.9 0-3.4 1.5-3.4 3.4s1.5 3.4 3.4 3.4 3.4-1.5 3.4-3.4-1.5-3.4-3.4-3.4z"></path>
<path d="M4.9 19.8c-.2 0-.3 0-.5-.1-.5-.3-.6-.9-.4-1.4 1.5-2.5 3.9-3.8 7-3.8s5.5 1.3 7 3.8c.3.5.1 1.1-.3 1.4-.5.3-1.1.1-1.4-.3-1.1-1.9-2.9-2.8-5.2-2.8-2.4 0-4.1.9-5.3 2.8-.2.2-.6.4-.9.4z"></path>
</svg>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="/login.html">Login</a>
<a class="dropdown-item" href="/?op=registration">Sign Up</a>
<a class="dropdown-item" href="/?op=forgot_pass">Forgot your password?</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<div class="container">
<div class="alert alert-danger" role="alert">
No such file=a9rp1d1pqom7
</div>
</div>
</main>
<footer class="footer">
<div class="container">
<div class="footer-main">
<div class="row justify-content-center justify-content-lg-start">
<div class="col-12 col-lg-4 col-xl-3 text-center text-lg-start">
<div class="mb-4">
<a href="https://fastream.to/" class="logo">
<img src="https://fastream.to/images2/logo-w.svg?" alt="">
</a>
</div>
<div class="mb-2 text-uppercase"><!--Share us: --></div>
<div class="mb-3">
<!--span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/fb.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/red.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/wha.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/x.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/msg.svg" alt=""></a>
</span>
<span class="me-1">
<a href="#"><img src="https://fastream.to/images2/social/in.svg" alt=""></a>
</span-->
</div>
</div>
<div class="col-lg-8 col-xl-7 d-none d-lg-block">
<div class="row">
<div class="col">
<div class="text-uppercase text-dark large">Info</div>
<div class="nav flex-column">
<a href="https://fastream.to/" class="nav-link">Home</a>
<a href="/api.html" class="nav-link">API</a>
<a href="/tos" class="nav-link">Terms of service</a>
</div>
</div>
<div class="col">
<div class="text-uppercase text-dark large">Features</div>
<div class="nav flex-column">
<a href="/premium" class="nav-link">Premium</a>
<a href="/make_money" class="nav-link">Make Money</a>
<a href="/check_files" class="nav-link">Link Checker</a>
</div>
</div>
<div class="col">
<div class="text-uppercase text-dark large">Support</div>
<div class="nav flex-column">
<a href="/contact" class="nav-link">Contact Us</a>
<a href="/faq" class="nav-link">FAQ</a>
</div>
</div>
</div>
</div>
<div class="col-xl-auto text-nowrap text-end d-none d-xl-block">
<a href="https://fastream.to/login.html" class="btn btn-light login-btn me-2">Login</a>
<a href="https://fastream.to/?op=registration" class="btn btn-primary reg-btn">Sign Up</a>
</div>
</div>
</div>
</div>
<div class="footer-bottom small">
<div class="container">
<div class="row align-items-center justify-content-between">
<div class="col-auto">
<div class="text-muted">&copy; 2024 Fastream</div>
</div>
<div class="col-auto">
</div>
</div>
</div>
</div>
</footer>
<div style ="display:none"><script id="_wau1xg">var _wau = _wau || []; _wau.push(["classic", "y7jk20u14x", "1xg"]);</script><script async src="//waust.at/c.js"></script></div>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'993a4e2d4ac5d2ab',t:'MTc2MTMxNzYyMw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>

View file

@ -1,145 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Watch 300938 570f5a24 c281 484d 93fa 734dabbec813 jkgg 2651705 vidhide mp4
</title>
<META NAME="description" CONTENT="">
<META NAME="keywords" CONTENT="">
<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">
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/static/images/favicon/manifest.json">
<link rel="icon" href="/static/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="/static/css/style.css?" rel="stylesheet">
<script src="/static/js/jquery-3.2.1.min.js?"></script>
<script src="/static/js/bootstrap.bundle.min.js?"></script>
<script src="/static/js/app.js?"></script>
<script src="/static/js/xupload.js?"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-48ZJD1VPGZ');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(93381808, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto" style="margin: 0 auto;">
<a href="/" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="block">
<div class="mb-3">
The file was deleted by administration because it didn&#39;t comply with our Terms of Use:
</div>
<h4>
300938--570f5a24-c281-484d-93fa-734dabbec813--jkgg--2651705-vidhide.mp4
</h4>
<br>
Reason for deletion: <br>
<b class="text-danger">
DMCA request
</b>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-lg-4 col-xs-12">
<div class="mb-3 text-center">
<a href="#" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
<div class="col-lg-8 col-xs-12" style="position: relative">
<div class="mb-4 text-center">
<p class="text-muted ">
</p>
</div>
</div>
</div>
</div>
</footer>
<style>
@media (min-width:481px) {
.text-muted {}
}
@media (min-width:961px) {
.text-muted {
position: absolute;
right: 0;
top: 15px
}
}
</style>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,161 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Watch
</title>
<META NAME="description" CONTENT="">
<META NAME="keywords" CONTENT="">
<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">
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/static/images/favicon/manifest.json">
<link rel="icon" href="/static/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="/static/css/style.css?" rel="stylesheet">
<script src="/static/js/jquery-3.2.1.min.js?"></script>
<script src="/static/js/bootstrap.bundle.min.js?"></script>
<script src="/static/js/app.js?"></script>
<script src="/static/js/xupload.js?"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-48ZJD1VPGZ');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(93381808, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto" style="margin: 0 auto;">
<a href="/" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="block">
<div class="text-center">
<div class="mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill-rule="evenodd" fill="#DC3545" d="M32 64C14.326 64 0 49.674 0 32S14.326 0 32 0c17.666.019 31.981 14.334 32 32 0 17.674-14.326 32-32 32Zm0-61C15.984 3 3 15.984 3 32s12.984 29 29 29c16.008-.019 28.981-12.992 29-29C61 15.984 48.016 3 32 3Zm14.065 43.125a1.504 1.504 0 0 1-2.125 0l-11.94-12L20.065 46.12A1.502 1.502 0 0 1 17.941 44L29.88 32 17.941 20a1.503 1.503 0 0 1 2.124-2.125l11.935 12 11.94-11.98a1.502 1.502 0 0 1 2.12 0A1.5 1.5 0 0 1 46.065 20L34.12 32l11.945 12a1.504 1.504 0 0 1 0 2.125Z"/></svg>
</div>
</div>
<h3 class="text-center mb-3">
File Not Found
</h3>
<div>
<p class="text-center">The file you were looking for could not be found, sorry for any inconvenience.</p>
</div>
</div>
<div class="text-muted">
<p class="text-center"><b>Possible causes of this error could be:</b></p>
<div class="row justify-content-center">
<div class="col-auto">
<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">
<div class="col-lg-4 col-xs-12">
<div class="mb-3 text-center">
<a href="#" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
<div class="col-lg-8 col-xs-12" style="position: relative">
<div class="mb-4 text-center">
<p class="text-muted ">
</p>
</div>
</div>
</div>
</div>
</footer>
<style>
@media (min-width:481px) {
.text-muted {}
}
@media (min-width:961px) {
.text-muted {
position: absolute;
right: 0;
top: 15px
}
}
</style>
</body>
</html>

View file

@ -1 +0,0 @@
{"destination_url":"https://RNzT2t4XVKU08.acek-cdn.com/hls2/01/05594/tyn45apubte2_n/master.m3u8?t=8y6hK40boUynXnkyW5s63kFaWpt6_A5UcG5LBUX3j_4&s=1756131487&e=129600&f=27974684&srv=F6vE2P5Wjncn&i=0.4&sp=500&p1=F6vE2P5Wjncn&p2=F6vE2P5Wjncn&asn=31898","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://filelions.to/v/tyn45apubte2"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}

View file

@ -1 +0,0 @@
{"destination_url":"https://6dkDxt1udLQhbho.ovaltinecdn.com/hls2/01/01552/tah5znapz3e5_n/master.m3u8?t=MnSHt6lZx3bdKvTdoU1JsTLWp56dlRDLwze5Prnefew&s=1756131487&e=129600&f=7760835&srv=GOweneve6B0I&i=0.4&sp=500&p1=GOweneve6B0I&p2=GOweneve6B0I&asn=31898","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://kinoger.com"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}

View file

@ -1,138 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>
Watch Versailles S01E01 German AC3 1080p BDRip x265 FuN
</title>
<META NAME="description" CONTENT="">
<META NAME="keywords" CONTENT="">
<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">
<link rel="apple-touch-icon" href="/static/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/static/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="/static/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/static/images/favicon/manifest.json">
<link rel="icon" href="/static/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="/static/css/style.css?" rel="stylesheet">
<script src="/static/js/jquery-3.2.1.min.js?"></script>
<script src="/static/js/bootstrap.bundle.min.js?"></script>
<script src="/static/js/app.js?"></script>
<script src="/static/js/xupload.js?"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-48ZJD1VPGZ"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-48ZJD1VPGZ');
</script>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(93381808, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true
});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/93381808" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</head>
<body>
<header class="header">
<div class="container">
<div class="row align-items-center flex-nowrap">
<div class="col-auto flex-grow-1 flex-shrink-1">
<div class="row align-items-center">
<div class="col-auto" style="margin: 0 auto;">
<a href="/" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<section class="section">
<div class="container">
<div class="alert alert-danger">
This video can be watched as embed only
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<div class="row">
<div class="col-lg-4 col-xs-12">
<div class="mb-3 text-center">
<a href="#" class="logo">
<img src="/static/images/logo.svg" alt="">
</a>
</div>
</div>
<div class="col-lg-8 col-xs-12" style="position: relative">
<div class="mb-4 text-center">
<p class="text-muted ">
</p>
</div>
</div>
</div>
</div>
</footer>
<style>
@media (min-width:481px) {
.text-muted {}
}
@media (min-width:961px) {
.text-muted {
position: absolute;
right: 0;
top: 15px
}
}
</style>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
(function () {
// Get all script tags in the <head>
const scripts = document.head.querySelectorAll('script');
// Iterate through the scripts to find the one injecting the Proxy
scripts.forEach((script) => {
if (script.innerHTML.includes('window.open=new Proxy(window.open')) {
console.log('Detected and removing injected script:', script);
script.remove(); // Remove the script from the <head>
}
});
// Restore the original window.open functionality (optional)
const originalOpen = Function.prototype.apply.bind(window.open);
window.open = function (...args) {
console.log('Restored window.open called:', args);
return originalOpen(this, args);
};
console.log('Injected script removed and window.open restored.');
})();
</script>
<title>Not Found</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/404.css?v=2">
<link rel="apple-touch-icon" href="https://filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon.ico">
</head>
<body>
</body>
<script src="/ad.js" type="text/javascript"></script>
<div id="adb" class="ad doubleclick skyscraper" style="height:1px;width:1px;position:absolute;left:-999px;top:-999px;">ad</div>
<script>
window.fmn7i8zodwjqr9 = { token: "bed5d8dfa2e30a9a04", adblock: false, blockadblock: true, fileid: "n7i8zodwjqr9", ampallow: false };
</script>
<div class="container">
<div class="error e404">
<div class="error-footer">
<h1>Page not found</h1>
<a href="/" class="button">Return home</a>
</div>
</div>
</div>
</html>

View file

@ -1,227 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Watch tt0307453</title>
<meta name="description" content="">
<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">
<link rel="apple-touch-icon" href="//filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="//filemoon.to/assets/css/front.css?v=51">
<META NAME="description" CONTENT="Watch video tt0307453">
<META NAME="keywords" CONTENT="tt0307453">
<meta name="robots" content="noindex, nofollow">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body class="is-home d-flex flex-column justify-content-between ">
<header class="header py-3 py-lg-4">
<div class="container">
<div class="row align-items-center justify-content-between justify-content-lg-start">
<div class="col-auto d-lg-none">
<button class="btn text-muted ssm-toggle-nav px-1" type="button">
<i class="icon icon-menu icon-size-22 text-muted"></i>
</button>
</div>
<div class="col-auto">
<a href="/" class="logo d-block"><img src="//filemoon.to/assets/images/logo.svg" alt=""></a>
</div>
<div class="col-auto d-none d-lg-block flex-lg-grow-1">
<nav class="nav ml-lg-3">
<a class="nav-link" href="/premium">Premium</a>
<a class="nav-link" href="/affiliate">Make Money</a>
</nav>
</div>
<div class="col-auto">
<div class="d-none d-lg-flex">
<a href="/login" class="btn btn-dark sign-btn rounded-pill mr-2">Log In</a>
<a href="/register" class="btn btn-success sign-btn rounded-pill">Sign Up</a>
</div>
<div class="d-lg-none">
<button class="btn text-muted usrmobile-toggle-nav px-1" type="button">
<i class="icon icon-user icon-size-18 text-muted"></i>
</button>
</div>
</div>
</div>
</div>
</header>
<script src="https://filemoon.to/js/jquery.cookie.js"></script>
<script>
$.cookie('file_id', '48721931', { expires: 10 });
$.cookie('aff', '89667', { expires: 10 });
$.cookie('ref_url', 'https://filemoon.to/d/ugj7f1kq94p1', { expires: 10 });
</script>
<script type="text/javascript" src='/js/ls.js'></script>
<main class="flex-grow-1 a-bean">
<div class="container mt-5">
<div class="mb-4 mb-lg-5 sm-bg px-4 py-5 p-lg-0 mx-lg-5 rounded-lg">
<div class="row justify-content-between align-items-center text-muted mb-4 mb-lg-5">
<div class="col-auto mb-2">
<h3 class="text-light mb-0 font-weight-bold">tt0307453</h3>
</div>
<div class="col-auto mb-2"> Uploaded: <span class="text-success">2025-08-24 00:12:36</span>
</div>
</div>
<div class="mb-4 mb-lg-5 ">
<div class="position-relative">
<div class="bimg-top text-center">
<div>
</div>
<div>
</div>
</div>
<div style="height: 640px;" id="iframe-holder">
<iframe src="https://ico3c.com/bkg/ugj7f1kq94p1" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" width="100%" height="100%" allowfullscreen></iframe>
</div>
<div class="bimg-bottom text-center">
<div>
</div>
<div>
</div>
</div>
</div>
</div>
<div class="mb-4 mb-lg-5 text-center">
<a href="/download/ugj7f1kq94p1" class="btn btn-success px-5">Download</a>
</div>
</div>
</div>
</main>
<style>
#vplayer {padding-top: 56.25%;}
.bimg-top,
.bimg-bottom {
display: -webkit-flex;
display: -ms-flex;
display: flex;
-ms-align-items: center;
align-items: center;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
justify-content: center;
}
.bimg-top div,
.bimg-bottom div {
margin: .5rem;
}
@media(min-width:1600px){
.bimg-top,
.bimg-bottom {
position: absolute;
top: 0;
bottom: 0;
flex-direction: column;
}
.bimg-top {
left: -310px;
}
.bimg-bottom {
right: -310px;
}
.bimg-top div,
.bimg-bottom div {
margin: .5rem 0;
}
}
@media(min-width:1800px){
.bimg-top {
left: -350px;
}
.bimg-bottom {
right: -350px;
}
}
</style>
<footer class="footer py-4 py-lg-5">
<div class="container">
<div class="row align-items-center justify-content-between">
<div class="col-auto my-1">
<div class="text-muted"> FileMoon © 2022 </div>
</div>
<div class="col-auto my-1">
<nav class="nav">
<a class="nav-link text-muted footer-link px-0 mr-4" href="/">Home</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="/tos">Terms of Service</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="/privacy">Privacy Policy</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="/copyright">Copyright Policy</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="/affiliate">Make Money</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="/api">API Documentation</a>
<a class="nav-link text-muted footer-link px-0 mr-4" href="https://status.filemoon.sx">Server Status</a>
</nav>
</div>
</div>
</div>
</footer>
</div>
<div class="ssm">
<nav class="nav ssm-nav flex-column">
<a class="nav-link ssm-nav-link active" href="/"><i class="icon icon-home icon-size-16 ssm-nav-icon mr-3"></i>Home</a>
<a class="nav-link ssm-nav-link" href="/dashboard"><i class="icon icon-account icon-size-16 ssm-nav-icon mr-3"></i>My account</a>
<a class="nav-link ssm-nav-link" href="/premium"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Premium</a>
<a class="nav-link ssm-nav-link" href="/affiliate"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Make Money</a>
</nav>
<hr class="mx-3">
<nav class="nav flex-column ssm-nav">
<a class="nav-link ssm-nav-link" href="/tos"><i class="icon icon-pen icon-size-16 ssm-nav-icon mr-3"></i>Terms of Service</a>
<a class="nav-link ssm-nav-link" href="/privacy"><i class="icon icon-key icon-size-16 ssm-nav-icon mr-3"></i>Privacy Policy</a>
<a class="nav-link ssm-nav-link" href="/api"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>API Documentation</a>
<a class="nav-link ssm-nav-link" href="https://status.filemoon.sx"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>Server Status</a>
</nav>
</div>
<div class="ssm-overlay ssm-toggle-nav"></div>
<div class="usrmobile p-5 usrmobile-toggle-nav">
<a href="/login" class="btn btn-success btn-success-custom btn-lg btn-block text-uppercase shadow">Sign in</a>
<a href="/register" class="btn btn-outline-success btn-lg btn-block mb-2 text-uppercase">Sign up</a>
</div>
<script src="//filemoon.to/assets/js/bootstrap.bundle.min.js"></script>
<script src="//filemoon.to/assets/js/jquery.slideandswipe.min.js"></script>
<script src="//filemoon.to/assets/js/jquery.touchSwipe.min.js"></script>
<script src="//filemoon.to/assets/js/front.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"destination_url":"https://be7713.rcr82.waw05.i8yz83pn.com/hls2/05/09711/aeuhexjcoysl_x/master.m3u8?t=fiqqJThcgnrc77etu-nLPLodbF1UeqY5Cou0vvnLIB0&s=1765556185&e=10800&f=48721931&srv=1050&asn=13335&sp=5500&p=","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://ico3c.com/bkg/ugj7f1kq94p1"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}

View file

@ -1 +0,0 @@
{"destination_url":"https://be7713.rcr82.waw05.i8yz83pn.com/hls2/01/09665/wkhcbggdxf1d_h/master.m3u8?t=359izbQ_L-sOHaIjj275YKdBDyiPzUi3ETfbNoN9bzU&s=1759754698&e=10800&f=48329866&srv=1075&asn=13335&sp=4000&p=","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://z1ekv717.fun/d/wkhcbggdxf1d"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}

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