Merge branch 'dev' into dev

This commit is contained in:
zisra
2023-10-03 17:44:10 -05:00
committed by GitHub
57 changed files with 20245 additions and 626 deletions

4
.docs/.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
dist
node_modules
.output
.nuxt

8
.docs/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: '@nuxt/eslint-config',
rules: {
'vue/max-attributes-per-line': 'off',
'vue/multi-word-component-names': 'off'
}
}

14
.docs/.gitignore vendored Normal file → Executable file
View File

@@ -1,3 +1,13 @@
node_modules node_modules
.vitepress/cache *.iml
.vitepress/dist .idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output
.nuxt

View File

@@ -1,28 +0,0 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
title: "MW provider docs",
description: "Documentation for @movie-web/providers",
srcDir: "src",
themeConfig: {
nav: [
{ text: 'Home', link: '/' },
{ text: 'Get Started', link: '/get-started/start' },
{ text: 'Reference', link: '/reference/start' }
],
sidebar: [
{
text: 'Examples',
items: [
{ text: 'Markdown Examples', link: '/markdown-examples' },
{ text: 'Runtime API Examples', link: '/api-examples' }
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/movie-web/providers' }
]
}
})

17
.docs/app.config.ts Normal file
View File

@@ -0,0 +1,17 @@
export default defineAppConfig({
docus: {
title: '@movie-web/providers',
description: 'For all your media scraping needs',
socials: {
github: 'movie-web/providers',
},
image: '',
aside: {
level: 0,
exclude: [],
},
header: {
logo: false,
},
},
});

View File

@@ -0,0 +1,3 @@
code > span {
white-space: pre;
}

51
.docs/content/0.index.md Normal file
View File

@@ -0,0 +1,51 @@
---
title: "@movie-web/providers | For all your media scraping needs"
navigation: false
layout: page
---
::block-hero
---
cta:
- Get Started
- /guide/usage
secondary:
- Open on GitHub →
- https://github.com/movie-web/providers
snippet: npm i @movie-web/providers
---
#title
@movie-web/providers
#description
Easily scrape all sorts of media sites for content
::
::card-grid
#title
What's included
#root
:ellipsis
#default
::card{icon="vscode-icons:file-type-light-json"}
#title
Scrape popular streaming websites.
#description
Don't settle for just one media site for you content, use everything that's available.
::
::card{icon="codicon:source-control"}
#title
Multi-platform.
#description
Scrape from browser or server, whichever you prefer.
::
::card{icon="logos:typescript-icon-round"}
#title
Easy to use.
#description
Get started with scraping your favourite media sites with just 5 lines of code. Fully typed of course.
::
::

View File

@@ -0,0 +1,53 @@
# Usage
Let's get started with `@movie-web/providers`. First lets install the package.
::code-group
```bash [NPM]
npm install @movie-web/providers
```
```bash [Yarn]
yarn add @movie-web/providers
```
```bash [PNPM]
pnpm install @movie-web/providers
```
::
## Scrape your first item
To get started with scraping on the **server**, first you have to make an instance of the providers.
```ts
import { makeProviders, makeDefaultFetcher, targets } from '@movie-web/providers';
// this is how the library will make http requests
const myFetcher = makeDefaultFetcher(fetch);
// make an instance of the providers library
const providers = makeProviders({
fetcher: myFetcher,
// will be played on a native video player
target: targets.NATIVE
})
```
Perfect, now we can start scraping a stream:
```ts [index.ts (server)]
// fetch some data from TMDB
const media = {
type: 'movie',
title: "Hamilton",
releaseYear: 2020,
tmdbId: "556574"
}
const output = await providers.runAll({
media: media
})
if (!output) console.log("No stream found")
console.log(`stream url: ${output.stream.playlist}`)
```

View File

@@ -0,0 +1,13 @@
# Targets
When making an instance of the library using `makeProviders()`. It will immediately require choosing a target.
::alert{type="info"}
A target is the device where the stream will be played on.
**Where the scraping is run has nothing to do with the target**, only where the stream is finally played in the end is significant in choosing a target.
::
#### Possible targets
- **`targets.BROWSER`** Stream will be played in a browser with CORS
- **`targets.NATIVE`** Stream will be played natively
- **`targets.ALL`** Stream will be played on a device with no restrictions of any kind

View File

@@ -0,0 +1,47 @@
# Fetchers
When making an instance of the library using `makeProviders()`. It will immediately make a fetcher.
This comes with some considerations depending on the environment youre running.
## Using `fetch()`
In most cases, you can use the `fetch()` API. This will work in newer versions of Node.js (18 and above) and on the browser.
```ts
const fetcher = makeDefaultFetcher(fetch);
```
If you using older version of Node.js. You can use the npm package `node-fetch` to polyfill fetch:
```ts
import fetch from "node-fetch";
const fetcher = makeDefaultFetcher(fetch);
```
## Using fetchers on the browser
When using this library on a browser, you will need a proxy. Browsers come with many restrictions on when a web request can be made, and to bypass those restrictions, you will need a cors proxy.
The movie-web team has a proxy pre-made and pre-configured for you to use. For more information, check out [movie-web/simple-proxy](https://github.com/movie-web/simple-proxy). After installing, you can use this proxy like so:
```ts
const fetcher = makeSimpleProxyFetcher("https://your.proxy.workers.dev/", fetch);
```
If you aren't able to use this specific proxy and need to use a different one, you can make your own fetcher in the next section.
## Making a custom fetcher
In some rare cases, a custom fetcher will need to be made. This can be quite difficult to do from scratch so it's recommended to base it off an existing fetcher and building your own functionality around it.
```ts
export function makeCustomFetcher(): Fetcher {
const fetcher = makeStandardFetcher(f);
const customFetcher: Fetcher = (url, ops) => {
return fetcher(url, ops);
};
return customFetcher;
}
```
If you need to make your own fetcher for a proxy. Make sure you make it compatible with the following headers: `Cookie`, `Referer`, `Origin`. Proxied fetchers need to be able to write those headers when making a request.

View File

@@ -0,0 +1,2 @@
icon: ph:book-open-fill
navigation.redirect: /guide/usage

View File

@@ -0,0 +1,34 @@
# `makeProviders`
Make an instance of providers with configuration.
This is the main entrypoint of the library. It is recommended to make one instance globally and reuse it throughout your application.
## Example
```ts
import { targets, makeProviders, makeDefaultFetcher } from "@movie-web/providers";
const providers = makeProviders({
fetcher: makeDefaultFetcher(fetch),
target: targets.NATIVE, // target native app streams
});
```
## Type
```ts
function makeProviders(ops: ProviderBuilderOptions): ProviderControls;
interface ProviderBuilderOptions {
// instance of a fetcher, all webrequests are made with the fetcher.
fetcher: Fetcher;
// instance of a fetcher, in case the request has cors restrictions.
// this fetcher will be called instead of normal fetcher.
// if your environment doesnt have cors restrictions (like nodejs), there is no need to set this.
proxiedFetcher?: Fetcher;
// target to get streams for
target: Targets;
}
```

View File

@@ -0,0 +1,61 @@
# `ProviderControls.runAll`
Run all providers one by one in order of their built-in ranking.
You can attach events if you need to know what is going on while its processing.
## Example
```ts
// media from TMDB
const media = {
type: 'movie',
title: "Hamilton",
releaseYear: 2020,
tmdbId: "556574"
}
// scrape a stream
const stream = await providers.runAll({
media: media,
})
// scrape a stream, but prioritize flixhq above all
// (other scrapers are stil ran if flixhq fails, it just has priority)
const flixhqStream = await providers.runAll({
media: media,
sourceOrder: ['flixhq']
})
```
## Type
```ts
function runAll(runnerOps: RunnerOptions): Promise<RunOutput | null>;
interface RunnerOptions {
// overwrite the order of sources to run. list of ids
// any omitted ids are in added to the end in order of rank (highest first)
sourceOrder?: string[];
// overwrite the order of embeds to run. list of ids
// any omitted ids are in added to the end in order of rank (highest first)
embedOrder?: string[];
// object of event functions
events?: FullScraperEvents;
// the media you want to see sources from
media: ScrapeMedia;
}
type RunOutput = {
// source scraper id
sourceId: string;
// if from an embed, this is the embed scraper id
embedId?: string;
// the outputed stream
stream: Stream;
};
```

View File

@@ -0,0 +1,66 @@
# `ProviderControls.runSourceScraper`
Run a specific source scraper and get its outputted streams.
## Example
```ts
import { SourcererOutput, NotFoundError } from "@movie-web/providers";
// media from TMDB
const media = {
type: 'movie',
title: "Hamilton",
releaseYear: 2020,
tmdbId: "556574"
}
// scrape a stream from flixhq
let output: SourcererOutput;
try {
output = await providers.runSourceScraper({
id: 'flixhq',
media: media,
})
} catch (err) {
if (err instanceof NotFoundError) {
console.log("source doesnt have this media");
} else {
console.log("failed to scrape")
}
return;
}
if (!output.stream && output.embeds.length === 0) {
console.log("no streams found");
}
```
## Type
```ts
function runSourceScraper(runnerOps: SourceRunnerOptions): Promise<SourcererOutput>;
interface SourceRunnerOptions {
// object of event functions
events?: IndividualScraperEvents;
// the media you want to see sources from
media: ScrapeMedia;
// id of the source scraper you want to scrape from
id: string;
}
type SourcererOutput = {
// list of embeds that the source scraper found.
// embed id is a reference to an embed scraper
embeds: {
embedId: string;
url: string;
}[];
// the stream that the scraper found
stream?: Stream;
};
```

View File

@@ -0,0 +1,44 @@
# `ProviderControls.runEmbedScraper`
Run a specific embed scraper and get its outputted streams.
## Example
```ts
import { SourcererOutput } from "@movie-web/providers";
// scrape a stream from upcloud
let output: EmbedOutput;
try {
output = await providers.runSourceScraper({
id: 'upcloud',
url: 'https://example.com/123',
})
} catch (err) {
console.log("failed to scrape")
return;
}
// output.stream now has your stream
```
## Type
```ts
function runEmbedScraper(runnerOps: SourceRunnerOptions): Promise<EmbedOutput>;
interface EmbedRunnerOptions {
// object of event functions
events?: IndividualScraperEvents;
// the embed url
url: string;
// id of the embed scraper you want to scrape from
id: string;
}
type EmbedOutput = {
stream: Stream;
};
```

View File

@@ -0,0 +1,25 @@
# `ProviderControls.listSources`
List all source scrapers that applicable for the target.
They are sorted by rank, highest first
## Example
```ts
const sourceScrapers = providers.listSources();
// Guaranteed to only return type: 'source'
```
## Type
```ts
function listSources(): MetaOutput[];
type MetaOutput = {
type: 'embed' | 'source';
id: string;
rank: number;
name: string;
mediaTypes?: Array<MediaTypes>;
};
```

View File

@@ -0,0 +1,25 @@
# `ProviderControls.listEmbeds`
List all embed scrapers that applicable for the target.
They are sorted by rank, highest first
## Example
```ts
const embedScrapers = providers.listEmbeds();
// Guaranteed to only return type: 'embed'
```
## Type
```ts
function listEmbeds(): MetaOutput[];
type MetaOutput = {
type: 'embed' | 'source';
id: string;
rank: number;
name: string;
mediaTypes?: Array<MediaTypes>;
};
```

View File

@@ -0,0 +1,24 @@
# `ProviderControls.getMetadata`
Get meta data for a scraper, can be either source or embed scraper.
Returns null if the `id` is not recognized.
## Example
```ts
const flixhqSource = providers.getMetadata('flixhq');
```
## Type
```ts
function getMetadata(id: string): MetaOutput | null;
type MetaOutput = {
type: 'embed' | 'source';
id: string;
rank: number;
name: string;
mediaTypes?: Array<MediaTypes>;
};
```

View File

@@ -0,0 +1,20 @@
# `makeStandardFetcher`
Make a fetcher from a `fetch()` API. It is used for making a instance of providers with `makeProviders()`.
## Example
```ts
import { targets, makeProviders, makeDefaultFetcher } from "@movie-web/providers";
const providers = makeProviders({
fetcher: makeDefaultFetcher(fetch),
target: targets.NATIVE,
});
```
## Type
```ts
function makeDefaultFetcher(fetchApi: typeof fetch): Fetcher;
```

View File

@@ -0,0 +1,23 @@
# `makeSimpleProxyFetcher`
Make a fetcher to use with [movie-web/simple-proxy](https://github.com/movie-web/simple-proxy). This is for making a proxiedFetcher, so you can run this library in the browser.
## Example
```ts
import { targets, makeProviders, makeDefaultFetcher, makeSimpleProxyFetcher } from "@movie-web/providers";
const proxyUrl = "https://your.proxy.workers.dev/"
const providers = makeProviders({
fetcher: makeDefaultFetcher(fetch),
proxiedFetcher: makeSimpleProxyFetcher(proxyUrl, fetch),
target: targets.BROWSER,
});
```
## Type
```ts
function makeSimpleProxyFetcher(proxyUrl: string, fetchApi: typeof fetch): Fetcher;
```

View File

@@ -0,0 +1,2 @@
icon: ph:file-code-fill
navigation.redirect: /api/makeproviders

21
.docs/nuxt.config.ts Executable file
View File

@@ -0,0 +1,21 @@
export default defineNuxtConfig({
// https://github.com/nuxt-themes/docus
extends: '@nuxt-themes/docus',
css: [
'@/assets/css/main.css',
],
build: {
transpile: [
"chalk"
]
},
modules: [
// https://github.com/nuxt-modules/plausible
'@nuxtjs/plausible',
// https://github.com/nuxt/devtools
'@nuxt/devtools'
]
})

19466
.docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

18
.docs/package.json Normal file → Executable file
View File

@@ -1,9 +1,21 @@
{ {
"name": "providers-docs",
"version": "0.1.0",
"private": true,
"scripts": { "scripts": {
"dev": "vitepress dev .", "dev": "nuxi dev",
"build": "vitepress build ." "build": "nuxi build",
"generate": "nuxi generate",
"preview": "nuxi preview",
"lint": "eslint ."
}, },
"devDependencies": { "devDependencies": {
"vitepress": "^1.0.0-rc.10" "@nuxt-themes/docus": "^1.13.1",
"@nuxt/devtools": "^0.6.7",
"@nuxt/eslint-config": "^0.1.1",
"@nuxtjs/plausible": "^0.2.1",
"@types/node": "^20.4.0",
"eslint": "^8.44.0",
"nuxt": "^3.6.2"
} }
} }

BIN
.docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

8
.docs/renovate.json Executable file
View File

@@ -0,0 +1,8 @@
{
"extends": [
"@nuxtjs"
],
"lockFileMaintenance": {
"enabled": true
}
}

View File

@@ -1,49 +0,0 @@
---
outline: deep
---
# Runtime API Examples
This page demonstrates usage of some of the runtime APIs provided by VitePress.
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
```md
<script setup>
import { useData } from 'vitepress'
const { theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
```
<script setup>
import { useData } from 'vitepress'
const { site, theme, page, frontmatter } = useData()
</script>
## Results
### Theme Data
<pre>{{ theme }}</pre>
### Page Data
<pre>{{ page }}</pre>
### Page Frontmatter
<pre>{{ frontmatter }}</pre>
## More
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).

View File

@@ -1,24 +0,0 @@
---
layout: home
hero:
name: "@movie-web/providers"
tagline: Providers for all kinds of media
actions:
- theme: brand
text: Get Started
link: /get-started/start
- theme: alt
text: reference
link: /reference/start
features:
- title: All the scraping!
icon: '!'
details: scrape popular streaming websites
- title: Client & server
icon: '!'
details: This library can be ran both server-side and client-side (with CORS proxy)
---

View File

@@ -1,85 +0,0 @@
# Markdown Extension Examples
This page demonstrates some of the built-in markdown extensions provided by VitePress.
## Syntax Highlighting
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
**Input**
````
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
````
**Output**
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
## Custom Containers
**Input**
```md
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
```
**Output**
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
## More
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).

18
.docs/tokens.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineTheme } from 'pinceau'
export default defineTheme({
color: {
primary: {
50: "#F5E5FF",
100: "#E7CCFF",
200: "#D4A9FF",
300: "#BE85FF",
400: "#A861FF",
500: "#8E3DFF",
600: "#7F36D4",
700: "#662CA6",
800: "#552578",
900: "#441E49"
}
}
})

3
.docs/tsconfig.json Executable file
View File

@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -3,7 +3,7 @@ module.exports = {
browser: true, browser: true,
}, },
extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
ignorePatterns: ['lib/*', '/*.js', '/*.ts', '/**/*.test.ts', 'test/*'], ignorePatterns: ['lib/*', 'tests/*', '/*.js', '/*.ts', '/**/*.test.ts', 'test/*'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',
@@ -33,7 +33,7 @@ module.exports = {
'no-eval': 'off', 'no-eval': 'off',
'no-await-in-loop': 'off', 'no-await-in-loop': 'off',
'no-nested-ternary': 'off', 'no-nested-ternary': 'off',
'no-param-reassign': ["error", { "props": false }], 'no-param-reassign': ['error', { props: false }],
'prefer-destructuring': 'off', 'prefer-destructuring': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'import/extensions': [ 'import/extensions': [

View File

@@ -7,28 +7,33 @@ on:
jobs: jobs:
build: build:
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
- name: Install packages
run: cd .docs && npm ci
- name: Build - name: Install packages
run: cd .docs && npm run build working-directory: ./.docs
run: npm install
- name: Build project
working-directory: ./.docs
run: npm run generate
- name: Upload - name: Upload production-ready build files
uses: actions/upload-pages-artifact@v2 uses: actions/upload-pages-artifact@v1
with: with:
path: ./.docs/.vitepress/dist path: ./.docs/.output/public
deploy: deploy:
name: Deploy
needs: build needs: build
permissions: permissions:
pages: write pages: write

View File

@@ -15,17 +15,20 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
- name: Install packages - name: Install packages
run: npm install run: npm install
- name: Run tests - name: Run tests
run: npm run test run: npm run test
- name: Run integration tests
run: npm run build && npm run test:integration
- name: Run linting - name: Run linting
run: npm run lint run: npm run lint

View File

@@ -4,10 +4,11 @@ package that holds all providers of movie-web.
Feel free to use for your own projects. Feel free to use for your own projects.
features: features:
- scrape popular streaming websites - scrape popular streaming websites
- works in both browser and server-side - works in both browser and server-side
Visit documentation here: https://providers.docs.movie-web.app/
## Development ## Development
To make testing scrapers easier during development a CLI tool is available to run specific sources. To run the CLI testing tool, use `npm run test:dev`. The script supports 2 execution modes To make testing scrapers easier during development a CLI tool is available to run specific sources. To run the CLI testing tool, use `npm run test:dev`. The script supports 2 execution modes
@@ -32,6 +33,3 @@ Example testing the FlixHQ source on the movie "Spirited Away"
```bash ```bash
npm run test:dev -- -sid flixhq -tid 129 -t movie npm run test:dev -- -sid flixhq -tid 129 -t movie
``` ```
Todos:
- make default fetcher maker thing work with both undici and node-fetch

42
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "0.0.12", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "0.0.12", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
@@ -32,6 +32,7 @@
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"node-fetch": "^2.7.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"spinnies": "^0.5.1", "spinnies": "^0.5.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@@ -3511,9 +3512,9 @@
} }
}, },
"node_modules/get-func-name": { "node_modules/get-func-name": {
"version": "2.0.0", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
@@ -4724,6 +4725,7 @@
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
@@ -4742,17 +4744,20 @@
"node_modules/node-fetch/node_modules/tr46": { "node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
}, },
"node_modules/node-fetch/node_modules/webidl-conversions": { "node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
}, },
"node_modules/node-fetch/node_modules/whatwg-url": { "node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": { "dependencies": {
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
@@ -5120,24 +5125,6 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6157,6 +6144,11 @@
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
}, },
"node_modules/unpacker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unpacker/-/unpacker-1.0.1.tgz",
"integrity": "sha512-0HTljwp8+JBdITpoHcK1LWi7X9U2BspUmWv78UWZh7NshYhbh1nec8baY/iSbe2OQTZ2bhAtVdnr6/BTD0DKVg=="
},
"node_modules/untildify": { "node_modules/untildify": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@movie-web/providers", "name": "@movie-web/providers",
"version": "0.0.12", "version": "1.0.1",
"description": "Package that contains all the providers of movie-web", "description": "Package that contains all the providers of movie-web",
"main": "./lib/index.umd.js", "main": "./lib/index.umd.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@@ -32,12 +32,13 @@
"bugs": { "bugs": {
"url": "https://github.com/movie-web/providers/issues" "url": "https://github.com/movie-web/providers/issues"
}, },
"homepage": "https://github.com/movie-web/providers#readme", "homepage": "https://providers.docs.movie-web.app/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"test": "vitest run", "test": "vitest run",
"test:dev": "ts-node ./src/dev-cli.ts", "test:dev": "ts-node ./src/dev-cli.ts",
"test:watch": "vitest", "test:watch": "vitest",
"test:integration": "node ./tests/cjs && node ./tests/esm",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"lint": "eslint --ext .ts,.js src/", "lint": "eslint --ext .ts,.js src/",
"lint:fix": "eslint --fix --ext .ts,.js src/", "lint:fix": "eslint --fix --ext .ts,.js src/",
@@ -62,6 +63,7 @@
"eslint-import-resolver-typescript": "^3.5.5", "eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"node-fetch": "^2.7.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"spinnies": "^0.5.1", "spinnies": "^0.5.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

View File

@@ -2,12 +2,9 @@ import util from 'node:util';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { program } from 'commander'; import { program } from 'commander';
// eslint-disable-next-line import/no-extraneous-dependencies
import dotenv from 'dotenv'; import dotenv from 'dotenv';
// eslint-disable-next-line import/no-extraneous-dependencies
import { prompt } from 'enquirer'; import { prompt } from 'enquirer';
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch';
// eslint-disable-next-line import/no-extraneous-dependencies
import Spinnies from 'spinnies'; import Spinnies from 'spinnies';
import { MetaOutput, MovieMedia, ProviderControls, ShowMedia, makeProviders, makeStandardFetcher, targets } from '.'; import { MetaOutput, MovieMedia, ProviderControls, ShowMedia, makeProviders, makeStandardFetcher, targets } from '.';
@@ -284,7 +281,7 @@ async function processOptions(options: CommandLineArguments) {
let fetcher; let fetcher;
if (options.fetcher === 'native') { if (options.fetcher === 'native') {
fetcher = makeStandardFetcher(fetch as any); fetcher = makeStandardFetcher(fetch);
} else { } else {
fetcher = makeStandardFetcher(nodeFetch); fetcher = makeStandardFetcher(nodeFetch);
} }

22
src/fetchers/fetch.ts Normal file
View File

@@ -0,0 +1,22 @@
/**
* This file is a very relaxed definition of the fetch api
* Only containing what we need for it to function.
*/
export type FetchOps = {
headers: Record<string, string>;
method: string;
body: any;
};
export type FetchHeaders = {
get(key: string): string | null;
};
export type FetchReply = {
text(): Promise<string>;
json(): Promise<any>;
headers: FetchHeaders;
};
export type FetchLike = (url: string, ops?: FetchOps | undefined) => Promise<FetchReply>;

View File

@@ -1,6 +1,5 @@
import fetch from 'node-fetch';
import { makeFullUrl } from '@/fetchers/common'; import { makeFullUrl } from '@/fetchers/common';
import { FetchLike } from '@/fetchers/fetch';
import { makeStandardFetcher } from '@/fetchers/standardFetch'; import { makeStandardFetcher } from '@/fetchers/standardFetch';
import { Fetcher } from '@/fetchers/types'; import { Fetcher } from '@/fetchers/types';
@@ -10,7 +9,7 @@ const headerMap: Record<string, string> = {
origin: 'X-Origin', origin: 'X-Origin',
}; };
export function makeSimpleProxyFetcher(proxyUrl: string, f: typeof fetch): Fetcher { export function makeSimpleProxyFetcher(proxyUrl: string, f: FetchLike): Fetcher {
const fetcher = makeStandardFetcher(f); const fetcher = makeStandardFetcher(f);
const proxiedFetch: Fetcher = async (url, ops) => { const proxiedFetch: Fetcher = async (url, ops) => {
const fullUrl = makeFullUrl(url, ops); const fullUrl = makeFullUrl(url, ops);

View File

@@ -1,10 +1,9 @@
import fetch from 'node-fetch';
import { serializeBody } from '@/fetchers/body'; import { serializeBody } from '@/fetchers/body';
import { makeFullUrl } from '@/fetchers/common'; import { makeFullUrl } from '@/fetchers/common';
import { FetchLike } from '@/fetchers/fetch';
import { Fetcher } from '@/fetchers/types'; import { Fetcher } from '@/fetchers/types';
export function makeStandardFetcher(f: typeof fetch): Fetcher { export function makeStandardFetcher(f: FetchLike): Fetcher {
const normalFetch: Fetcher = async (url, ops) => { const normalFetch: Fetcher = async (url, ops) => {
const fullUrl = makeFullUrl(url, ops); const fullUrl = makeFullUrl(url, ops);
const seralizedBody = serializeBody(ops.body); const seralizedBody = serializeBody(ops.body);

View File

@@ -7,6 +7,7 @@ export type Flags = (typeof flags)[keyof typeof flags];
export const targets = { export const targets = {
BROWSER: 'browser', BROWSER: 'browser',
NATIVE: 'native', NATIVE: 'native',
ALL: 'all',
} as const; } as const;
export type Targets = (typeof targets)[keyof typeof targets]; export type Targets = (typeof targets)[keyof typeof targets];
@@ -22,6 +23,9 @@ export const targetToFeatures: Record<Targets, FeatureMap> = {
native: { native: {
requires: [], requires: [],
}, },
all: {
requires: [],
},
} as const; } as const;
export function getTargetFeatures(target: Targets): FeatureMap { export function getTargetFeatures(target: Targets): FeatureMap {

View File

@@ -1,22 +1,25 @@
import { Embed, Sourcerer } from '@/providers/base'; import { Embed, Sourcerer } from '@/providers/base';
import { febBoxScraper } from '@/providers/embeds/febBox'; import { febBoxScraper } from '@/providers/embeds/febBox';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
import { mp4uploadScraper } from '@/providers/embeds/mp4upload'; import { mp4uploadScraper } from '@/providers/embeds/mp4upload';
import { streamsbScraper } from '@/providers/embeds/streamsb'; import { streamsbScraper } from '@/providers/embeds/streamsb';
import { upcloudScraper } from '@/providers/embeds/upcloud'; import { upcloudScraper } from '@/providers/embeds/upcloud';
import { upstreamScraper } from '@/providers/embeds/upstream';
import { flixhqScraper } from '@/providers/sources/flixhq/index'; import { flixhqScraper } from '@/providers/sources/flixhq/index';
import { goMoviesScraper } from '@/providers/sources/gomovies/index'; import { goMoviesScraper } from '@/providers/sources/gomovies/index';
import { kissAsianScraper } from '@/providers/sources/kissasian/index'; import { kissAsianScraper } from '@/providers/sources/kissasian/index';
import { remotestreamScraper } from '@/providers/sources/remotestream'; import { remotestreamScraper } from '@/providers/sources/remotestream';
import { superStreamScraper } from '@/providers/sources/superstream/index'; import { superStreamScraper } from '@/providers/sources/superstream/index';
import { zoechipScraper } from '@/providers/sources/zoechip';
import { showBoxScraper } from './sources/showbox'; import { showBoxScraper } from './sources/showbox';
export function gatherAllSources(): Array<Sourcerer> { export function gatherAllSources(): Array<Sourcerer> {
// all sources are gathered here // all sources are gathered here
return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper, showBoxScraper]; return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper, zoechipScraper, showBoxScraper];
} }
export function gatherAllEmbeds(): Array<Embed> { export function gatherAllEmbeds(): Array<Embed> {
// all embeds are gathered here // all embeds are gathered here
return [upcloudScraper, mp4uploadScraper, streamsbScraper, febBoxScraper]; return [upcloudScraper, mp4uploadScraper, streamsbScraper, upstreamScraper, febBoxScraper, mixdropScraper];
} }

View File

@@ -0,0 +1,52 @@
import * as unpacker from 'unpacker';
import { makeEmbed } from '@/providers/base';
const packedRegex = /(eval\(function\(p,a,c,k,e,d\){.*{}\)\))/;
const linkRegex = /MDCore\.wurl="(.*?)";/;
export const mixdropScraper = makeEmbed({
id: 'mixdrop',
name: 'MixDrop',
rank: 198,
async scrape(ctx) {
// Example url: https://mixdrop.co/e/pkwrgp0pizgod0
// Example url: https://mixdrop.vc/e/pkwrgp0pizgod0
const streamRes = await ctx.proxiedFetcher<string>(ctx.url);
const packed = streamRes.match(packedRegex);
// MixDrop uses a queue system for embeds
// If an embed is too new, the queue will
// not be completed and thus the packed
// JavaScript not present
if (!packed) {
throw new Error('failed to find packed mixdrop JavaScript');
}
const unpacked = unpacker.unpack(packed[1]);
const link = unpacked.match(linkRegex);
if (!link) {
throw new Error('failed to find packed mixdrop source link');
}
const url = link[1];
return {
stream: {
type: 'file',
flags: [],
qualities: {
unknown: {
type: 'mp4',
url: url.startsWith('http') ? url : `https:${url}`, // URLs don't always start with the protocol
headers: {
// MixDrop requires this header on all streams
Referer: 'https://mixdrop.co/',
},
},
},
},
};
},
});

View File

@@ -0,0 +1,35 @@
import * as unpacker from 'unpacker';
import { flags } from '@/main/targets';
import { makeEmbed } from '@/providers/base';
const packedRegex = /(eval\(function\(p,a,c,k,e,d\).*\)\)\))/;
const linkRegex = /sources:\[{file:"(.*?)"/;
export const upstreamScraper = makeEmbed({
id: 'upstream',
name: 'UpStream',
rank: 199,
async scrape(ctx) {
// Example url: https://upstream.to/embed-omscqgn6jc8r.html
const streamRes = await ctx.proxiedFetcher<string>(ctx.url);
const packed = streamRes.match(packedRegex);
if (packed) {
const unpacked = unpacker.unpack(packed[1]);
const link = unpacked.match(linkRegex);
if (link) {
return {
stream: {
type: 'hls',
playlist: link[1],
flags: [flags.NO_CORS],
},
};
}
}
throw new Error('upstream source not found');
},
});

View File

@@ -0,0 +1,71 @@
import { MovieMedia, ShowMedia } from '@/main/media';
import { mixdropScraper } from '@/providers/embeds/mixdrop';
import { upcloudScraper } from '@/providers/embeds/upcloud';
import { upstreamScraper } from '@/providers/embeds/upstream';
import { getZoeChipSourceURL, getZoeChipSources } from '@/providers/sources/zoechip/scrape';
import { ScrapeContext } from '@/utils/context';
export const zoeBase = 'https://zoechip.cc';
export type MovieContext = ScrapeContext & {
media: MovieMedia;
};
export type ShowContext = ScrapeContext & {
media: ShowMedia;
};
export type ZoeChipSourceDetails = {
type: string; // Only seen "iframe" so far
link: string;
sources: string[]; // Never seen this populated, assuming it's a string array
tracks: string[]; // Never seen this populated, assuming it's a string array
title: string;
};
export async function formatSource(ctx: MovieContext | ShowContext, source: { embed: string; episodeId: string }) {
const link = await getZoeChipSourceURL(ctx, source.episodeId);
if (link) {
const embed = {
embedId: '',
url: link,
};
const parsedUrl = new URL(link);
switch (parsedUrl.host) {
case 'rabbitstream.net':
embed.embedId = upcloudScraper.id;
break;
case 'upstream.to':
embed.embedId = upstreamScraper.id;
break;
case 'mixdrop.co':
embed.embedId = mixdropScraper.id;
break;
default:
throw new Error(`Failed to find ZoeChip embed source for ${link}`);
}
return embed;
}
}
export async function createZoeChipStreamData(ctx: MovieContext | ShowContext, id: string) {
const sources = await getZoeChipSources(ctx, id);
const embeds: {
embedId: string;
url: string;
}[] = [];
for (const source of sources) {
const formatted = await formatSource(ctx, source);
if (formatted) {
embeds.push(formatted);
}
}
return {
embeds,
};
}

View File

@@ -0,0 +1,13 @@
import { flags } from '@/main/targets';
import { makeSourcerer } from '@/providers/base';
import { scrapeMovie } from '@/providers/sources/zoechip/scrape-movie';
import { scrapeShow } from '@/providers/sources/zoechip/scrape-show';
export const zoechipScraper = makeSourcerer({
id: 'zoechip',
name: 'ZoeChip',
rank: 110,
flags: [flags.NO_CORS],
scrapeMovie,
scrapeShow,
});

View File

@@ -0,0 +1,12 @@
import { MovieContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
import { getZoeChipMovieID } from '@/providers/sources/zoechip/search';
import { NotFoundError } from '@/utils/errors';
export async function scrapeMovie(ctx: MovieContext) {
const movieID = await getZoeChipMovieID(ctx, ctx.media);
if (!movieID) {
throw new NotFoundError('no search results match');
}
return createZoeChipStreamData(ctx, movieID);
}

View File

@@ -0,0 +1,23 @@
import { ShowContext, createZoeChipStreamData } from '@/providers/sources/zoechip/common';
import { getZoeChipEpisodeID, getZoeChipSeasonID } from '@/providers/sources/zoechip/scrape';
import { getZoeChipShowID } from '@/providers/sources/zoechip/search';
import { NotFoundError } from '@/utils/errors';
export async function scrapeShow(ctx: ShowContext) {
const showID = await getZoeChipShowID(ctx, ctx.media);
if (!showID) {
throw new NotFoundError('no search results match');
}
const seasonID = await getZoeChipSeasonID(ctx, ctx.media, showID);
if (!seasonID) {
throw new NotFoundError('no season found');
}
const episodeID = await getZoeChipEpisodeID(ctx, ctx.media, seasonID);
if (!episodeID) {
throw new NotFoundError('no episode found');
}
return createZoeChipStreamData(ctx, episodeID);
}

View File

@@ -0,0 +1,126 @@
import { load } from 'cheerio';
import { ShowMedia } from '@/main/media';
import { MovieContext, ShowContext, ZoeChipSourceDetails, zoeBase } from '@/providers/sources/zoechip/common';
import { ScrapeContext } from '@/utils/context';
export async function getZoeChipSources(ctx: MovieContext | ShowContext, id: string) {
// Movies use /ajax/episode/list/ID
// Shows use /ajax/episode/servers/ID
const endpoint = ctx.media.type === 'movie' ? 'list' : 'servers';
const html = await ctx.proxiedFetcher<string>(`/ajax/episode/${endpoint}/${id}`, {
baseUrl: zoeBase,
});
const $ = load(html);
return $('.nav-item a')
.toArray()
.map((el) => {
// Movies use data-linkid
// Shows use data-id
const idAttribute = ctx.media.type === 'movie' ? 'data-linkid' : 'data-id';
const element = $(el);
const embedTitle = element.attr('title');
const linkId = element.attr(idAttribute);
if (!embedTitle || !linkId) {
throw new Error('invalid sources');
}
return {
embed: embedTitle,
episodeId: linkId,
};
});
}
export async function getZoeChipSourceURL(ctx: ScrapeContext, sourceID: string): Promise<string | null> {
const details = await ctx.proxiedFetcher<ZoeChipSourceDetails>(`/ajax/sources/${sourceID}`, {
baseUrl: zoeBase,
});
// TODO - Support non-iframe sources
if (details.type !== 'iframe') {
return null;
}
// TODO - Extract the other data from the source
return details.link;
}
export async function getZoeChipSeasonID(ctx: ScrapeContext, media: ShowMedia, showID: string): Promise<string | null> {
const html = await ctx.proxiedFetcher<string>(`/ajax/season/list/${showID}`, {
baseUrl: zoeBase,
});
const $ = load(html);
const seasons = $('.dropdown-menu a')
.toArray()
.map((el) => {
const element = $(el);
const seasonID = element.attr('data-id');
const seasonNumber = element.html()?.split(' ')[1];
if (!seasonID || !seasonNumber || Number.isNaN(Number(seasonNumber))) {
throw new Error('invalid season');
}
return {
id: seasonID,
season: Number(seasonNumber),
};
});
const foundSeason = seasons.find((season) => season.season === media.season.number);
if (!foundSeason) {
return null;
}
return foundSeason.id;
}
export async function getZoeChipEpisodeID(
ctx: ScrapeContext,
media: ShowMedia,
seasonID: string,
): Promise<string | null> {
const episodeNumberRegex = /Eps (\d*):/;
const html = await ctx.proxiedFetcher<string>(`/ajax/season/episodes/${seasonID}`, {
baseUrl: zoeBase,
});
const $ = load(html);
const episodes = $('.eps-item')
.toArray()
.map((el) => {
const element = $(el);
const episodeID = element.attr('data-id');
const title = element.attr('title');
if (!episodeID || !title) {
throw new Error('invalid episode');
}
const regexResult = title.match(episodeNumberRegex);
if (!regexResult || Number.isNaN(Number(regexResult[1]))) {
throw new Error('invalid episode');
}
return {
id: episodeID,
episode: Number(regexResult[1]),
};
});
const foundEpisode = episodes.find((episode) => episode.episode === media.episode.number);
if (!foundEpisode) {
return null;
}
return foundEpisode.id;
}

View File

@@ -0,0 +1,111 @@
import { load } from 'cheerio';
import { MovieMedia, ShowMedia } from '@/main/media';
import { zoeBase } from '@/providers/sources/zoechip/common';
import { compareMedia } from '@/utils/compare';
import { ScrapeContext } from '@/utils/context';
export async function getZoeChipSearchResults(ctx: ScrapeContext, media: MovieMedia | ShowMedia) {
const titleCleaned = media.title.toLocaleLowerCase().replace(/ /g, '-');
const html = await ctx.proxiedFetcher<string>(`/search/${titleCleaned}`, {
baseUrl: zoeBase,
});
const $ = load(html);
return $('.film_list-wrap .flw-item .film-detail')
.toArray()
.map((element) => {
const movie = $(element);
const anchor = movie.find('.film-name a');
const info = movie.find('.fd-infor');
const title = anchor.attr('title');
const href = anchor.attr('href');
const type = info.find('.fdi-type').html();
let year = info.find('.fdi-item').html();
const id = href?.split('-').pop();
if (!title) {
return null;
}
if (!href) {
return null;
}
if (!type) {
return null;
}
// TV shows on ZoeChip do not have a year in their search results
// Allow TV shows to pass this failure
if (!year || Number.isNaN(Number(year))) {
if (type === 'TV') {
year = '0';
} else {
return null;
}
}
if (!id) {
return null;
}
return {
title,
year: Number(year),
id,
type,
href,
};
});
}
export async function getZoeChipMovieID(ctx: ScrapeContext, media: MovieMedia): Promise<string | null> {
const searchResults = await getZoeChipSearchResults(ctx, media);
const matchingItem = searchResults.find((v) => v && v.type === 'Movie' && compareMedia(media, v.title, v.year));
if (!matchingItem) {
return null;
}
return matchingItem.id;
}
export async function getZoeChipShowID(ctx: ScrapeContext, media: ShowMedia): Promise<string | null> {
// ZoeChip TV shows don't have a year on their search results
// This makes it hard to filter between shows with the same name
// To find the year, we must query each shows details page
// This is slower, but more reliable
const releasedRegex = /<\/strong><\/span> (\d.*)-\d.*-\d.*/;
const searchResults = await getZoeChipSearchResults(ctx, media);
const filtered = searchResults.filter((v) => v && v.type === 'TV' && compareMedia(media, v.title));
for (const result of filtered) {
// This gets filtered above but the linter Gods don't think so
if (!result) {
continue;
}
const html = await ctx.proxiedFetcher<string>(result.href, {
baseUrl: zoeBase,
});
// The HTML is not structured in a way that makes using Cheerio clean
// There are no unique IDs or classes to query, resulting in long ugly queries
// Regex is faster and cleaner in this case
const regexResult = html.match(releasedRegex);
if (regexResult) {
const year = Number(regexResult[1]);
if (!Number.isNaN(year) && compareMedia(media, result.title, year)) {
return result.id;
}
}
}
return null;
}

View File

@@ -3,9 +3,10 @@ import { Flags } from '@/main/targets';
export type StreamFile = { export type StreamFile = {
type: 'mp4'; type: 'mp4';
url: string; url: string;
headers?: Record<string, string>;
}; };
export type Qualities = '360' | '480' | '720' | '1080'; export type Qualities = 'unknown' | '360' | '480' | '720' | '1080';
export type FileBasedStream = { export type FileBasedStream = {
type: 'file'; type: 'file';

3
tests/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Integration test folder
This folder simply holds some import tests, to see if the library still works with all its dependencies.

2
tests/cjs/index.js Normal file
View File

@@ -0,0 +1,2 @@
require('../../lib/index.umd');
console.log('import successful!');

4
tests/cjs/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"main": "index.js",
"type": "commonjs"
}

2
tests/esm/index.mjs Normal file
View File

@@ -0,0 +1,2 @@
import '../../lib/index.mjs';
console.log('import successful!');

4
tests/esm/package.json Normal file
View File

@@ -0,0 +1,4 @@
{
"main": "index.mjs",
"type": "module"
}