Compare commits
No commits in common. "main" and "v0.37.0" have entirely different histories.
691 changed files with 21998 additions and 145618 deletions
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
28
.github/workflows/build-tag.yml
vendored
28
.github/workflows/build-tag.yml
vendored
|
|
@ -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
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
23
.github/workflows/tests.yml
vendored
23
.github/workflows/tests.yml
vendored
|
|
@ -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@v4
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
".": "0.69.1"
|
||||
".": "0.37.0"
|
||||
}
|
||||
|
|
|
|||
2074
CHANGELOG.md
2074
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:24 AS builder
|
||||
FROM node:22.17-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.17-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
|
|
|||
98
README.md
98
README.md
|
|
@ -1,47 +1,15 @@
|
|||
# WebStreamr
|
||||
|
||||
[](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
|
||||
[](https://github.com/webstreamr/webstreamr/actions/workflows/tests.yml)
|
||||
[](https://github.com/webstreamr/webstreamr/releases)
|
||||

|
||||
|
||||
[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).
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const config: Config = {
|
|||
coverageDirectory: '<rootDir>/coverage',
|
||||
coveragePathIgnorePatterns: [
|
||||
'/src/controller/',
|
||||
'/src/utils/dispatcher.ts',
|
||||
],
|
||||
coverageProvider: 'babel',
|
||||
coverageThreshold: {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
process.env['CACHE_DIR'] = '/dev/null';
|
||||
process.env['HOST'] = 'example.test';
|
||||
process.env['PROTOCOL'] = 'https';
|
||||
|
||||
if (!process.env['TMDB_ACCESS_TOKEN']) {
|
||||
process.env['TMDB_ACCESS_TOKEN'] = 'some access token';
|
||||
}
|
||||
|
||||
jest.mock('randomstring', () => ({
|
||||
generate: jest.fn(() => 'mocked-random-string'),
|
||||
}));
|
||||
|
|
@ -13,5 +5,3 @@ jest.mock('randomstring', () => ({
|
|||
beforeEach(() => {
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => 639837296000);
|
||||
});
|
||||
|
||||
console.log = console.warn = console.error = console.info = console.debug = () => { /* disable in favor of logger */ };
|
||||
|
|
|
|||
5702
package-lock.json
generated
5702
package-lock.json
generated
File diff suppressed because it is too large
Load diff
48
package.json
48
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "webstreamr",
|
||||
"description": "Provides HTTP URLs from streaming websites.",
|
||||
"version": "0.69.1",
|
||||
"version": "0.37.0",
|
||||
"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",
|
||||
"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",
|
||||
"tough-cookie": "^5.1.2",
|
||||
"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/jsdom": "^21.1.7",
|
||||
"@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",
|
||||
|
|
|
|||
|
|
@ -1,35 +1,32 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import { Extractor } from '../extractor';
|
||||
import { landingTemplate } from '../landingTemplate';
|
||||
import { Source } from '../source';
|
||||
import { Config } from '../types';
|
||||
import { buildManifest, getDefaultConfig, isElfHostedInstance } from '../utils';
|
||||
import { buildManifest, getDefaultConfig } from '../utils';
|
||||
|
||||
export class ConfigureController {
|
||||
public readonly router: Router;
|
||||
|
||||
private readonly sources: Source[];
|
||||
private readonly extractors: Extractor[];
|
||||
|
||||
public constructor(sources: Source[], extractors: Extractor[]) {
|
||||
public constructor(sources: Source[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.sources = sources;
|
||||
this.extractors = extractors;
|
||||
|
||||
this.router.get('/configure', this.getConfigure.bind(this));
|
||||
this.router.get('/:config/configure', this.getConfigure.bind(this));
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (!req.params['config'] && req.host.endsWith('elfhosted.com')) {
|
||||
config.mediaFlowProxyUrl = `${req.protocol}://${req.host.replace('webstreamr', 'mediaflow-proxy')}`;
|
||||
}
|
||||
|
||||
const manifest = buildManifest(this.sources, this.extractors, config);
|
||||
const manifest = buildManifest(this.sources, config);
|
||||
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.send(landingTemplate(manifest));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Request, Response, Router } from 'express';
|
||||
import { Extractor } from '../extractor';
|
||||
import { Source } from '../source';
|
||||
import { Config } from '../types';
|
||||
import { buildManifest, getDefaultConfig } from '../utils';
|
||||
|
|
@ -8,22 +7,20 @@ export class ManifestController {
|
|||
public readonly router: Router;
|
||||
|
||||
private readonly sources: Source[];
|
||||
private readonly extractors: Extractor[];
|
||||
|
||||
public constructor(sources: Source[], extractors: Extractor[]) {
|
||||
public constructor(sources: Source[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.sources = sources;
|
||||
this.extractors = extractors;
|
||||
|
||||
this.router.get('/manifest.json', this.getManifest.bind(this));
|
||||
this.router.get('/:config/manifest.json', this.getManifest.bind(this));
|
||||
}
|
||||
|
||||
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);
|
||||
const manifest = buildManifest(this.sources, config);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(manifest);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Mutex } from 'async-mutex';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { ContentType } from 'stremio-addon-sdk';
|
||||
import winston from 'winston';
|
||||
import { Source } from '../source';
|
||||
import { contextFromRequestAndResponse, envIsProd, Id, ImdbId, StreamResolver, TmdbId } from '../utils';
|
||||
import { Config, Context } from '../types';
|
||||
import { envIsProd, getDefaultConfig, ImdbId, StreamResolver } from '../utils';
|
||||
|
||||
export class StreamController {
|
||||
public readonly router: Router;
|
||||
|
|
@ -12,8 +12,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();
|
||||
|
||||
|
|
@ -21,50 +19,32 @@ export class StreamController {
|
|||
this.sources = sources;
|
||||
this.streamResolver = streams;
|
||||
|
||||
this.router.get('/stream/:type/:id.json', this.getStream.bind(this));
|
||||
this.router.get('/:config/stream/:type/:id.json', this.getStream.bind(this));
|
||||
}
|
||||
|
||||
private async getStream(req: Request, res: Response) {
|
||||
const config: Config = req.params['config'] ? JSON.parse(req.params['config']) : getDefaultConfig();
|
||||
const type: ContentType = (req.params['type'] || '') as ContentType;
|
||||
const rawId: string = req.params['id'] as string || '';
|
||||
const id: string = req.params['id'] || '';
|
||||
|
||||
let id: Id;
|
||||
if (rawId.startsWith('tmdb:')) {
|
||||
id = TmdbId.fromString(rawId.replace('tmdb:', ''));
|
||||
} else if (rawId.startsWith('tt')) {
|
||||
id = ImdbId.fromString(rawId);
|
||||
} else {
|
||||
res.status(400).send(`Unsupported ID: ${rawId}`);
|
||||
const ctx: Context = {
|
||||
hostUrl: new URL(`${req.protocol}://${req.host}`),
|
||||
id: res.getHeader('X-Request-ID') as string,
|
||||
...(req.ip && { ip: req.ip }),
|
||||
config,
|
||||
};
|
||||
|
||||
return;
|
||||
this.logger.info(`Search stream for type "${type}" and id "${id}" for ip ${ctx.ip}`, ctx);
|
||||
|
||||
const sources = this.sources.filter(handler => handler.countryCodes.filter(countryCode => countryCode in ctx.config).length);
|
||||
|
||||
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, ImdbId.fromString(id));
|
||||
|
||||
if (ttl && envIsProd()) {
|
||||
res.setHeader('Cache-Control', `max-age=${ttl / 1000}, public`);
|
||||
}
|
||||
|
||||
const ctx = contextFromRequestAndResponse(req, res);
|
||||
|
||||
this.logger.info(`Search stream for type "${type}" and id "${rawId}" for ip ${ctx.ip}`, ctx);
|
||||
|
||||
const sources = this.sources.filter(source => source.countryCodes.filter(countryCode => countryCode in ctx.config).length);
|
||||
|
||||
let mutex = this.locks.get(rawId);
|
||||
if (!mutex) {
|
||||
mutex = new Mutex();
|
||||
this.locks.set(rawId, mutex);
|
||||
}
|
||||
|
||||
await mutex.runExclusive(async () => {
|
||||
const { streams, ttl } = await this.streamResolver.resolve(ctx, sources, type, id);
|
||||
|
||||
if (ttl && envIsProd()) {
|
||||
res.setHeader('Cache-Control', `public, max-age=${Math.floor(ttl / 1000)}, immutable`);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({ streams }));
|
||||
});
|
||||
|
||||
if (!mutex.isLocked()) {
|
||||
this.locks.delete(rawId);
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.send(JSON.stringify({ streams }));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './ExtractController';
|
||||
export * from './ConfigureController';
|
||||
export * from './ManifestController';
|
||||
export * from './StreamController';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
import winston from 'winston';
|
||||
import { BlockedReason, Context } from '../types';
|
||||
import { BlockedError } from './BlockedError';
|
||||
import { HttpError } from './HttpError';
|
||||
import { QueueIsFullError } from './QueueIsFullError';
|
||||
import { TimeoutError } from './TimeoutError';
|
||||
import { TooManyRequestsError } from './TooManyRequestsError';
|
||||
import { TooManyTimeoutsError } from './TooManyTimeoutsError';
|
||||
|
||||
export * from './BlockedError';
|
||||
export * from './HttpError';
|
||||
export * from './NotFoundError';
|
||||
|
|
@ -14,61 +5,3 @@ export * from './QueueIsFullError';
|
|||
export * from './TimeoutError';
|
||||
export * from './TooManyRequestsError';
|
||||
export * from './TooManyTimeoutsError';
|
||||
|
||||
export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger, source: string, error: unknown): string => {
|
||||
if (error instanceof BlockedError) {
|
||||
if (error.reason === BlockedReason.media_flow_proxy_auth) {
|
||||
return '⚠️ MediaFlow Proxy authentication failed. Please set the correct password.';
|
||||
}
|
||||
|
||||
logger.warn(`${source}: Request to ${error.url} was blocked, reason: ${error.reason}, headers: ${JSON.stringify(error.headers)}.`, ctx);
|
||||
|
||||
return `⚠️ Request to ${error.url.host} was blocked. Reason: ${error.reason}`;
|
||||
}
|
||||
|
||||
if (error instanceof TooManyRequestsError) {
|
||||
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
|
||||
|
||||
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
|
||||
}
|
||||
|
||||
if (error instanceof TooManyTimeoutsError) {
|
||||
logger.warn(`${source}: Too many timeouts when requesting ${error.url}.`, ctx);
|
||||
|
||||
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
|
||||
}
|
||||
|
||||
if (error instanceof TimeoutError) {
|
||||
logger.warn(`${source}: Request to ${error.url} timed out.`, ctx);
|
||||
|
||||
return `🐢 Request to ${error.url.host} timed out.`;
|
||||
}
|
||||
|
||||
if (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) {
|
||||
// sometimes this gets through, no idea why..
|
||||
logger.warn(`${source}: Request timed out.`, ctx);
|
||||
|
||||
return '🐢 Request timed out.';
|
||||
}
|
||||
|
||||
if (error instanceof QueueIsFullError) {
|
||||
logger.warn(`${source}: Request queue for ${error.url.host} is full.`, ctx);
|
||||
|
||||
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
|
||||
}
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
logger.error(`${source}: Error when requesting url ${error.url}, HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
|
||||
|
||||
if (error.status >= 500) {
|
||||
return `❌ Remote server ${error.url.host} has issues. We can't fix this, please try later again.`;
|
||||
}
|
||||
|
||||
return `❌ Request to ${error.url.host} failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
|
||||
}
|
||||
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
logger.error(`${source} error: ${error}, cause: ${cause}, stack: ${(error as Error).stack}`, ctx);
|
||||
|
||||
return `❌ Request failed. Request-id: ${ctx.id}.`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
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 +10,66 @@ 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 null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|d0o0d|do0od|d0000d|d000d|vidply|all3do|doply|vide0/);
|
||||
};
|
||||
|
||||
public override normalize(url: URL): URL {
|
||||
const videoId = url.pathname.replace(/\/+$/, '').split('/').at(-1) as string;
|
||||
const videoId = url.pathname.split('/').slice(-1)[0] as string;
|
||||
|
||||
return new URL(`http://dood.to/e/${videoId}`);
|
||||
};
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
|
||||
const html = await this.fetcher.text(ctx, new URL(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(`http://dood.to${passMd5Match[0]}`));
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('title').text().trim().replace(/ - DoodStream$/, '').trim();
|
||||
|
||||
const downloadHtml = await this.fetcher.text(ctx, new URL(url.href.replace('/e/', '/d/')));
|
||||
const sizeMatch = downloadHtml.match(/([\d.]+ ?[GM]B)/);
|
||||
let mp4Url: URL;
|
||||
let bytes: number | undefined;
|
||||
if (baseUrl.includes('cloudflarestorage')) {
|
||||
mp4Url = new URL(baseUrl);
|
||||
} else {
|
||||
mp4Url = new URL(`${baseUrl}${randomstring.generate(10)}?token=${token}&expiry=${Date.now()}`);
|
||||
const mp4Head = await this.fetcher.head(ctx, mp4Url, { headers: { Referer: url.origin } });
|
||||
if (mp4Head['content-length']) {
|
||||
bytes = parseInt(mp4Head['content-length'] as string);
|
||||
}
|
||||
}
|
||||
|
||||
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 }),
|
||||
},
|
||||
requestHeaders: {
|
||||
Referer: url.origin,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { showExternalUrls } from '../utils';
|
||||
import { Context, CountryCode, Format, UrlResult } from '../types';
|
||||
import { Fetcher, showExternalUrls } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class ExternalUrl extends Extractor {
|
||||
|
|
@ -7,20 +7,40 @@ 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 {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
url: url,
|
||||
format: Format.unknown,
|
||||
isExternal: true,
|
||||
label: `${url.host}`,
|
||||
meta,
|
||||
sourceId: `${this.id}_${countryCode}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
countryCodes: [countryCode],
|
||||
title,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
|
||||
import { Fetcher } from '../utils';
|
||||
import { Context, CountryCode, Format, UrlResult } from '../types';
|
||||
import { guessHeightFromTitle } from '../utils';
|
||||
|
||||
export abstract class Extractor {
|
||||
public abstract readonly id: string;
|
||||
|
|
@ -9,34 +9,29 @@ 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,
|
||||
const urlResults = await this.extractInternal(ctx, url, countryCode, title);
|
||||
|
||||
}),
|
||||
);
|
||||
return urlResults.map((urlResult) => {
|
||||
let height = urlResult.meta.height;
|
||||
|
||||
if (!height && urlResult.meta.title) {
|
||||
height = guessHeightFromTitle(urlResult.meta.title);
|
||||
}
|
||||
|
||||
return { ...urlResult, meta: { ...urlResult.meta, height } };
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return [];
|
||||
|
|
@ -48,15 +43,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,112 +1,41 @@
|
|||
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 { 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: 1024 });
|
||||
}
|
||||
|
||||
public stats() {
|
||||
return {
|
||||
urlResultCache: this.urlResultCache.stats,
|
||||
lazyUrlResultCache: this.lazyUrlResultCache.stats,
|
||||
};
|
||||
};
|
||||
|
||||
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
|
||||
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
|
||||
public async handle(ctx: Context, url: URL, countryCode: CountryCode, title?: string | undefined): Promise<UrlResult[]> {
|
||||
const extractor = this.extractors.find(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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import winston from 'winston';
|
||||
import { createTestContext } from '../test';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { Fastream } from './Fastream';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new Fastream(new FetcherMock(`${__dirname}/__fixtures__/Fastream`))]);
|
||||
|
||||
const ctx = createTestContext({ mediaFlowProxyUrl: 'https://mediaflow-proxy.test', mediaFlowProxyPassword: 'asdfg' });
|
||||
|
||||
describe('Fastream', () => {
|
||||
test('fastream.to embed', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-3aooif4ozt10.html'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('expired or deleted', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://fastream.to/embed-a9rp1d1pqom7.html'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import bytes from 'bytes';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { buildMediaFlowProxyExtractorStreamUrl, supportsMediaFlowProxy } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class Fastream extends Extractor {
|
||||
public readonly id = 'fastream';
|
||||
|
||||
public readonly label = 'Fastream';
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
|
||||
public supports(ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/fastream/) && supportsMediaFlowProxy(ctx);
|
||||
}
|
||||
|
||||
public override normalize(url: URL): URL {
|
||||
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const downloadUrl = new URL(url.href.replace('/embed-', '/d/'));
|
||||
const html = await this.fetcher.text(ctx, downloadUrl, { headers });
|
||||
|
||||
if (/No such file/.test(html)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url, headers);
|
||||
|
||||
const heightAndSizeMatch = html.match(/\d{3,}x(\d{3,}), ([\d.]+ ?[GM]B)/) as string[];
|
||||
const titleMatch = html.match(/>Download (.*?)</) as string[];
|
||||
|
||||
return [
|
||||
{
|
||||
url: playlistUrl,
|
||||
format: Format.hls,
|
||||
meta: {
|
||||
...meta,
|
||||
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
|
||||
height: parseInt(heightAndSizeMatch[1] as string),
|
||||
title: titleMatch[1],
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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) }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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(),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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) : [];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/kinoger.py */
|
||||
|
|
@ -9,69 +9,47 @@ 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 [
|
||||
'asianembed.cam',
|
||||
'disneycdn.net',
|
||||
'dzo.vidplayer.live',
|
||||
'filedecrypt.link',
|
||||
'filma365.strp2p.site',
|
||||
'flimmer.rpmvip.com',
|
||||
'flixfilmesonline.strp2p.site',
|
||||
'kinoger.p2pplay.pro',
|
||||
'kinoger.re',
|
||||
'moflix.rpmplay.xyz',
|
||||
'moflix.upns.xyz',
|
||||
'player.upn.one',
|
||||
'securecdn.shop',
|
||||
'shiid4u.upn.one',
|
||||
'srbe84.vidplayer.live',
|
||||
'strp2p.site',
|
||||
't1.p2pplay.pro',
|
||||
'tuktuk.rpmvid.com',
|
||||
'ultrastream.online',
|
||||
'videoland.cfd',
|
||||
'videoshar.uns.bio',
|
||||
'w1tv.xyz',
|
||||
'wasuytm.store',
|
||||
].includes(url.host);
|
||||
return null !== url.host.match(/kinoger\.re|shiid4u\.upn\.one|moflix\.upns\.xyz|player\.upn\.one|wasuytm\.store|ultrastream\.online/);
|
||||
}
|
||||
|
||||
public override normalize(url: URL): URL {
|
||||
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = {
|
||||
'Origin': url.origin,
|
||||
'Referer': url.origin + '/',
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
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 m3u8Url = new URL(source);
|
||||
const { cf, title } = JSON.parse(decrypted) as { cf: string; title: string };
|
||||
|
||||
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],
|
||||
title,
|
||||
},
|
||||
requestHeaders: headers,
|
||||
requestHeaders: {
|
||||
Referer: url.origin,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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,10 @@ 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('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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
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;
|
||||
|
||||
public supports(ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
|
||||
private readonly fetcher: Fetcher;
|
||||
|
||||
public constructor(fetcher: Fetcher) {
|
||||
super();
|
||||
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
|
||||
public supports(ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/mixdrop/) && supportsMediaFlowProxy(ctx);
|
||||
}
|
||||
|
||||
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 +34,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 +43,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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import winston from 'winston';
|
||||
import { createTestContext } from '../test';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { SaveFiles } from './SaveFiles';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new SaveFiles(new FetcherMock(`${__dirname}/__fixtures__/SaveFiles`))]);
|
||||
|
||||
const ctx = createTestContext();
|
||||
|
||||
describe('SafeFiles', () => {
|
||||
test('savefiles', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/ip0k0dj2g0i3'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('savefiles /e/', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/e/edptfmhyjr39'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('savefiles /d/', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/d/s9g6zb5kjbqd'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('savefiles locked file', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/omqq55i59nvv'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('savefiles deleted file', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://savefiles.com/bejqk3vlsv02'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('streamhls /e/', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://streamhls.to/e/ip0k0dj2g0i3'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import * as cheerio from 'cheerio';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class SaveFiles extends Extractor {
|
||||
public readonly id = 'savefiles';
|
||||
|
||||
public readonly label = 'SaveFiles';
|
||||
|
||||
public override readonly ttl: number = 21600000; // 6h
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/savefiles|streamhls/);
|
||||
}
|
||||
|
||||
public override normalize(url: URL): URL {
|
||||
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
if (/file was locked|file was deleted/i.test(html)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const fileMatch = html.match(/file:"(.*?)"/) as string[];
|
||||
const sizeMatch = html.match(/\[\d{3,}x(\d{3,})/) as string[];
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('.download-title').text().trim();
|
||||
|
||||
return [
|
||||
{
|
||||
url: new URL(fileMatch[1] as string),
|
||||
format: Format.hls,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
height: parseInt(sizeMatch[1] as string),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
import winston from 'winston';
|
||||
import { createTestContext } from '../test';
|
||||
import { CountryCode } from '../types';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { VidSrc } from './VidSrc';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]);
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`))]);
|
||||
|
||||
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, 'Black Mirror 4x2')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 } from '../error';
|
||||
import { Context, CountryCode, Format, UrlResult } from '../types';
|
||||
import { Fetcher, guessHeightFromPlaylist } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
|
|
@ -9,49 +10,22 @@ export class VidSrc extends Extractor {
|
|||
|
||||
public readonly label = 'VidSrc';
|
||||
|
||||
public override readonly ttl: number = 10800000; // 3h
|
||||
private readonly fetcher: Fetcher;
|
||||
|
||||
private readonly domains: NonEmptyArray<string>;
|
||||
public constructor(fetcher: Fetcher) {
|
||||
super();
|
||||
|
||||
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
|
||||
super(fetcher);
|
||||
|
||||
this.domains = domains;
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
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 };
|
||||
protected async extractInternal(ctx: Context, url: URL, countryCode: CountryCode): Promise<UrlResult[]> {
|
||||
const html = await this.fetcher.text(ctx, url);
|
||||
|
||||
return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]);
|
||||
};
|
||||
|
||||
private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise<InternalUrlResult[]> {
|
||||
const domainIndex = Math.floor(Math.random() * domains.length);
|
||||
const [domain] = domains.splice(domainIndex, 1) as [string];
|
||||
|
||||
const newUrl = new URL(url);
|
||||
newUrl.hostname = domain;
|
||||
|
||||
let html: string;
|
||||
try {
|
||||
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
|
||||
} catch (error) {
|
||||
/* istanbul ignore next */
|
||||
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
|
||||
return this.extractUsingRandomDomain(ctx, url, meta, domains);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
throw error;
|
||||
}
|
||||
|
||||
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
|
||||
const title = $('title').text().trim();
|
||||
|
|
@ -62,25 +36,30 @@ 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,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import winston from 'winston';
|
||||
import { createTestContext } from '../test';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { YouTube } from './YouTube';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new YouTube(new FetcherMock(`${__dirname}/__fixtures__/YouTube`))]);
|
||||
|
||||
const ctx = createTestContext();
|
||||
|
||||
describe('YouTube', () => {
|
||||
test('Solaris', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://www.youtube.com/watch?v=Z8ZhQPaw4rE'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('unsupported format', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://youtu.be/Z8ZhQPaw4rE?feature=shared'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class YouTube extends Extractor {
|
||||
public readonly id = 'youtube';
|
||||
|
||||
public readonly label = 'YouTube';
|
||||
|
||||
public override readonly ttl: number = 21600000; // 6h
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
const titleMatch = html.match(/"title":{"runs":\[{"text":"(.*?)"/) as string[];
|
||||
|
||||
return [
|
||||
{
|
||||
url,
|
||||
format: Format.unknown,
|
||||
ytId: url.searchParams.get('v') as string,
|
||||
meta: {
|
||||
...meta,
|
||||
title: titleMatch[1] as string,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"server":"nginx","date":"Sun, 08 Jun 2025 19:20:59 GMT","content-type":"video/mp4","content-length":791702409,"last-modified":"Wed, 03 Apr 2024 09:53:31 GMT","connection":"keep-alive","etag":"660d271b-2f306b89","access-control-allow-origin":"*","accept-ranges":"bytes"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/extractor/__fixtures__/DoodStream/http:dood.toe1cfcevn6dg8shrfvht22odxw2lty18hr
generated
Normal file
1
src/extractor/__fixtures__/DoodStream/http:dood.toe1cfcevn6dg8shrfvht22odxw2lty18hr
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1 @@
|
|||
<!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>
|
||||
<!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></body></html>
|
||||
File diff suppressed because one or more lines are too long
1
src/extractor/__fixtures__/DoodStream/http:dood.toewfpwtsgyr1xi
generated
Normal file
1
src/extractor/__fixtures__/DoodStream/http:dood.toewfpwtsgyr1xi
generated
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
https://ijs155l.cloudatacdn.com/u5kj2rz5qtdlsdgge4hrgj2flz5cjodea6auatveaphxogr2vvukxfzmwzva/egprvdjvpr~
|
||||
|
|
@ -0,0 +1 @@
|
|||
https://ee317r.cloudatacdn.com/u5kj63yvxddlsdgge7qremqqka27irwiupc4k7o5cikbhjliz6xwv2xr53uq/lco4d3heif~
|
||||
|
|
@ -0,0 +1 @@
|
|||
https://wec6bnh7rx7rw11bv9e9vslw4rud8nam.5f1ebd98099ce35faeeddb30c1752191.r2.cloudflarestorage.com/icxxqqdsfdc4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ab085b515b950aae0a86cae59456cacd%2F20250714%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250714T154400Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=845ebfa5feedd840027e431f31e013513dd4c4f74364b4e75a2c8ddb213bd97a
|
||||
|
|
@ -0,0 +1 @@
|
|||
https://aa360cc.cloudatacdn.com/u5kjv4jxytd3sdgge5uogji5dg4huat2pxrm2qibdbm5rlyusvl46iz3u3ta/1f4isxg0tw~
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
|
@ -1,233 +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>
|
||||
|
||||
<table class="tbl1" >
|
||||
<th colspan=2>Download black.mirror.s01e01.lat.720p.mp4</th>
|
||||
|
||||
<tr><td><a href="https://fastream.to/d/3aooif4ozt10_x">UHD quality</a></td><td>1920x1080, 1.0 GB</td></tr>
|
||||
|
||||
<tr><td><a href="https://fastream.to/d/3aooif4ozt10_h">HD quality</a></td><td>1280x720, 396.8 MB</td></tr>
|
||||
|
||||
<tr><td><a href="https://fastream.to/d/3aooif4ozt10_n">Normal quality</a></td><td>852x480, 210.1 MB</td></tr>
|
||||
|
||||
<tr><td><a href="https://fastream.to/d/3aooif4ozt10_l">Low quality</a></td><td>640x360, 104.0 MB</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
</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">© 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:'97115f719f301903',t:'MTc1NTUxOTcwNQ=='};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>
|
||||
|
|
@ -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">© 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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"destination_url":"https://s28.fastream.to/hls2/09/00024/3aooif4ozt10_,l,n,h,x,.urlset/master.m3u8?t=nw6_aeN6pDROuXsJQwyrJVm3qHKFedVfbTNj5LgXH-4&s=1752863379&e=43200&v=1058373&i=0.3&sp=0","request_headers":{"user-agent":"Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0","Accept":"*/*","Connection":"keep-alive","Accept-Language":"en-US,en;q=0.5","referer":"https://fastream.to/","origin":"https://fastream.to"},"mediaflow_proxy_url":"https://mediaflow-proxy.test/proxy/hls/manifest.m3u8","query_params":{"api_password":"asdfg"}}
|
||||
|
|
@ -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'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
|
|
@ -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'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>
|
||||
|
||||
|
|
@ -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"}}
|
||||
|
|
@ -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"}}
|
||||
|
|
@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue