mirror of
https://github.com/movie-web/providers.git
synced 2025-09-13 12:43:25 +00:00
Merge branch 'dev' into dev
This commit is contained in:
4
.docs/.eslintignore
Normal file
4
.docs/.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
node_modules
|
||||
.output
|
||||
.nuxt
|
8
.docs/.eslintrc.cjs
Normal file
8
.docs/.eslintrc.cjs
Normal 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
14
.docs/.gitignore
vendored
Normal file → Executable file
@@ -1,3 +1,13 @@
|
||||
node_modules
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
*.iml
|
||||
.idea
|
||||
*.log*
|
||||
.nuxt
|
||||
.vscode
|
||||
.DS_Store
|
||||
coverage
|
||||
dist
|
||||
sw.*
|
||||
.env
|
||||
.output
|
||||
.nuxt
|
||||
|
@@ -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
17
.docs/app.config.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
});
|
3
.docs/assets/css/main.css
Normal file
3
.docs/assets/css/main.css
Normal file
@@ -0,0 +1,3 @@
|
||||
code > span {
|
||||
white-space: pre;
|
||||
}
|
51
.docs/content/0.index.md
Normal file
51
.docs/content/0.index.md
Normal 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.
|
||||
::
|
||||
::
|
53
.docs/content/1.Guide/0.usage.md
Normal file
53
.docs/content/1.Guide/0.usage.md
Normal 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}`)
|
||||
```
|
13
.docs/content/1.Guide/1.targets.md
Normal file
13
.docs/content/1.Guide/1.targets.md
Normal 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
|
47
.docs/content/1.Guide/2.fetchers.md
Normal file
47
.docs/content/1.Guide/2.fetchers.md
Normal 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.
|
2
.docs/content/1.Guide/_dir.yml
Normal file
2
.docs/content/1.Guide/_dir.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
icon: ph:book-open-fill
|
||||
navigation.redirect: /guide/usage
|
34
.docs/content/2.Api/0.makeProviders.md
Normal file
34
.docs/content/2.Api/0.makeProviders.md
Normal 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;
|
||||
}
|
||||
```
|
61
.docs/content/2.Api/1.ProviderControlsRunAll.md
Normal file
61
.docs/content/2.Api/1.ProviderControlsRunAll.md
Normal 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;
|
||||
};
|
||||
```
|
66
.docs/content/2.Api/2.ProviderControlsrunSourceScraper.md
Normal file
66
.docs/content/2.Api/2.ProviderControlsrunSourceScraper.md
Normal 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;
|
||||
};
|
||||
```
|
44
.docs/content/2.Api/3.ProviderControlsrunEmbedScraper.md
Normal file
44
.docs/content/2.Api/3.ProviderControlsrunEmbedScraper.md
Normal 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;
|
||||
};
|
||||
```
|
25
.docs/content/2.Api/4.ProviderControlslistSources.md
Normal file
25
.docs/content/2.Api/4.ProviderControlslistSources.md
Normal 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>;
|
||||
};
|
||||
```
|
25
.docs/content/2.Api/5.ProviderControlslistEmbeds.md
Normal file
25
.docs/content/2.Api/5.ProviderControlslistEmbeds.md
Normal 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>;
|
||||
};
|
||||
```
|
24
.docs/content/2.Api/6.ProviderControlsgetMetadata.md
Normal file
24
.docs/content/2.Api/6.ProviderControlsgetMetadata.md
Normal 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>;
|
||||
};
|
||||
```
|
20
.docs/content/2.Api/7.makeStandardFetcher.md
Normal file
20
.docs/content/2.Api/7.makeStandardFetcher.md
Normal 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;
|
||||
```
|
23
.docs/content/2.Api/8.makeSimpleProxyFetcher.md
Normal file
23
.docs/content/2.Api/8.makeSimpleProxyFetcher.md
Normal 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;
|
||||
```
|
2
.docs/content/2.Api/_dir.yml
Normal file
2
.docs/content/2.Api/_dir.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
icon: ph:file-code-fill
|
||||
navigation.redirect: /api/makeproviders
|
21
.docs/nuxt.config.ts
Executable file
21
.docs/nuxt.config.ts
Executable 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
19466
.docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
.docs/package.json
Normal file → Executable file
18
.docs/package.json
Normal file → Executable file
@@ -1,9 +1,21 @@
|
||||
{
|
||||
"name": "providers-docs",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vitepress dev .",
|
||||
"build": "vitepress build ."
|
||||
"dev": "nuxi dev",
|
||||
"build": "nuxi build",
|
||||
"generate": "nuxi generate",
|
||||
"preview": "nuxi preview",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"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
BIN
.docs/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
8
.docs/renovate.json
Executable file
8
.docs/renovate.json
Executable file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nuxtjs"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
@@ -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).
|
@@ -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)
|
||||
---
|
||||
|
||||
|
@@ -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
18
.docs/tokens.config.ts
Normal 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
3
.docs/tsconfig.json
Executable file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
@@ -3,7 +3,7 @@ module.exports = {
|
||||
browser: true,
|
||||
},
|
||||
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',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
@@ -33,7 +33,7 @@ module.exports = {
|
||||
'no-eval': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'no-param-reassign': ["error", { "props": false }],
|
||||
'no-param-reassign': ['error', { props: false }],
|
||||
'prefer-destructuring': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'import/extensions': [
|
||||
|
33
.github/workflows/docs.yml
vendored
33
.github/workflows/docs.yml
vendored
@@ -7,28 +7,33 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install packages
|
||||
run: cd .docs && npm ci
|
||||
- name: Install packages
|
||||
working-directory: ./.docs
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: cd .docs && npm run build
|
||||
- name: Build project
|
||||
working-directory: ./.docs
|
||||
run: npm run generate
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: ./.docs/.vitepress/dist
|
||||
- name: Upload production-ready build files
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: ./.docs/.output/public
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: build
|
||||
permissions:
|
||||
pages: write
|
||||
|
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -27,5 +27,8 @@ jobs:
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run build && npm run test:integration
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
@@ -4,10 +4,11 @@ package that holds all providers of movie-web.
|
||||
Feel free to use for your own projects.
|
||||
|
||||
features:
|
||||
|
||||
- scrape popular streaming websites
|
||||
- works in both browser and server-side
|
||||
|
||||
Visit documentation here: https://providers.docs.movie-web.app/
|
||||
|
||||
## 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
|
||||
|
||||
@@ -32,6 +33,3 @@ Example testing the FlixHQ source on the movie "Spirited Away"
|
||||
```bash
|
||||
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
42
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@movie-web/providers",
|
||||
"version": "0.0.12",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@movie-web/providers",
|
||||
"version": "0.0.12",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
@@ -32,6 +32,7 @@
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "^2.6.2",
|
||||
"spinnies": "^0.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
@@ -3511,9 +3512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-func-name": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -4724,6 +4725,7 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -4742,17 +4744,20 @@
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
@@ -5120,24 +5125,6 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -6157,6 +6144,11 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@movie-web/providers",
|
||||
"version": "0.0.12",
|
||||
"version": "1.0.1",
|
||||
"description": "Package that contains all the providers of movie-web",
|
||||
"main": "./lib/index.umd.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
@@ -32,12 +32,13 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/movie-web/providers/issues"
|
||||
},
|
||||
"homepage": "https://github.com/movie-web/providers#readme",
|
||||
"homepage": "https://providers.docs.movie-web.app/",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"test:dev": "ts-node ./src/dev-cli.ts",
|
||||
"test:watch": "vitest",
|
||||
"test:integration": "node ./tests/cjs && node ./tests/esm",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint --ext .ts,.js src/",
|
||||
"lint:fix": "eslint --fix --ext .ts,.js src/",
|
||||
@@ -62,6 +63,7 @@
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"prettier": "^2.6.2",
|
||||
"spinnies": "^0.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
|
@@ -2,12 +2,9 @@ import util from 'node:util';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { program } from 'commander';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import dotenv from 'dotenv';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { prompt } from 'enquirer';
|
||||
import nodeFetch from 'node-fetch';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import Spinnies from 'spinnies';
|
||||
|
||||
import { MetaOutput, MovieMedia, ProviderControls, ShowMedia, makeProviders, makeStandardFetcher, targets } from '.';
|
||||
@@ -284,7 +281,7 @@ async function processOptions(options: CommandLineArguments) {
|
||||
let fetcher;
|
||||
|
||||
if (options.fetcher === 'native') {
|
||||
fetcher = makeStandardFetcher(fetch as any);
|
||||
fetcher = makeStandardFetcher(fetch);
|
||||
} else {
|
||||
fetcher = makeStandardFetcher(nodeFetch);
|
||||
}
|
||||
|
22
src/fetchers/fetch.ts
Normal file
22
src/fetchers/fetch.ts
Normal 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>;
|
@@ -1,6 +1,5 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { makeFullUrl } from '@/fetchers/common';
|
||||
import { FetchLike } from '@/fetchers/fetch';
|
||||
import { makeStandardFetcher } from '@/fetchers/standardFetch';
|
||||
import { Fetcher } from '@/fetchers/types';
|
||||
|
||||
@@ -10,7 +9,7 @@ const headerMap: Record<string, string> = {
|
||||
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 proxiedFetch: Fetcher = async (url, ops) => {
|
||||
const fullUrl = makeFullUrl(url, ops);
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { serializeBody } from '@/fetchers/body';
|
||||
import { makeFullUrl } from '@/fetchers/common';
|
||||
import { FetchLike } from '@/fetchers/fetch';
|
||||
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 fullUrl = makeFullUrl(url, ops);
|
||||
const seralizedBody = serializeBody(ops.body);
|
||||
|
@@ -7,6 +7,7 @@ export type Flags = (typeof flags)[keyof typeof flags];
|
||||
export const targets = {
|
||||
BROWSER: 'browser',
|
||||
NATIVE: 'native',
|
||||
ALL: 'all',
|
||||
} as const;
|
||||
|
||||
export type Targets = (typeof targets)[keyof typeof targets];
|
||||
@@ -22,6 +23,9 @@ export const targetToFeatures: Record<Targets, FeatureMap> = {
|
||||
native: {
|
||||
requires: [],
|
||||
},
|
||||
all: {
|
||||
requires: [],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function getTargetFeatures(target: Targets): FeatureMap {
|
||||
|
@@ -1,22 +1,25 @@
|
||||
import { Embed, Sourcerer } from '@/providers/base';
|
||||
import { febBoxScraper } from '@/providers/embeds/febBox';
|
||||
import { mixdropScraper } from '@/providers/embeds/mixdrop';
|
||||
import { mp4uploadScraper } from '@/providers/embeds/mp4upload';
|
||||
import { streamsbScraper } from '@/providers/embeds/streamsb';
|
||||
import { upcloudScraper } from '@/providers/embeds/upcloud';
|
||||
import { upstreamScraper } from '@/providers/embeds/upstream';
|
||||
import { flixhqScraper } from '@/providers/sources/flixhq/index';
|
||||
import { goMoviesScraper } from '@/providers/sources/gomovies/index';
|
||||
import { kissAsianScraper } from '@/providers/sources/kissasian/index';
|
||||
import { remotestreamScraper } from '@/providers/sources/remotestream';
|
||||
import { superStreamScraper } from '@/providers/sources/superstream/index';
|
||||
import { zoechipScraper } from '@/providers/sources/zoechip';
|
||||
|
||||
import { showBoxScraper } from './sources/showbox';
|
||||
|
||||
export function gatherAllSources(): Array<Sourcerer> {
|
||||
// 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> {
|
||||
// all embeds are gathered here
|
||||
return [upcloudScraper, mp4uploadScraper, streamsbScraper, febBoxScraper];
|
||||
return [upcloudScraper, mp4uploadScraper, streamsbScraper, upstreamScraper, febBoxScraper, mixdropScraper];
|
||||
}
|
||||
|
52
src/providers/embeds/mixdrop.ts
Normal file
52
src/providers/embeds/mixdrop.ts
Normal 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/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
35
src/providers/embeds/upstream.ts
Normal file
35
src/providers/embeds/upstream.ts
Normal 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');
|
||||
},
|
||||
});
|
71
src/providers/sources/zoechip/common.ts
Normal file
71
src/providers/sources/zoechip/common.ts
Normal 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,
|
||||
};
|
||||
}
|
13
src/providers/sources/zoechip/index.ts
Normal file
13
src/providers/sources/zoechip/index.ts
Normal 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,
|
||||
});
|
12
src/providers/sources/zoechip/scrape-movie.ts
Normal file
12
src/providers/sources/zoechip/scrape-movie.ts
Normal 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);
|
||||
}
|
23
src/providers/sources/zoechip/scrape-show.ts
Normal file
23
src/providers/sources/zoechip/scrape-show.ts
Normal 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);
|
||||
}
|
126
src/providers/sources/zoechip/scrape.ts
Normal file
126
src/providers/sources/zoechip/scrape.ts
Normal 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;
|
||||
}
|
111
src/providers/sources/zoechip/search.ts
Normal file
111
src/providers/sources/zoechip/search.ts
Normal 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;
|
||||
}
|
@@ -3,9 +3,10 @@ import { Flags } from '@/main/targets';
|
||||
export type StreamFile = {
|
||||
type: 'mp4';
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type Qualities = '360' | '480' | '720' | '1080';
|
||||
export type Qualities = 'unknown' | '360' | '480' | '720' | '1080';
|
||||
|
||||
export type FileBasedStream = {
|
||||
type: 'file';
|
||||
|
3
tests/README.md
Normal file
3
tests/README.md
Normal 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
2
tests/cjs/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
require('../../lib/index.umd');
|
||||
console.log('import successful!');
|
4
tests/cjs/package.json
Normal file
4
tests/cjs/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "index.js",
|
||||
"type": "commonjs"
|
||||
}
|
2
tests/esm/index.mjs
Normal file
2
tests/esm/index.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
import '../../lib/index.mjs';
|
||||
console.log('import successful!');
|
4
tests/esm/package.json
Normal file
4
tests/esm/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "index.mjs",
|
||||
"type": "module"
|
||||
}
|
Reference in New Issue
Block a user