Compare commits
No commits in common. "main" and "v0.53.5" have entirely different histories.
644 changed files with 34910 additions and 87444 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@v5
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v5
|
||||
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.53.5"
|
||||
}
|
||||
|
|
|
|||
1075
CHANGELOG.md
1075
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:24 AS builder
|
||||
FROM node:22-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-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).
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
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'),
|
||||
|
|
|
|||
3820
package-lock.json
generated
3820
package-lock.json
generated
File diff suppressed because it is too large
Load diff
35
package.json
35
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "webstreamr",
|
||||
"description": "Provides HTTP URLs from streaming websites.",
|
||||
"version": "0.69.1",
|
||||
"version": "0.53.5",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"analyse": "tsc --noEmit --project ./tsconfig.dev.json",
|
||||
|
|
@ -28,55 +28,38 @@
|
|||
"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",
|
||||
"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",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-cache-semantics": "^4.2.0",
|
||||
"minimatch": "^10.0.3",
|
||||
"randomstring": "^1.3.1",
|
||||
"rot13-cipher": "^1.0.0",
|
||||
"slugify": "^1.6.6",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"undici": "^7.10.0",
|
||||
"unpacker": "^1.0.1",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@tsconfig/node24": "^24.0.0",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/fast-levenshtein": "^0.0.4",
|
||||
"@types/http-cache-semantics": "^4.0.4",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/memoizee": "^0.4.12",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/make-fetch-happen": "^10.0.4",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/randomstring": "^1.3.0",
|
||||
"@types/stremio-addon-sdk": "^1.6.11",
|
||||
"babel-jest": "^30.0.0",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class ConfigureController {
|
|||
}
|
||||
|
||||
private getConfigure(req: Request, res: Response) {
|
||||
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
|
||||
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
|
||||
|
||||
// Convenience preset for ElfHosted WebStreamr bundle including Media Flow Proxy
|
||||
if (!req.params['config'] && isElfHostedInstance(req)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export class ManifestController {
|
|||
}
|
||||
|
||||
private getManifest(req: Request, res: Response) {
|
||||
const config: Config = JSON.parse(req.params['config'] as string || JSON.stringify(getDefaultConfig()));
|
||||
const config: Config = JSON.parse(req.params['config'] || JSON.stringify(getDefaultConfig()));
|
||||
|
||||
const manifest = buildManifest(this.sources, this.extractors, config);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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 { contextFromRequestAndResponse, envIsProd, Id, ImdbId, noCache, StreamResolver, TmdbId } from '../utils';
|
||||
|
||||
export class StreamController {
|
||||
public readonly router: Router;
|
||||
|
|
@ -27,7 +27,7 @@ export class StreamController {
|
|||
|
||||
private async getStream(req: Request, res: Response) {
|
||||
const type: ContentType = (req.params['type'] || '') as ContentType;
|
||||
const rawId: string = req.params['id'] as string || '';
|
||||
const rawId: string = req.params['id'] || '';
|
||||
|
||||
let id: Id;
|
||||
if (rawId.startsWith('tmdb:')) {
|
||||
|
|
@ -35,9 +35,7 @@ export class StreamController {
|
|||
} else if (rawId.startsWith('tt')) {
|
||||
id = ImdbId.fromString(rawId);
|
||||
} else {
|
||||
res.status(400).send(`Unsupported ID: ${rawId}`);
|
||||
|
||||
return;
|
||||
throw new Error(`Unsupported ID: ${rawId}`);
|
||||
}
|
||||
|
||||
const ctx = contextFromRequestAndResponse(req, res);
|
||||
|
|
@ -55,8 +53,10 @@ export class StreamController {
|
|||
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`);
|
||||
if (noCache(ctx.config)) {
|
||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
||||
} else if (ttl && envIsProd()) {
|
||||
res.setHeader('Cache-Control', `max-age=${Math.floor(ttl / 1000)}, public`);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from './ExtractController';
|
||||
export * from './ConfigureController';
|
||||
export * from './ManifestController';
|
||||
export * from './StreamController';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
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(url: URL, reason: BlockedReason, headers: Record<string, string[] | string | undefined>) {
|
||||
super();
|
||||
|
||||
this.url = url;
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -21,50 +21,46 @@ export const logErrorAndReturnNiceString = (ctx: Context, logger: winston.Logger
|
|||
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);
|
||||
logger.warn(`${source}: Request to ${error.url.href} was blocked, reason: ${error.reason}, headers: ${JSON.stringify(error.headers)}.`, ctx);
|
||||
|
||||
return `⚠️ Request to ${error.url.host} was blocked. Reason: ${error.reason}`;
|
||||
return `⚠️ Request was blocked. Reason: ${error.reason}`;
|
||||
}
|
||||
|
||||
if (error instanceof TooManyRequestsError) {
|
||||
logger.warn(`${source}: Request to ${error.url} was rate limited for ${error.retryAfter} seconds.`, ctx);
|
||||
logger.warn(`${source}: Rate limited for ${error.retryAfter} seconds.`, ctx);
|
||||
|
||||
return `🚦 Request to ${error.url.host} was rate-limited. Please try again later or consider self-hosting.`;
|
||||
return '🚦 Request was rate-limited. Please try again later or consider self-hosting.';
|
||||
}
|
||||
|
||||
if (error instanceof TooManyTimeoutsError) {
|
||||
logger.warn(`${source}: Too many timeouts when requesting ${error.url}.`, ctx);
|
||||
logger.warn(`${source}: Too many timeouts.`, ctx);
|
||||
|
||||
return `🚦 Too many recent timeouts when requesting ${error.url.host}. Please try again later.`;
|
||||
return '🚦 Too many recent timeouts. Please try again later.';
|
||||
}
|
||||
|
||||
if (error instanceof TimeoutError) {
|
||||
logger.warn(`${source}: Request to ${error.url} timed out.`, ctx);
|
||||
|
||||
return `🐢 Request to ${error.url.host} timed out.`;
|
||||
}
|
||||
|
||||
if (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) {
|
||||
// sometimes this gets through, no idea why..
|
||||
if (
|
||||
error instanceof TimeoutError
|
||||
|| (error instanceof DOMException && ['AbortError', 'TimeoutError'].includes(error.name)) // sometimes this gets through, no idea why..
|
||||
) {
|
||||
logger.warn(`${source}: Request timed out.`, ctx);
|
||||
|
||||
return '🐢 Request timed out.';
|
||||
}
|
||||
|
||||
if (error instanceof QueueIsFullError) {
|
||||
logger.warn(`${source}: Request queue for ${error.url.host} is full.`, ctx);
|
||||
logger.warn(`${source}: Request queue is full.`, ctx);
|
||||
|
||||
return `⏳ Request queue for ${error.url.host} is full. Please try again later or consider self-hosting.`;
|
||||
return '⏳ Request queue is full. Please try again later or consider self-hosting.';
|
||||
}
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
logger.error(`${source}: Error when requesting url ${error.url}, HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
|
||||
logger.error(`${source}: HTTP status ${error.status} (${error.statusText}), headers: ${JSON.stringify(error.headers)}, stack: ${error.stack}.`, ctx);
|
||||
|
||||
if (error.status >= 500) {
|
||||
return `❌ Remote server ${error.url.host} has issues. We can't fix this, please try later again.`;
|
||||
return `❌ Remote server has issues. We can't fix this, please try later again.`;
|
||||
}
|
||||
|
||||
return `❌ Request to ${error.url.host} failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
|
||||
return `❌ Request failed with status ${error.status} (${error.statusText}). Request-id: ${ctx.id}.`;
|
||||
}
|
||||
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
|
|
|
|||
|
|
@ -7,18 +7,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();
|
||||
});
|
||||
|
||||
test('doodster', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://dooodster.com/e/1cfcevn6dg8shrfvht22odxw2lty18hr'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('missing pass_md5 -> not found', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://dood.to/e/gy8l8mb2i311'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('can guess height from title', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://do7go.com/e/dfx8me4un4ul'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('not found', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://myvidplay.com/e/cz7cus0bvlzr'))).toMatchSnapshot();
|
||||
test('cloudflare storage', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://doodstream.com/e/wfpwtsgyr1xi'))).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, Format, Meta, UrlResult } from '../types';
|
||||
import { guessSizeFromMp4 } from '../utils/size';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class DoodStream extends Extractor {
|
||||
|
|
@ -12,42 +12,57 @@ export class DoodStream extends Extractor {
|
|||
|
||||
public override readonly ttl: number = 21600000; // 6h
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
|
||||
/** @see https://github.com/Gujal00/ResolveURL/blob/master/script.module.resolveurl/lib/resolveurl/plugins/doodstream.py */
|
||||
public supports(ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/dood|do[0-9]go|doood|dooood|ds2play|ds2video|dsvplay|d0o0d|do0od|d0000d|d000d|myvidplay|vidply|all3do|doply|vide0|vvide0|d-s/) && supportsMediaFlowProxy(ctx);
|
||||
public 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|vidply|all3do|doply|vide0|vvide0|d-s/);
|
||||
};
|
||||
|
||||
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[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
if (/Video not found/.test(html)) {
|
||||
const passMd5Match = html.match(/\/pass_md5\/[\w-]+\/([\w-]+)/);
|
||||
if (!passMd5Match) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const token = passMd5Match[1] as string;
|
||||
|
||||
const baseUrl = await this.fetcher.text(ctx, new URL(passMd5Match[0], url.origin), { headers: { Referer: url.href } });
|
||||
|
||||
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()}`);
|
||||
bytes = await guessSizeFromMp4(ctx, this.fetcher, mp4Url, { headers: { Referer: url.href } });
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Doodstream', url, headers),
|
||||
url: mp4Url,
|
||||
format: Format.mp4,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
|
||||
...(bytes && { bytes }),
|
||||
},
|
||||
requestHeaders: {
|
||||
Referer: url.origin,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ describe('Dropload', () => {
|
|||
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/embed-lyo2h1snpe5c.html'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('dropload unknown height and size', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/gf2apij3ql37'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('file not found', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'))).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, Format, Meta, UrlResult } from '../types';
|
||||
import { extractUrlFromPacked } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class Dropload extends Extractor {
|
||||
|
|
@ -13,12 +13,12 @@ export class Dropload extends Extractor {
|
|||
public override readonly ttl: number = 10800000; // 3h
|
||||
|
||||
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[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
|
@ -27,31 +27,26 @@ export class Dropload extends Extractor {
|
|||
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}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
bytes: bytes.parse(sizeMatch[1] as string) as number,
|
||||
height: parseInt(heightMatch[1] as string),
|
||||
title,
|
||||
...(size && { bytes: size }),
|
||||
...(height && { height }),
|
||||
},
|
||||
requestHeaders: playlistHeaders,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { BlockedError } from '../error';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { showExternalUrls } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
|
|
@ -13,13 +14,27 @@ export class ExternalUrl extends Extractor {
|
|||
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, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
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 });
|
||||
} catch (error) {
|
||||
/* istanbul ignore if */
|
||||
if (!(error instanceof BlockedError)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
url: url,
|
||||
format: Format.unknown,
|
||||
isExternal: true,
|
||||
label: `${url.host}`,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta, UrlResult } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Fetcher } from '../utils';
|
||||
|
||||
export abstract class Extractor {
|
||||
|
|
@ -9,8 +9,6 @@ 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;
|
||||
|
|
@ -25,18 +23,11 @@ export abstract class Extractor {
|
|||
return url;
|
||||
};
|
||||
|
||||
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]>;
|
||||
protected abstract extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]>;
|
||||
|
||||
public async extract(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
try {
|
||||
return (await this.extractInternal(ctx, url, meta)).map(
|
||||
urlResult => ({
|
||||
...urlResult,
|
||||
label: this.formatLabel(urlResult.label ?? this.label),
|
||||
ttl: this.ttl,
|
||||
|
||||
}),
|
||||
);
|
||||
return await this.extractInternal(ctx, url, meta);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return [];
|
||||
|
|
@ -48,15 +39,11 @@ export abstract class Extractor {
|
|||
format: Format.unknown,
|
||||
isExternal: true,
|
||||
error,
|
||||
label: this.formatLabel(this.label),
|
||||
ttl: 0,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}`,
|
||||
meta,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
private formatLabel(label: string): string {
|
||||
return this.viaMediaFlowProxy ? `${label} (MFP)` : label;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@ describe('ExtractorRegistry', () => {
|
|||
expect(urlResults).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('empty results are cached', async () => {
|
||||
const urlResults = await extractorRegistry.handle(ctx, new URL('https://dropload.io/asdfghijklmn.html'), { title: 'title' });
|
||||
expect(urlResults).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('stats returns something', async () => {
|
||||
const stats = extractorRegistry.stats();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,23 @@
|
|||
// eslint-disable-next-line import/no-named-as-default
|
||||
import KeyvSqlite from '@keyv/sqlite';
|
||||
import { Cacheable, CacheableMemory, Keyv } from 'cacheable';
|
||||
import winston from 'winston';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { createKeyvSqlite, envGet, isExtractorDisabled } from '../utils';
|
||||
import { Context, Meta, UrlResult } from '../types';
|
||||
import { getCacheDir, isExtractorDisabled, noCache } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class ExtractorRegistry {
|
||||
private readonly logger: winston.Logger;
|
||||
private readonly extractors: Extractor[];
|
||||
|
||||
private readonly urlResultCache: Cacheable;
|
||||
private readonly lazyUrlResultCache: Cacheable;
|
||||
|
||||
public constructor(logger: winston.Logger, extractors: Extractor[]) {
|
||||
this.logger = logger;
|
||||
this.extractors = extractors;
|
||||
|
||||
this.urlResultCache = new Cacheable({
|
||||
nonBlocking: true,
|
||||
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
||||
secondary: createKeyvSqlite('extractor-cache'),
|
||||
stats: true,
|
||||
});
|
||||
|
||||
this.lazyUrlResultCache = new Cacheable({
|
||||
nonBlocking: true,
|
||||
primary: new Keyv({ store: new CacheableMemory({ lruSize: 1024 }) }),
|
||||
secondary: createKeyvSqlite('extractor-lazy-cache'),
|
||||
secondary: new Keyv(new KeyvSqlite(`sqlite://${getCacheDir()}/webstreamr-extractor-cache.sqlite`)),
|
||||
stats: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -33,80 +25,45 @@ export class ExtractorRegistry {
|
|||
public stats() {
|
||||
return {
|
||||
urlResultCache: this.urlResultCache.stats,
|
||||
lazyUrlResultCache: this.lazyUrlResultCache.stats,
|
||||
};
|
||||
};
|
||||
|
||||
public async handle(ctx: Context, url: URL, meta?: Meta, allowLazy?: boolean): Promise<UrlResult[]> {
|
||||
public async handle(ctx: Context, url: URL, meta?: Meta): Promise<UrlResult[]> {
|
||||
const extractor = this.extractors.find(extractor => !isExtractorDisabled(ctx.config, extractor) && extractor.supports(ctx, url));
|
||||
if (!extractor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedUrl = extractor.normalize(url);
|
||||
const cacheKey = this.determineCacheKey(ctx, extractor, normalizedUrl);
|
||||
const cacheKey = extractor.viaMediaFlowProxy
|
||||
? `${extractor.id}_${normalizedUrl}_${ctx.config.mediaFlowProxyUrl}`
|
||||
: `${extractor.id}_${normalizedUrl}`;
|
||||
|
||||
const storedDataRaw = await this.urlResultCache.getRaw<UrlResult[]>(cacheKey);
|
||||
const expires = storedDataRaw?.expires;
|
||||
if (storedDataRaw && expires) {
|
||||
const ttl = expires - Date.now();
|
||||
if (storedDataRaw && expires && !noCache(ctx.config)) {
|
||||
// Ignore the cache randomly after at least 2/3 of the TTL passed to start refreshing results slowly
|
||||
const refreshTimestamp = this.randomInteger(expires - extractor.ttl * (2 / 3), expires);
|
||||
const now = Date.now();
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (ttl > 0) {
|
||||
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl, url: new URL(urlResult.url) }));
|
||||
if (refreshTimestamp > now) {
|
||||
return (storedDataRaw.value as UrlResult[]).map(urlResult => ({ ...urlResult, ttl: expires - now, url: new URL(urlResult.url) }));
|
||||
}
|
||||
}
|
||||
|
||||
const lazyUrlResults = await this.lazyUrlResultCache.get<UrlResult[]>(normalizedUrl.href) ?? [];
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (
|
||||
lazyUrlResults.length && allowLazy && !extractor.viaMediaFlowProxy
|
||||
&& lazyUrlResults.every(urlResult => urlResult.format !== Format.hls) // related to Android issues, e.g. https://github.com/Stremio/stremio-bugs/issues/1574 or https://github.com/Stremio/stremio-bugs/issues/1579
|
||||
) {
|
||||
// generate lazy extract urls
|
||||
return lazyUrlResults.map((urlResult, index) => {
|
||||
const extractUrl = new URL(`${envGet('PROTOCOL')}:${envGet('HOST')}/extract/`);
|
||||
|
||||
extractUrl.searchParams.set('index', `${index}`);
|
||||
extractUrl.searchParams.set('url', url.href);
|
||||
|
||||
return { ...urlResult, url: extractUrl };
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(`Extract ${url} using ${extractor.id} extractor`, ctx);
|
||||
|
||||
const mergedMeta: Meta = { ...meta, ...lazyUrlResults[0]?.meta };
|
||||
const urlResults = await extractor.extract(ctx, normalizedUrl, { extractorId: extractor.id, ...mergedMeta });
|
||||
const urlResults = await extractor.extract(ctx, normalizedUrl, { ...meta, countryCodes: meta?.countryCodes ?? [] });
|
||||
|
||||
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) {
|
||||
await this.urlResultCache.set<UrlResult[]>(cacheKey, urlResults, 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}`;
|
||||
private randomInteger(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,4 @@ 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,13 +1,12 @@
|
|||
import bytes from 'bytes';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } 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 readonly label = 'Fastream (via MediaFlow Proxy)';
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
|
||||
|
|
@ -19,18 +18,14 @@ export class Fastream extends Extractor {
|
|||
return new URL(url.href.replace('/e/', '/embed-').replace('/d/', '/embed-'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'Fastream', url, headers);
|
||||
|
||||
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[];
|
||||
|
||||
|
|
@ -38,6 +33,9 @@ export class Fastream extends Extractor {
|
|||
{
|
||||
url: playlistUrl,
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
bytes: bytes.parse(heightAndSizeMatch[2] as string) as number,
|
||||
|
|
|
|||
|
|
@ -17,12 +17,4 @@ describe('FileLions', () => {
|
|||
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,15 +1,14 @@
|
|||
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 { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { buildMediaFlowProxyExtractorStreamUrl, guessHeightFromPlaylist, supportsMediaFlowProxy } 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 readonly label = 'FileLions (via MediaFlow Proxy)';
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
|
||||
|
|
@ -22,19 +21,16 @@ export class FileLions extends Extractor {
|
|||
'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',
|
||||
'kinoger.be',
|
||||
'lumiawatch.top',
|
||||
'minochinos.com',
|
||||
'mivalyo.com',
|
||||
'moflix-stream.click',
|
||||
'motvy55.store',
|
||||
|
|
@ -59,38 +55,35 @@ export class FileLions extends Extractor {
|
|||
}
|
||||
|
||||
public override normalize(url: URL): URL {
|
||||
return new URL(url.href.replace('/v/', '/f/').replace('/download/', '/f/').replace('/file/', '/f/'));
|
||||
return new URL(url.href.replace('/f/', '/v/').replace('/download/', '/v/').replace('/file/', '/v/'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
const playlistUrl = await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', 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 fileUrl = new URL(url.href.replace('/v/', '/f/'));
|
||||
const html = await this.fetcher.text(ctx, fileUrl, { headers });
|
||||
|
||||
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/);
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('meta[name="description"]').attr('content') as string;
|
||||
const title = $('title').text().trim().replace(/^Watch /, '').trim();
|
||||
|
||||
return [
|
||||
{
|
||||
url: await buildMediaFlowProxyExtractorStreamUrl(ctx, this.fetcher, 'FileLions', url, headers),
|
||||
url: playlistUrl,
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
height: parseInt(heightMatch[1] as string),
|
||||
...(sizeMatch && { bytes: bytes.parse(sizeMatch[1] as string) as number }),
|
||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers: { Referer: url.href }, queueLimit: 4 }),
|
||||
...(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) }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -10,7 +10,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/'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Dead City', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/901994/'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('How to Train Your Dragon', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://fsst.online/embed/902668/'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as cheerio from 'cheerio';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class Fsst extends Extractor {
|
||||
|
|
@ -7,35 +7,36 @@ export class Fsst extends Extractor {
|
|||
|
||||
public readonly label = 'Fsst';
|
||||
|
||||
public override readonly ttl: number = 10800000; // 3h
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/fsst/);
|
||||
};
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers, noProxyHeaders: true });
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
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) => {
|
||||
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}_${meta.countryCodes?.join('_')}`,
|
||||
meta: {
|
||||
...meta,
|
||||
height: parseInt(heightAndUrlMatch[1] || heightFromFileHrefMatch[1] as string),
|
||||
title,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,4 @@ describe('HubCloud', () => {
|
|||
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,7 +1,6 @@
|
|||
import bytes from 'bytes';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { findCountryCodes, findHeight } from '../utils';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class HubCloud extends Extractor {
|
||||
|
|
@ -9,15 +8,13 @@ export class HubCloud extends Extractor {
|
|||
|
||||
public readonly label = 'HubCloud';
|
||||
|
||||
public override readonly ttl: number = 43200000; // 12h
|
||||
|
||||
public override readonly cacheVersion = 1;
|
||||
public override readonly ttl: number = 259200000; // 3d
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/hubcloud/);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const redirectHtml = await this.fetcher.text(ctx, url, { headers });
|
||||
|
|
@ -26,16 +23,12 @@ export class HubCloud extends Extractor {
|
|||
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([
|
||||
const urlResults = [
|
||||
...$('a')
|
||||
.filter((_i, el) => {
|
||||
const text = $(el).text();
|
||||
|
||||
return text.includes('FSL') && !text.includes('FSLv2');
|
||||
return text.includes('FSL') || text.includes('Download File');
|
||||
})
|
||||
.map((_i, el) => {
|
||||
const url = new URL($(el).attr('href') as string);
|
||||
|
|
@ -43,60 +36,48 @@ export class HubCloud extends Extractor {
|
|||
url,
|
||||
format: Format.unknown,
|
||||
label: `${this.label} (FSL)`,
|
||||
sourceId: `${this.id}_fsl`,
|
||||
ttl: this.ttl,
|
||||
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,
|
||||
title: $('title').text().trim(),
|
||||
},
|
||||
};
|
||||
}).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', '');
|
||||
|
||||
const url = new URL(($(el).attr('href') as string).replace('/u/', '/api/file/'));
|
||||
return {
|
||||
url,
|
||||
format: Format.unknown,
|
||||
label: `${this.label} (PixelServer)`,
|
||||
sourceId: `${this.id}_pixelserver`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
bytes: bytes.parse($('#size').text()) as number,
|
||||
extractorId: `${this.id}_pixelserver`,
|
||||
countryCodes,
|
||||
height,
|
||||
title,
|
||||
title: $('title').text().trim(),
|
||||
},
|
||||
requestHeaders: { Referer: userUrl.href },
|
||||
};
|
||||
}).toArray(),
|
||||
]);
|
||||
];
|
||||
|
||||
return this.asyncFilter(urlResults, async ({ url }) => {
|
||||
try {
|
||||
await this.fetcher.head(ctx, url);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
private async asyncFilter<T>(array: T[], asyncCallback: (item: T) => Promise<boolean>): Promise<T[]> {
|
||||
const results = await Promise.all(array.map(asyncCallback));
|
||||
|
||||
return array.filter((_, index) => results[index]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) : [];
|
||||
};
|
||||
}
|
||||
|
|
@ -17,12 +17,4 @@ describe('KinoGer', () => {
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import crypto from 'crypto';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { guessHeightFromPlaylist } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
|
|
@ -17,19 +17,11 @@ export class KinoGer extends Extractor {
|
|||
'disneycdn.net',
|
||||
'dzo.vidplayer.live',
|
||||
'filedecrypt.link',
|
||||
'filma365.strp2p.site',
|
||||
'flimmer.rpmvip.com',
|
||||
'flixfilmesonline.strp2p.site',
|
||||
'kinoger.p2pplay.pro',
|
||||
'kinoger.re',
|
||||
'moflix.rpmplay.xyz',
|
||||
'moflix.upns.xyz',
|
||||
'player.upn.one',
|
||||
'securecdn.shop',
|
||||
'shiid4u.upn.one',
|
||||
'srbe84.vidplayer.live',
|
||||
'strp2p.site',
|
||||
't1.p2pplay.pro',
|
||||
'tuktuk.rpmvid.com',
|
||||
'ultrastream.online',
|
||||
'videoland.cfd',
|
||||
|
|
@ -43,35 +35,36 @@ export class KinoGer extends Extractor {
|
|||
return new URL(`${url.origin}/api/v1/video?id=${url.hash.slice(1)}`);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
const headers = {
|
||||
'Origin': url.origin,
|
||||
'Referer': url.origin + '/',
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { 'Referer': meta.referer ?? url.href, 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' };
|
||||
|
||||
const hexData = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
const encrypted = Buffer.from(hexData.slice(0, -1), 'hex');
|
||||
const encrypted = Buffer.from(hexData, 'hex');
|
||||
const key = Buffer.from('6b69656d7469656e6d75613931316361', 'hex');
|
||||
const iv = Buffer.from('313233343536373839306f6975797472', 'hex');
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
|
||||
|
||||
const { source, title } = JSON.parse(decrypted) as { source: string; title: string };
|
||||
const { cf, title } = JSON.parse(decrypted) as { cf: string; title: string };
|
||||
|
||||
const m3u8Url = new URL(source);
|
||||
const m3u8Url = new URL(cf);
|
||||
|
||||
return [
|
||||
{
|
||||
url: m3u8Url,
|
||||
url: new URL(cf),
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers }),
|
||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.href } }),
|
||||
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,26 +1,28 @@
|
|||
import bytes from 'bytes';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class Mixdrop extends Extractor {
|
||||
public readonly id = 'mixdrop';
|
||||
|
||||
public readonly label = 'Mixdrop';
|
||||
public readonly label = 'Mixdrop (via MediaFlow Proxy)';
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
|
||||
public supports(ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/mixdrop|mixdrp|mixdroop|m1xdrop/) && supportsMediaFlowProxy(ctx);
|
||||
return null !== url.host.match(/mixdrop/) && supportsMediaFlowProxy(ctx);
|
||||
}
|
||||
|
||||
public override readonly normalize = (url: URL): URL => new URL(url.href.replace('/f/', '/e/'));
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const fileUrl = new URL(url.href.replace('/e/', '/f/'));
|
||||
const html = await this.fetcher.text(ctx, fileUrl);
|
||||
const html = await this.fetcher.text(ctx, fileUrl, { headers });
|
||||
|
||||
if (/can't find the (file|video)/.test(html)) {
|
||||
throw new NotFoundError();
|
||||
|
|
@ -33,8 +35,11 @@ export class Mixdrop extends Extractor {
|
|||
|
||||
return [
|
||||
{
|
||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url),
|
||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Mixdrop', url, headers),
|
||||
format: Format.mp4,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
bytes: bytes.parse((sizeMatch[1] as string).replace(',', '')) as number,
|
||||
|
|
|
|||
|
|
@ -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,6 +1,6 @@
|
|||
import * as cheerio from 'cheerio';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class SaveFiles extends Extractor {
|
||||
|
|
@ -18,7 +18,7 @@ export class SaveFiles extends Extractor {
|
|||
return new URL(url.href.replace('/e/', '/').replace('/d/', '/'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
|
@ -37,6 +37,9 @@ export class SaveFiles extends Extractor {
|
|||
{
|
||||
url: new URL(fileMatch[1] as string),
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
|
|
|
|||
|
|
@ -2,23 +2,23 @@ import winston from 'winston';
|
|||
import { createTestContext } from '../test';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { RgShows } from './RgShows';
|
||||
import { Soaper } from './Soaper';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new RgShows(new FetcherMock(`${__dirname}/__fixtures__/RgShows`))]);
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new Soaper(new FetcherMock(`${__dirname}/__fixtures__/Soaper`))]);
|
||||
|
||||
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();
|
||||
describe('Soaper', () => {
|
||||
test('Full Metal Jacket', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/movie_d8kdeypDY9.html'))).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('Black Mirror', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_5KDq78eGp1.html'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handle parasite 2019 (mp4)', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://api.rgshows.ru/main/movie/496243'))).toMatchSnapshot();
|
||||
test('last of us s2e3', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://soaper.live/episode_rYg3vMEDL1.html'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
60
src/extractor/Soaper.ts
Normal file
60
src/extractor/Soaper.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { guessHeightFromPlaylist } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
interface SoaperInfoResponsePartial {
|
||||
val: string;
|
||||
val_bak: string;
|
||||
}
|
||||
|
||||
export class Soaper extends Extractor {
|
||||
public readonly id = 'soaper';
|
||||
|
||||
public readonly label = 'Soaper';
|
||||
|
||||
public override readonly ttl: number = 43200000; // 12h
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string;
|
||||
|
||||
const form = new URLSearchParams();
|
||||
form.append('pass', movieOrEpisodeId);
|
||||
form.append('param', '');
|
||||
form.append('extra', '');
|
||||
form.append('e2', '');
|
||||
form.append('server', '0');
|
||||
|
||||
const response = await this.fetcher.textPost(
|
||||
ctx,
|
||||
new URL(url.pathname.includes('episode') ? '/home/index/GetEInfoAjax' : '/home/index/GetMInfoAjax', url.origin),
|
||||
form.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Referer': url.origin,
|
||||
},
|
||||
},
|
||||
);
|
||||
const jsonResponse = JSON.parse(response) as SoaperInfoResponsePartial;
|
||||
|
||||
const m3u8Url = new URL(jsonResponse['val'], url.origin);
|
||||
|
||||
return [
|
||||
{
|
||||
url: m3u8Url,
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.href } }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -13,8 +13,4 @@ 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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class StreamEmbed extends Extractor {
|
||||
|
|
@ -7,27 +6,24 @@ export class StreamEmbed extends Extractor {
|
|||
|
||||
public readonly label = 'StreamEmbed';
|
||||
|
||||
public override readonly ttl: number = 259200000; // 3d
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/bullstream|mp4player|watch\.gxplayer/);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
if (/Video is not ready/.test(html)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const video = JSON.parse((html.match(/video ?= ?(.*);/) as string[])[1] as string);
|
||||
|
||||
return [
|
||||
{
|
||||
url: new URL(`/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}`, url.origin),
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
height: parseInt(JSON.parse(video.quality)[0]),
|
||||
|
|
|
|||
|
|
@ -1,68 +1,46 @@
|
|||
import bytes from 'bytes';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import {
|
||||
buildMediaFlowProxyExtractorRedirectUrl,
|
||||
supportsMediaFlowProxy,
|
||||
} from '../utils';
|
||||
import { guessSizeFromMp4 } from '../utils/size';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class Streamtape extends Extractor {
|
||||
public readonly id = 'streamtape';
|
||||
|
||||
public readonly label = 'Streamtape';
|
||||
|
||||
public override viaMediaFlowProxy = true;
|
||||
public readonly label = 'Streamtape (via MediaFlow Proxy)';
|
||||
|
||||
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/'));
|
||||
return new URL(url.href.replace('/v/', '/e/'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
// Only needed to properly find non-existing files via 404 response
|
||||
await this.fetcher.text(ctx, new URL(url.href.replace('/v/', '/e/')), { headers });
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
const sizeMatch = html.match(/([\d.]+ ?[GM]B)/) as string[];
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('meta[name="og:title"]').attr('content') as string;
|
||||
|
||||
const mp4Url = buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers);
|
||||
|
||||
return [
|
||||
{
|
||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Streamtape', url, headers),
|
||||
url: mp4Url,
|
||||
format: Format.mp4,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
bytes: bytes.parse(sizeMatch[1] as string) as number,
|
||||
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url, { queueLimit: 4 }),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import bytes from 'bytes';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { extractUrlFromPacked, guessHeightFromPlaylist } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export class SuperVideo extends Extractor {
|
|||
return new URL(url.href.replace('/e/', '/').replace('/k/', '/').replace('/embed-', '/'));
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
|
@ -33,29 +33,30 @@ export class SuperVideo extends Extractor {
|
|||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const playlistUrl = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
|
||||
const playlistHeaders = { Referer: 'https://supervideo.cc/' };
|
||||
const m3u8Url = extractUrlFromPacked(html, [/sources:\[{file:"(.*?)"/]);
|
||||
|
||||
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 });
|
||||
: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: url.href } });
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const title = $('.download__title').text().trim();
|
||||
|
||||
return [
|
||||
{
|
||||
url: playlistUrl,
|
||||
url: m3u8Url,
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
...(size && { bytes: size }),
|
||||
...(height && { height }),
|
||||
},
|
||||
requestHeaders: playlistHeaders,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import * as cheerio from 'cheerio';
|
||||
import { NotFoundError } from '../error';
|
||||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { buildMediaFlowProxyExtractorRedirectUrl, supportsMediaFlowProxy } from '../utils';
|
||||
import { guessSizeFromMp4 } from '../utils/size';
|
||||
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 viaMediaFlowProxy = true;
|
||||
|
||||
|
|
@ -19,8 +20,10 @@ export class Uqload extends Extractor {
|
|||
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);
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
||||
if (/File Not Found/.test(html)) {
|
||||
throw new NotFoundError();
|
||||
|
|
@ -31,13 +34,19 @@ export class Uqload extends Extractor {
|
|||
const $ = cheerio.load(html);
|
||||
const title = $('h1').text().trim();
|
||||
|
||||
const mp4Url = buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url, headers);
|
||||
|
||||
return [
|
||||
{
|
||||
url: buildMediaFlowProxyExtractorRedirectUrl(ctx, 'Uqload', url),
|
||||
url: mp4Url,
|
||||
format: Format.mp4,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
title,
|
||||
bytes: await guessSizeFromMp4(ctx, this.fetcher, mp4Url, { headers: { Referer: url.href }, queueLimit: 4 }),
|
||||
...(heightMatch && {
|
||||
height: parseInt(heightMatch[1] as string),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,57 @@
|
|||
import { MockAgent, setGlobalDispatcher } from 'undici';
|
||||
import winston from 'winston';
|
||||
import { createTestContext } from '../test';
|
||||
import { FetcherMock } from '../utils';
|
||||
import { Fetcher, FetcherMock } from '../utils';
|
||||
import { ExtractorRegistry } from './ExtractorRegistry';
|
||||
import { VidSrc } from './VidSrc';
|
||||
|
||||
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['vidsrc-embed.ru'])]);
|
||||
const extractorRegistry = new ExtractorRegistry(logger, [new VidSrc(new FetcherMock(`${__dirname}/__fixtures__/VidSrc`), ['xyz'])]);
|
||||
|
||||
const ctx = createTestContext();
|
||||
|
||||
describe('VidSrc', () => {
|
||||
test('Full Metal Jacket', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc-embed.ru/embed/movie/tt0093058'))).toMatchSnapshot();
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt0093058'))).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'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('not found', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vidsrc.xyz/embed/movie/tt35628853'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('rate limit issues are retried and fail if no tlds are left', async () => {
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true });
|
||||
mockAgent.disableNetConnect();
|
||||
setGlobalDispatcher(mockAgent);
|
||||
|
||||
mockAgent.get('https://vidsrc.xyz')
|
||||
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
|
||||
mockAgent.get('https://vidsrc.net')
|
||||
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(429);
|
||||
|
||||
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
|
||||
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
|
||||
|
||||
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), {})).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('blocking issues are retried and fail if no tlds are left', async () => {
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true });
|
||||
mockAgent.disableNetConnect();
|
||||
setGlobalDispatcher(mockAgent);
|
||||
|
||||
mockAgent.get('https://vidsrc.xyz')
|
||||
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(403);
|
||||
mockAgent.get('https://vidsrc.net')
|
||||
.intercept({ path: '/embed/movie/tt33043892/1/1' }).reply(403);
|
||||
|
||||
const fetcher = new Fetcher(winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }));
|
||||
const vidSrc = new VidSrc(fetcher, ['net', 'xyz']);
|
||||
|
||||
expect(await vidSrc.extract(ctx, new URL('https://vidsrc.xyz/embed/movie/tt33043892/1/1'), {})).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 { BlockedError, NotFoundError, TooManyRequestsError } from '../error';
|
||||
import { Context, Format, Meta, NonEmptyArray, UrlResult } from '../types';
|
||||
import { Fetcher, guessHeightFromPlaylist } from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
|
|
@ -11,47 +12,43 @@ export class VidSrc extends Extractor {
|
|||
|
||||
public override readonly ttl: number = 10800000; // 3h
|
||||
|
||||
private readonly domains: NonEmptyArray<string>;
|
||||
private readonly tlds: NonEmptyArray<string>;
|
||||
|
||||
public constructor(fetcher: Fetcher, domains: NonEmptyArray<string>) {
|
||||
public constructor(fetcher: Fetcher, tlds: NonEmptyArray<string>) {
|
||||
super(fetcher);
|
||||
|
||||
this.domains = domains;
|
||||
this.tlds = tlds;
|
||||
}
|
||||
|
||||
public supports(_ctx: Context, url: URL): boolean {
|
||||
return null !== url.host.match(/vidsrc|vsrc/);
|
||||
return null !== url.host.match(/vidsrc/);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
// While this is a crappy thing to do, they seem to be blocking overly strict IMO
|
||||
const randomIp = `${Math.floor(Math.random() * 223) + 1}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}.${Math.floor(Math.random() * 256)}`;
|
||||
const newCtx = { ...ctx, ip: randomIp };
|
||||
|
||||
return this.extractUsingRandomDomain(newCtx, url, meta, [...this.domains]);
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
return this.extractUsingRandomTld(ctx, url, meta, [...this.tlds]);
|
||||
};
|
||||
|
||||
private async extractUsingRandomDomain(ctx: Context, url: URL, meta: Meta, domains: string[]): Promise<InternalUrlResult[]> {
|
||||
const domainIndex = Math.floor(Math.random() * domains.length);
|
||||
const [domain] = domains.splice(domainIndex, 1) as [string];
|
||||
private async extractUsingRandomTld(ctx: Context, url: URL, meta: Meta, tlds: string[]): Promise<UrlResult[]> {
|
||||
const tldIndex = Math.floor(Math.random() * tlds.length);
|
||||
const [tld] = tlds.splice(tldIndex, 1) as [string];
|
||||
|
||||
const newUrl = new URL(url);
|
||||
newUrl.hostname = domain;
|
||||
const hostnameParts = newUrl.hostname.split('.');
|
||||
hostnameParts[hostnameParts.length - 1] = tld;
|
||||
newUrl.hostname = hostnameParts.join('.');
|
||||
|
||||
let html: string;
|
||||
try {
|
||||
html = await this.fetcher.text(ctx, newUrl, { queueLimit: 1 });
|
||||
} catch (error) {
|
||||
/* istanbul ignore next */
|
||||
if (domains.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
|
||||
return this.extractUsingRandomDomain(ctx, url, meta, domains);
|
||||
if (tlds.length && (error instanceof TooManyRequestsError || error instanceof BlockedError)) {
|
||||
return this.extractUsingRandomTld(ctx, url, meta, tlds);
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
throw error;
|
||||
}
|
||||
|
||||
const $ = cheerio.load(html.replace('<!--', '').replace('-->', '')); // server HTML is commented-out
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const iframeUrl = new URL(($('#player_iframe').attr('src') as string).replace(/^\/\//, 'https://'));
|
||||
const title = $('title').text().trim();
|
||||
|
|
@ -67,13 +64,19 @@ export class VidSrc extends Extractor {
|
|||
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 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,
|
||||
sourceId: `${this.id}_${slugify(serverName)}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, m3u8Url, { headers: { Referer: iframeUrl.href } }),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ 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({ de: 'on', en: 'on', it: 'on' });
|
||||
|
||||
describe('VixSrc', () => {
|
||||
test('Full Metal Jacket', async () => {
|
||||
|
|
@ -18,13 +18,9 @@ describe('VixSrc', () => {
|
|||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Rental Family', async () => {
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/movie/1208348'))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Black Mirror is excluded if no matching language was found', async () => {
|
||||
const ctx = createTestContext({ de: 'on' });
|
||||
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/3'))).toMatchSnapshot();
|
||||
expect(await extractorRegistry.handle(ctx, new URL('https://vixsrc.to/tv/42009/4/2'))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { Context, CountryCode, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { CustomRequestConfig, guessHeightFromPlaylist, hasMultiEnabled, iso639FromCountryCode } from '../utils';
|
||||
import { Context, CountryCode, Format, Meta, UrlResult } from '../types';
|
||||
import {
|
||||
CustomRequestInit,
|
||||
guessHeightFromPlaylist,
|
||||
iso639FromCountryCode,
|
||||
} from '../utils';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class VixSrc extends Extractor {
|
||||
|
|
@ -13,7 +17,7 @@ export class VixSrc extends Extractor {
|
|||
return null !== url.host.match(/vixsrc/);
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url);
|
||||
|
|
@ -28,29 +32,27 @@ export class VixSrc extends Extractor {
|
|||
playlistUrl.searchParams.append('expires', expiresMatch[1] as string);
|
||||
playlistUrl.searchParams.append('h', '1');
|
||||
|
||||
const countryCodes = meta.countryCodes ?? [CountryCode.multi, ...(await this.determineCountryCodesFromPlaylist(ctx, playlistUrl, { headers }))];
|
||||
|
||||
if (!hasMultiEnabled(ctx.config) && !countryCodes.some(countryCode => countryCode in ctx.config)) {
|
||||
return [];
|
||||
}
|
||||
const countryCodes = await this.determineCountryCodesFromPlaylist(ctx, playlistUrl, { headers });
|
||||
|
||||
return [
|
||||
{
|
||||
url: playlistUrl,
|
||||
format: Format.hls,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
meta: {
|
||||
...meta,
|
||||
countryCodes,
|
||||
height: meta.height ?? await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
|
||||
height: await guessHeightFromPlaylist(ctx, this.fetcher, playlistUrl, { headers }),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestConfig): Promise<CountryCode[]> {
|
||||
private async determineCountryCodesFromPlaylist(ctx: Context, playlistUrl: URL, init?: CustomRequestInit): Promise<CountryCode[]> {
|
||||
const playlist = await this.fetcher.text(ctx, playlistUrl, init);
|
||||
|
||||
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,4 +1,4 @@
|
|||
import { Context, Format, InternalUrlResult, Meta } from '../types';
|
||||
import { Context, Format, Meta, UrlResult } from '../types';
|
||||
import { Extractor } from './Extractor';
|
||||
|
||||
export class YouTube extends Extractor {
|
||||
|
|
@ -12,7 +12,7 @@ export class YouTube extends Extractor {
|
|||
return null !== url.host.match(/youtube/) && url.searchParams.has('v');
|
||||
}
|
||||
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<InternalUrlResult[]> {
|
||||
protected async extractInternal(ctx: Context, url: URL, meta: Meta): Promise<UrlResult[]> {
|
||||
const headers = { Referer: meta.referer ?? url.href };
|
||||
|
||||
const html = await this.fetcher.text(ctx, url, { headers });
|
||||
|
|
@ -24,6 +24,9 @@ export class YouTube extends Extractor {
|
|||
url,
|
||||
format: Format.unknown,
|
||||
ytId: url.searchParams.get('v') as string,
|
||||
label: this.label,
|
||||
sourceId: `${this.id}_${meta.countryCodes?.join('_')}`,
|
||||
ttl: this.ttl,
|
||||
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,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,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>
|
||||
|
||||
6
src/extractor/__fixtures__/FileLions/https:mediaflow.test.org-3ae485009fb31845c4840cef8ccac74b
generated
Normal file
6
src/extractor/__fixtures__/FileLions/https:mediaflow.test.org-3ae485009fb31845c4840cef8ccac74b
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#EXTM3U
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="Deutsch",LANGUAGE="de",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-a1.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="English",LANGUAGE="en",AUTOSELECT=NO,DEFAULT=NO,CHANNELS="2",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-a2.m3u8"
|
||||
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2862563,RESOLUTION=1920x1080,FRAME-RATE=25.000,CODECS="avc1.640028,mp4a.40.2",VIDEO-RANGE=SDR,AUDIO="audio0"
|
||||
https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Findex-v1-a1.m3u8
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=123231,RESOLUTION=1920x1080,CODECS="avc1.640028",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Fkinoger.com&d=https%3A%2F%2F6dkdxt1udlqhbho.ovaltinecdn.com%2Fhls2%2F01%2F01552%2Ftah5znapz3e5_n%2Fiframes-v1-a1.m3u8",VIDEO-RANGE=SDR
|
||||
4
src/extractor/__fixtures__/FileLions/https:mediaflow.test.org-efb89e5a2d68b05cdb412bff3f87e8b7
generated
Normal file
4
src/extractor/__fixtures__/FileLions/https:mediaflow.test.org-efb89e5a2d68b05cdb412bff3f87e8b7
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#EXTM3U
|
||||
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1242387,RESOLUTION=1728x720,FRAME-RATE=23.974,CODECS="avc1.640020,mp4a.40.2",VIDEO-RANGE=SDR
|
||||
https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Ffilelions.to%2Fv%2Ftyn45apubte2&d=https%3A%2F%2Frnzt2t4xvku08.acek-cdn.com%2Fhls2%2F01%2F05594%2Ftyn45apubte2_n%2Findex-v1-a1.m3u8%3Ft%3D8y6hK40boUynXnkyW5s63kFaWpt6_A5UcG5LBUX3j_4%26s%3D1756131487%26e%3D129600%26f%3D27974684%26srv%3DF6vE2P5Wjncn%26i%3D0.4%26sp%3D500%26p1%3DF6vE2P5Wjncn%26p2%3DF6vE2P5Wjncn%26asn%3D31898
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=69531,RESOLUTION=1728x720,CODECS="avc1.640020",URI="https://mediaflow.test.org/proxy/hls/manifest.m3u8?api_password=test&h_user-agent=Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F136.0.0.0+Safari%2F537.36&h_referer=https%3A%2F%2Ffilelions.to%2Fv%2Ftyn45apubte2&d=https%3A%2F%2Frnzt2t4xvku08.acek-cdn.com%2Fhls2%2F01%2F05594%2Ftyn45apubte2_n%2Fiframes-v1-a1.m3u8%3Ft%3D8y6hK40boUynXnkyW5s63kFaWpt6_A5UcG5LBUX3j_4%26s%3D1756131487%26e%3D129600%26f%3D27974684%26srv%3DF6vE2P5Wjncn%26i%3D0.4%26sp%3D500%26p1%3DF6vE2P5Wjncn%26p2%3DF6vE2P5Wjncn%26asn%3D31898",VIDEO-RANGE=SDR
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,57 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
(function () {
|
||||
// Get all script tags in the <head>
|
||||
const scripts = document.head.querySelectorAll('script');
|
||||
|
||||
// Iterate through the scripts to find the one injecting the Proxy
|
||||
scripts.forEach((script) => {
|
||||
if (script.innerHTML.includes('window.open=new Proxy(window.open')) {
|
||||
console.log('Detected and removing injected script:', script);
|
||||
script.remove(); // Remove the script from the <head>
|
||||
}
|
||||
});
|
||||
|
||||
// Restore the original window.open functionality (optional)
|
||||
const originalOpen = Function.prototype.apply.bind(window.open);
|
||||
window.open = function (...args) {
|
||||
console.log('Restored window.open called:', args);
|
||||
return originalOpen(this, args);
|
||||
};
|
||||
|
||||
console.log('Injected script removed and window.open restored.');
|
||||
})();
|
||||
</script>
|
||||
<title>Not Found</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/404.css?v=2">
|
||||
<link rel="apple-touch-icon" href="https://filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="icon" href="https://filemoon.to/assets/images/favicon/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
<script src="/ad.js" type="text/javascript"></script>
|
||||
<div id="adb" class="ad doubleclick skyscraper" style="height:1px;width:1px;position:absolute;left:-999px;top:-999px;">ad</div>
|
||||
<script>
|
||||
window.fmn7i8zodwjqr9 = { token: "bed5d8dfa2e30a9a04", adblock: false, blockadblock: true, fileid: "n7i8zodwjqr9", ampallow: false };
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="error e404">
|
||||
<div class="error-footer">
|
||||
<h1>Page not found</h1>
|
||||
<a href="/" class="button">Return home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Watch tt0307453</title>
|
||||
<meta name="description" content="">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#FFF">
|
||||
|
||||
<link rel="apple-touch-icon" href="//filemoon.to/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="icon" href="//filemoon.to/assets/images/favicon/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//filemoon.to/assets/css/front.css?v=51">
|
||||
<META NAME="description" CONTENT="Watch video tt0307453">
|
||||
<META NAME="keywords" CONTENT="tt0307453">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="is-home d-flex flex-column justify-content-between ">
|
||||
<header class="header py-3 py-lg-4">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between justify-content-lg-start">
|
||||
<div class="col-auto d-lg-none">
|
||||
<button class="btn text-muted ssm-toggle-nav px-1" type="button">
|
||||
<i class="icon icon-menu icon-size-22 text-muted"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/" class="logo d-block"><img src="//filemoon.to/assets/images/logo.svg" alt=""></a>
|
||||
</div>
|
||||
<div class="col-auto d-none d-lg-block flex-lg-grow-1">
|
||||
<nav class="nav ml-lg-3">
|
||||
<a class="nav-link" href="/premium">Premium</a>
|
||||
<a class="nav-link" href="/affiliate">Make Money</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-none d-lg-flex">
|
||||
|
||||
<a href="/login" class="btn btn-dark sign-btn rounded-pill mr-2">Log In</a>
|
||||
<a href="/register" class="btn btn-success sign-btn rounded-pill">Sign Up</a>
|
||||
|
||||
</div>
|
||||
<div class="d-lg-none">
|
||||
<button class="btn text-muted usrmobile-toggle-nav px-1" type="button">
|
||||
<i class="icon icon-user icon-size-18 text-muted"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<script src="https://filemoon.to/js/jquery.cookie.js"></script>
|
||||
<script>
|
||||
$.cookie('file_id', '48721931', { expires: 10 });
|
||||
$.cookie('aff', '89667', { expires: 10 });
|
||||
$.cookie('ref_url', 'https://filemoon.to/d/ugj7f1kq94p1', { expires: 10 });
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src='/js/ls.js'></script>
|
||||
<main class="flex-grow-1 a-bean">
|
||||
<div class="container mt-5">
|
||||
<div class="mb-4 mb-lg-5 sm-bg px-4 py-5 p-lg-0 mx-lg-5 rounded-lg">
|
||||
<div class="row justify-content-between align-items-center text-muted mb-4 mb-lg-5">
|
||||
<div class="col-auto mb-2">
|
||||
<h3 class="text-light mb-0 font-weight-bold">tt0307453</h3>
|
||||
</div>
|
||||
<div class="col-auto mb-2"> Uploaded: <span class="text-success">2025-08-24 00:12:36</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 mb-lg-5 ">
|
||||
<div class="position-relative">
|
||||
|
||||
<div class="bimg-top text-center">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 640px;" id="iframe-holder">
|
||||
|
||||
<iframe src="https://ico3c.com/bkg/ugj7f1kq94p1" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" width="100%" height="100%" allowfullscreen></iframe>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="bimg-bottom text-center">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-4 mb-lg-5 text-center">
|
||||
<a href="/download/ugj7f1kq94p1" class="btn btn-success px-5">Download</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#vplayer {padding-top: 56.25%;}
|
||||
.bimg-top,
|
||||
.bimg-bottom {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-ms-align-items: center;
|
||||
align-items: center;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.bimg-top div,
|
||||
.bimg-bottom div {
|
||||
margin: .5rem;
|
||||
}
|
||||
@media(min-width:1600px){
|
||||
.bimg-top,
|
||||
.bimg-bottom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bimg-top {
|
||||
left: -310px;
|
||||
}
|
||||
.bimg-bottom {
|
||||
right: -310px;
|
||||
}
|
||||
.bimg-top div,
|
||||
.bimg-bottom div {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
}
|
||||
@media(min-width:1800px){
|
||||
.bimg-top {
|
||||
left: -350px;
|
||||
}
|
||||
.bimg-bottom {
|
||||
right: -350px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<footer class="footer py-4 py-lg-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between">
|
||||
<div class="col-auto my-1">
|
||||
<div class="text-muted"> FileMoon © 2022 </div>
|
||||
</div>
|
||||
<div class="col-auto my-1">
|
||||
<nav class="nav">
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/">Home</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/tos">Terms of Service</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/privacy">Privacy Policy</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/copyright">Copyright Policy</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/affiliate">Make Money</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/api">API Documentation</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="https://status.filemoon.sx">Server Status</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="ssm">
|
||||
<nav class="nav ssm-nav flex-column">
|
||||
<a class="nav-link ssm-nav-link active" href="/"><i class="icon icon-home icon-size-16 ssm-nav-icon mr-3"></i>Home</a>
|
||||
<a class="nav-link ssm-nav-link" href="/dashboard"><i class="icon icon-account icon-size-16 ssm-nav-icon mr-3"></i>My account</a>
|
||||
<a class="nav-link ssm-nav-link" href="/premium"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Premium</a>
|
||||
<a class="nav-link ssm-nav-link" href="/affiliate"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Make Money</a>
|
||||
</nav>
|
||||
<hr class="mx-3">
|
||||
<nav class="nav flex-column ssm-nav">
|
||||
<a class="nav-link ssm-nav-link" href="/tos"><i class="icon icon-pen icon-size-16 ssm-nav-icon mr-3"></i>Terms of Service</a>
|
||||
<a class="nav-link ssm-nav-link" href="/privacy"><i class="icon icon-key icon-size-16 ssm-nav-icon mr-3"></i>Privacy Policy</a>
|
||||
<a class="nav-link ssm-nav-link" href="/api"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>API Documentation</a>
|
||||
<a class="nav-link ssm-nav-link" href="https://status.filemoon.sx"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>Server Status</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="ssm-overlay ssm-toggle-nav"></div>
|
||||
<div class="usrmobile p-5 usrmobile-toggle-nav">
|
||||
|
||||
<a href="/login" class="btn btn-success btn-success-custom btn-lg btn-block text-uppercase shadow">Sign in</a>
|
||||
<a href="/register" class="btn btn-outline-success btn-lg btn-block mb-2 text-uppercase">Sign up</a>
|
||||
|
||||
</div>
|
||||
<script src="//filemoon.to/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="//filemoon.to/assets/js/jquery.slideandswipe.min.js"></script>
|
||||
<script src="//filemoon.to/assets/js/jquery.touchSwipe.min.js"></script>
|
||||
<script src="//filemoon.to/assets/js/front.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
{"destination_url":"https://be7713.rcr82.waw05.i8yz83pn.com/hls2/05/09711/aeuhexjcoysl_x/master.m3u8?t=fiqqJThcgnrc77etu-nLPLodbF1UeqY5Cou0vvnLIB0&s=1765556185&e=10800&f=48721931&srv=1050&asn=13335&sp=5500&p=","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://ico3c.com/bkg/ugj7f1kq94p1"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"destination_url":"https://be7713.rcr82.waw05.i8yz83pn.com/hls2/01/09665/wkhcbggdxf1d_h/master.m3u8?t=359izbQ_L-sOHaIjj275YKdBDyiPzUi3ETfbNoN9bzU&s=1759754698&e=10800&f=48329866&srv=1075&asn=13335&sp=4000&p=","request_headers":{"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36","referer":"https://z1ekv717.fun/d/wkhcbggdxf1d"},"mediaflow_proxy_url":"https://mediaflow.test.org/proxy/hls/manifest.m3u8","query_params":{"api_password":"test"}}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,226 +0,0 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Watch alien earth s01e01 1080p web h264 successfulcrab</title>
|
||||
<meta name="description" content="">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#FFF">
|
||||
|
||||
<link rel="apple-touch-icon" href="//z1ekv717.fun/assets/images/favicon/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="icon" href="//z1ekv717.fun/assets/images/favicon/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" href="//z1ekv717.fun/assets/images/favicon/favicon-16x16.png" sizes="16x16">
|
||||
<link rel="icon" href="//z1ekv717.fun/assets/images/favicon/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//z1ekv717.fun/assets/css/front.css?v=51">
|
||||
<META NAME="description" CONTENT="Watch video alien earth s01e01 1080p web h264 successfulcrab">
|
||||
<META NAME="keywords" CONTENT="alien, earth, s01e01, 1080p, web, h264, successfulcrab">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="is-home d-flex flex-column justify-content-between ">
|
||||
<header class="header py-3 py-lg-4">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between justify-content-lg-start">
|
||||
<div class="col-auto d-lg-none">
|
||||
<button class="btn text-muted ssm-toggle-nav px-1" type="button">
|
||||
<i class="icon icon-menu icon-size-22 text-muted"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="/" class="logo d-block"><img src="//z1ekv717.fun/assets/images/logo.svg" alt=""></a>
|
||||
</div>
|
||||
<div class="col-auto d-none d-lg-block flex-lg-grow-1">
|
||||
<nav class="nav ml-lg-3">
|
||||
<a class="nav-link" href="/premium">Premium</a>
|
||||
<a class="nav-link" href="/affiliate">Make Money</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-none d-lg-flex">
|
||||
|
||||
<a href="/login" class="btn btn-dark sign-btn rounded-pill mr-2">Log In</a>
|
||||
<a href="/register" class="btn btn-success sign-btn rounded-pill">Sign Up</a>
|
||||
|
||||
</div>
|
||||
<div class="d-lg-none">
|
||||
<button class="btn text-muted usrmobile-toggle-nav px-1" type="button">
|
||||
<i class="icon icon-user icon-size-18 text-muted"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
<script src="https://filemoon.to/js/jquery.cookie.js"></script>
|
||||
<script>
|
||||
$.cookie('file_id', '48329866', { expires: 10 });
|
||||
$.cookie('aff', '31148', { expires: 10 });
|
||||
$.cookie('ref_url', 'https://z1ekv717.fun/d/wkhcbggdxf1d', { expires: 10 });
|
||||
</script>
|
||||
<script type="text/javascript" src='/js/ls.js'></script>
|
||||
<main class="flex-grow-1 a-bean">
|
||||
<div class="container mt-5">
|
||||
<div class="mb-4 mb-lg-5 sm-bg px-4 py-5 p-lg-0 mx-lg-5 rounded-lg">
|
||||
<div class="row justify-content-between align-items-center text-muted mb-4 mb-lg-5">
|
||||
<div class="col-auto mb-2">
|
||||
<h3 class="text-light mb-0 font-weight-bold">alien earth s01e01 1080p web h264-successfulcrab</h3>
|
||||
</div>
|
||||
<div class="col-auto mb-2"> Uploaded: <span class="text-success">2025-08-13 00:08:29</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 mb-lg-5 ">
|
||||
<div class="position-relative">
|
||||
|
||||
<div class="bimg-top text-center">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 640px;" id="iframe-holder">
|
||||
|
||||
<iframe src="https://pqham.com/bkg/wkhcbggdxf1d" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" width="100%" height="100%" allowfullscreen></iframe>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="bimg-bottom text-center">
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-4 mb-lg-5 text-center">
|
||||
<a href="/download/wkhcbggdxf1d" class="btn btn-success px-5">Download</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#vplayer {padding-top: 56.25%;}
|
||||
.bimg-top,
|
||||
.bimg-bottom {
|
||||
display: -webkit-flex;
|
||||
display: -ms-flex;
|
||||
display: flex;
|
||||
-ms-align-items: center;
|
||||
align-items: center;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.bimg-top div,
|
||||
.bimg-bottom div {
|
||||
margin: .5rem;
|
||||
}
|
||||
@media(min-width:1600px){
|
||||
.bimg-top,
|
||||
.bimg-bottom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
.bimg-top {
|
||||
left: -310px;
|
||||
}
|
||||
.bimg-bottom {
|
||||
right: -310px;
|
||||
}
|
||||
.bimg-top div,
|
||||
.bimg-bottom div {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
}
|
||||
@media(min-width:1800px){
|
||||
.bimg-top {
|
||||
left: -350px;
|
||||
}
|
||||
.bimg-bottom {
|
||||
right: -350px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<footer class="footer py-4 py-lg-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between">
|
||||
<div class="col-auto my-1">
|
||||
<div class="text-muted"> FileMoon © 2022 </div>
|
||||
</div>
|
||||
<div class="col-auto my-1">
|
||||
<nav class="nav">
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/">Home</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/tos">Terms of Service</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/privacy">Privacy Policy</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/copyright">Copyright Policy</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/affiliate">Make Money</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="/api">API Documentation</a>
|
||||
<a class="nav-link text-muted footer-link px-0 mr-4" href="https://status.filemoon.sx">Server Status</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="ssm">
|
||||
<nav class="nav ssm-nav flex-column">
|
||||
<a class="nav-link ssm-nav-link active" href="/"><i class="icon icon-home icon-size-16 ssm-nav-icon mr-3"></i>Home</a>
|
||||
<a class="nav-link ssm-nav-link" href="/dashboard"><i class="icon icon-account icon-size-16 ssm-nav-icon mr-3"></i>My account</a>
|
||||
<a class="nav-link ssm-nav-link" href="/premium"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Premium</a>
|
||||
<a class="nav-link ssm-nav-link" href="/affiliate"><i class="icon icon-money icon-size-16 ssm-nav-icon mr-3"></i>Make Money</a>
|
||||
</nav>
|
||||
<hr class="mx-3">
|
||||
<nav class="nav flex-column ssm-nav">
|
||||
<a class="nav-link ssm-nav-link" href="/tos"><i class="icon icon-pen icon-size-16 ssm-nav-icon mr-3"></i>Terms of Service</a>
|
||||
<a class="nav-link ssm-nav-link" href="/privacy"><i class="icon icon-key icon-size-16 ssm-nav-icon mr-3"></i>Privacy Policy</a>
|
||||
<a class="nav-link ssm-nav-link" href="/api"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>API Documentation</a>
|
||||
<a class="nav-link ssm-nav-link" href="https://status.filemoon.sx"><i class="icon icon-note icon-size-16 ssm-nav-icon mr-3"></i>Server Status</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="ssm-overlay ssm-toggle-nav"></div>
|
||||
<div class="usrmobile p-5 usrmobile-toggle-nav">
|
||||
|
||||
<a href="/login" class="btn btn-success btn-success-custom btn-lg btn-block text-uppercase shadow">Sign in</a>
|
||||
<a href="/register" class="btn btn-outline-success btn-lg btn-block mb-2 text-uppercase">Sign up</a>
|
||||
|
||||
</div>
|
||||
<script src="//z1ekv717.fun/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="//z1ekv717.fun/assets/js/jquery.slideandswipe.min.js"></script>
|
||||
<script src="//z1ekv717.fun/assets/js/jquery.touchSwipe.min.js"></script>
|
||||
<script src="//z1ekv717.fun/assets/js/front.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"server":"nginx","date":"Tue, 06 Jan 2026 15:18:15 GMT","content-type":"text/html; charset=UTF-8","connection":"close","referrer-policy":"strict-origin-when-cross-origin","location":"https://a41.filevideo1.com/remote_control.php?time=1767712695&cv=a5df8582e6558fcf6bc25d7f02e5e646&lr=0&cv2=81173262e5b3e34fa9ebf67326ca98a5&file=%2Fvideos%2F948000%2F948429%2F948429_720p.mp4&cv3=2d8ee1a66c0ca6ce75d84c67a50a70e9&cv4=6974d60c75ea0485c4f2a71b164a21ce"}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue