Initial commit
This commit is contained in:
commit
6e86adb0e6
23 files changed
+6141
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,337 @@
|
||||
# Nuvio Provider Development Guide
|
||||
|
||||
This comprehensive guide covers everything you need to know to build, debug, and publish streaming providers for the Nuvio app.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [Getting Started](#2-getting-started)
|
||||
- [Setup](#setup)
|
||||
- [Architecture](#architecture)
|
||||
3. [The Provider Template](#3-the-provider-template)
|
||||
4. [Development Workflows](#4-development-workflows)
|
||||
- [Workflow A: Multi-File (Recommended)](#workflow-a-multi-file-recommended)
|
||||
- [Workflow B: Single-File (Legacy/Simple)](#workflow-b-single-file-legacysimple)
|
||||
5. [The Build System](#5-the-build-system)
|
||||
- [Bundling Source Providers](#bundling-source-providers)
|
||||
- [Transpiling Async/Await](#transpiling-asyncawait)
|
||||
- [Watch Mode](#watch-mode)
|
||||
6. [API Reference](#6-api-reference)
|
||||
7. [Testing & Debugging](#7-testing--debugging)
|
||||
8. [Publishing](#8-publishing)
|
||||
9. [FAQ & Troubleshooting](#9-faq--troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
A **Nuvio Provider** is a JavaScript module responsible for finding video streams. When a user taps a movie in the app, the provider receives the media details (TMDB ID, title, etc.) and returns a list of playable URLs.
|
||||
|
||||
Providers run locally on the user's device. The Nuvio app uses the **Hermes** JavaScript engine.
|
||||
|
||||
**Crucial Limitation:** Hermes does not natively support `async/await` syntax inside dynamically loaded code (plugins).
|
||||
**Our Solution:** We provide a build script that automatically transpiles your modern `async/await` code into generator functions that Hermes can execute safely.
|
||||
|
||||
**Also Important (Runtime Differences):** Local Node.js tests can pass even when the provider fails in-app.
|
||||
The Nuvio runtime is React Native + Hermes, so many Node-specific APIs/modules are not available (for example Node built-ins like `crypto`, and some crypto libraries that assume a Node/browser environment such as `node-forge`).
|
||||
If your provider uses encryption/decryption or heavy parsing dependencies, always test it in the Nuvio app (Plugin Tester) even if it works locally.
|
||||
|
||||
---
|
||||
|
||||
## 2. Getting Started
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/tapframe/nuvio-providers.git
|
||||
cd nuvio-providers
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
This installs the build tools (`esbuild`) required for transpilation.
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
- **`src/`**: The workspace for modern, multi-file providers.
|
||||
- **`providers/`**: The distribution folder. The app reads files from here. **Do not edit these files manually if they were built from `src/`.**
|
||||
- **`build.js`**: The utility script that builds and transpiles your code.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Provider Template
|
||||
|
||||
We have included a starter template in `src/_template`. To create a new provider:
|
||||
|
||||
1. **Copy the template**
|
||||
```bash
|
||||
cp -r src/_template src/my-new-provider
|
||||
```
|
||||
|
||||
2. **Rename/Edit files**
|
||||
- `src/my-new-provider/index.js`: The main entry point.
|
||||
- `src/my-new-provider/http.js`: Helper for network requests.
|
||||
- `src/my-new-provider/extractor.js`: Your scraping logic.
|
||||
|
||||
---
|
||||
|
||||
## 4. Development Workflows
|
||||
|
||||
### Workflow A: Multi-File (Recommended)
|
||||
|
||||
Best for complex providers. You write modern code in `src/`, split across multiple files.
|
||||
|
||||
1. **Develpop in `src/myprovider/`**.
|
||||
You can use `import`/`export` and `async/await` freely.
|
||||
```javascript
|
||||
// src/myprovider/index.js
|
||||
import { getStream } from './extractor.js';
|
||||
|
||||
async function getStreams(tmdbId, ...) {
|
||||
return await getStream(tmdbId);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Build**
|
||||
```bash
|
||||
node build.js myprovider
|
||||
```
|
||||
This bundles everything into a single file at `providers/myprovider.js` and transpiles the async code.
|
||||
|
||||
### Workflow B: Single-File (Legacy/Simple)
|
||||
|
||||
If you have a simple script or are porting a provider from another project, you might have a single file in `providers/`.
|
||||
|
||||
1. **Develop in `providers/myprovider.js`**.
|
||||
If you use `async/await`, it will **crash** the app unless you process it.
|
||||
|
||||
2. **Transpile**
|
||||
Run the transpile command to convert the async code in-place:
|
||||
```bash
|
||||
node build.js --transpile myprovider.js
|
||||
```
|
||||
This rewrites `providers/myprovider.js` with Hermes-compatible code.
|
||||
|
||||
---
|
||||
|
||||
## 5. The Build System
|
||||
|
||||
The `build.js` script is your primary tool. It handles two main jobs:
|
||||
1. **Bundling**: Combines multiple files from `src/` into one.
|
||||
2. **Transpiling**: Converts ES2017+ async/await into ES2016 Generators.
|
||||
|
||||
### Bundling Source Providers
|
||||
|
||||
Usage: `node build.js [provider_names...]`
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `node build.js` | Builds **ALL** providers found in `src/`. |
|
||||
| `node build.js vidlink` | Builds only the `src/vidlink` provider. |
|
||||
| `node build.js vidlink castle` | Builds multiple specific providers. |
|
||||
|
||||
**Output**: Creates `providers/<name>.js`.
|
||||
|
||||
### Transpiling Async/Await (Single Files)
|
||||
|
||||
If you have a standalone file in `providers/` that uses `async/await`, you must transpile it.
|
||||
|
||||
Usage: `node build.js --transpile [filenames...]`
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `node build.js --transpile` | Scans `providers/` for single files using async and transpiles them all. |
|
||||
| `node build.js --transpile old-scraper` | Transpiles `providers/old-scraper.js` in-place. |
|
||||
| `node build.js --transpile file1 file2` | Transpiles multiple specific files. |
|
||||
|
||||
**Note**: This overwrites the file with the transpiled version. The original source is lost unless you keep a backup or use Git. This is why **Workflow A (src folder)** is recommended, as it keeps your source code separate from the build artifact.
|
||||
|
||||
### Watch Mode
|
||||
|
||||
Automatically rebuilds source providers when you modify files in `src/`.
|
||||
|
||||
```bash
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
### Minification
|
||||
|
||||
By default, builds keep code readable for debugging. You can enable minification to reduce file size.
|
||||
|
||||
Usage: `node build.js --minify [provider_names...]`
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `node build.js --minify` | Builds **ALL** providers with minification. |
|
||||
| `node build.js --minify vidlink` | Builds only `vidlink` provider, minified. |
|
||||
| `node build.js --minify vidlink castle` | Builds multiple providers, all minified. |
|
||||
|
||||
#### Advantages of Minification
|
||||
|
||||
- **Smaller File Size**: ~50% reduction for providers with heavy dependencies (e.g., using `node-forge`, `cheerio`).
|
||||
- Example: A 1.0 MB unminified bundle becomes ~473 KB when minified.
|
||||
- **Faster Load Time**: Smaller files load quicker in the Nuvio app and over network transfers.
|
||||
- **Reduced Storage**: Less disk space consumed on user devices.
|
||||
- **Production Ready**: Recommended for final releases.
|
||||
|
||||
#### Disadvantages of Minification
|
||||
|
||||
- **Hard to Debug**: Variable and function names are mangled (e.g., `getStreams()` → `u()`), making it difficult to troubleshoot errors from crash reports or logs.
|
||||
- **Longer Build Time**: Minification adds a slight overhead to the build process.
|
||||
- **Stack Traces Unreadable**: Error messages won't map back to original function names.
|
||||
|
||||
#### Recommendation
|
||||
|
||||
- **Development**: Use unminified builds (`node build.js`) for easier debugging.
|
||||
- **Testing**: Test both minified and unminified versions before publishing.
|
||||
- **Production/Release**: Use minified builds (`node build.js --minify`) for deployment to users.
|
||||
|
||||
---
|
||||
|
||||
## 6. API Reference
|
||||
|
||||
Your provider must export a `getStreams` function.
|
||||
|
||||
```javascript
|
||||
/*
|
||||
* @param {string} tmdbId - The TMDB ID (e.g., "550")
|
||||
* @param {string} mediaType - "movie" or "tv"
|
||||
* @param {number} season - Season number (1-based), null for movies
|
||||
* @param {number} episode - Episode number (1-based), null for movies
|
||||
* @returns {Promise<Array>} - List of streams
|
||||
*/
|
||||
async function getStreams(tmdbId, mediaType, season, episode) { ... }
|
||||
```
|
||||
|
||||
### Stream Object
|
||||
|
||||
```javascript
|
||||
{
|
||||
"name": "MyProvider", // Short identifier
|
||||
"title": "1080p Stream", // Display name
|
||||
"url": "https://server.com/...", // Playable URL
|
||||
"quality": "1080p", // 4K, 1080p, 720p, CAM
|
||||
"headers": { // (Optional)
|
||||
"User-Agent": "Key for playback",
|
||||
"Referer": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing & Debugging
|
||||
|
||||
While local Node.js scripts are useful for initial logic verification, providers must be tested within the Nuvio application to ensure compatibility with the Hermes engine and the app's runtime environment.
|
||||
|
||||
### 7.1. Local Logic Verification (Node.js)
|
||||
|
||||
Create a temporary test script (e.g., `test.js`) to verify your provider's scraping logic on your computer.
|
||||
|
||||
```javascript
|
||||
const { getStreams } = require('./providers/myprovider.js');
|
||||
|
||||
async function run() {
|
||||
console.log("Fetching streams...");
|
||||
try {
|
||||
const streams = await getStreams('550', 'movie'); // Fight Club
|
||||
console.log(streams);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
run();
|
||||
```
|
||||
|
||||
Run it using:
|
||||
```bash
|
||||
node test.js
|
||||
```
|
||||
|
||||
### 7.2. In-App Testing (Plugin Tester)
|
||||
|
||||
The **Plugin Tester** is a dedicated developer tool within the Nuvio app that allows you to load, run, and debug providers directly on your device interactively.
|
||||
|
||||
#### Prerequisites
|
||||
1. **Get the App**: You need the **debug version** of Nuvio.
|
||||
- **Download**: Get the latest `debug.apk` from the **Releases** tab on GitHub.
|
||||
- **Build**: Or run `npx expo run:android` / `npx expo run:ios` locally.
|
||||
> *Note: Production versions do not include the Plugin Tester.*
|
||||
|
||||
2. Ensure your computer and mobile device are on the same Wi-Fi network.
|
||||
3. Start the local development server in this repository:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
This serves your `providers/` directory and `manifest.json` over HTTP (e.g., `http://192.168.1.X:3000`).
|
||||
|
||||
#### Accessing the Plugin Tester
|
||||
1. Open the Nuvio application.
|
||||
2. Navigate to **Settings**.
|
||||
3. Scroll down to the **Developer Section** and select **Plugin Tester**.
|
||||
|
||||
#### Testing Individual Providers
|
||||
The "Individual Plugin" tab is designed for rapid iteration on a single provider script.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Code Requirements:** You must use the **compiled/bundled** file (found in `providers/`) or a standalone **single-file** provider.
|
||||
> The app **cannot** execute source files that use `import` to load other local files (e.g., from `src/`). If you are using the multi-file workflow, you must build your provider first (`node build.js myprovider`) and test the generated output file.
|
||||
|
||||
1. **Load Source**:
|
||||
- **From URL**: Enter the direct URL to your *compiled* provider file hosted by your local server (e.g., `http://192.168.1.5:3000/providers/myprovider.js`) and tap **Load**.
|
||||
- **Direct Input**: Alternatively, paste your *compiled* provider code directly into the code editor.
|
||||
2. **Parameters**: Set the test parameters (TMDB ID, Media Type, Season, Episode).
|
||||
3. **Run Test**: Tap the **Run Test** button.
|
||||
4. **View Results**:
|
||||
- **Logs**: Check the "Logs" tab for `console.log` output and errors.
|
||||
- **Results**: View the list of discovered streams in the "Results" tab.
|
||||
- **Playback**: Tap the **Play** button on any stream result to verify that the URL is playable in the native player (KSPlayer on iOS, AndroidVideoPlayer on Android).
|
||||
|
||||
#### Testing Repositories
|
||||
The "Repo Tester" tab allows you to validate an entire plugin repository manifest.
|
||||
|
||||
1. Enter your local manifest URL (e.g., `http://192.168.1.5:3000/manifest.json`).
|
||||
2. Tap **Fetch Manifest** to load the list of available providers.
|
||||
3. Tap **Test All** to run a connectivity test on all enabled providers in the manifest, or test specific providers individually.
|
||||
|
||||
> [!NOTE]
|
||||
> The Plugin Tester behaves exactly like the production app environment (Hermes), so if a provider works here, it will work for users.
|
||||
|
||||
---
|
||||
|
||||
## 8. Publishing
|
||||
|
||||
1. **Build your provider**: Ensure `providers/myprovider.js` is up to date.
|
||||
```bash
|
||||
node build.js myprovider
|
||||
```
|
||||
2. **Update Manifest**: Add your provider entry to `manifest.json`.
|
||||
3. **Commit & Push**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add new provider"
|
||||
git push
|
||||
```
|
||||
|
||||
Users can then use your raw GitHub repository URL to load the plugins in Nuvio.
|
||||
|
||||
---
|
||||
|
||||
## 9. FAQ & Troubleshooting
|
||||
|
||||
### Error: `SyntaxError: async functions are unsupported`
|
||||
**Cause**: The app running on Hermes cannot execute `async function` directly in plugins.
|
||||
**Fix**: You forgot to build/transpile.
|
||||
- If using `src/`: Run `node build.js myprovider`.
|
||||
- If using single file: Run `node build.js --transpile myprovider.js`.
|
||||
|
||||
### Error: `fetch is not defined` (in local testing)
|
||||
**Cause**: Node.js (before v18) doesn't have native `fetch`.
|
||||
**Fix**: Use Node v18+, or our build environment handles this for the app. For local testing, ensure you are on a recent Node version.
|
||||
|
||||
### The app crashes when loading my provider
|
||||
**Cause**: Syntax error or unhandled exception at the root level.
|
||||
**Fix**: Check your `index.js`. Ensure you are not doing heavy work (like networking) at the top level. All logic must be inside `getStreams`.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,271 @@
|
||||
# Nuvio Providers
|
||||
|
||||
A collection of streaming providers for the Nuvio app. Providers are JavaScript modules that fetch streams from various sources.
|
||||
|
||||
📖 **[Read the Comprehensive Developer Guide](DOCUMENTATION.md)**
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using in Nuvio App
|
||||
|
||||
1. Open **Nuvio** > **Settings** > **Plugins**
|
||||
2. Add this repository URL:
|
||||
```
|
||||
https://raw.githubusercontent.com/yoruix/nuvio-providers/refs/heads/main/manifest.json
|
||||
```
|
||||
3. Refresh and enable the providers you want
|
||||
4. **Developer Mode**: To test local changes, run `npm start` on your computer.
|
||||
> ⚠️ **Important:** You must use the **development build** of Nuvio (`npx expo run:android` or `npx expo run:ios`). Some providers may work locally but fail in React Native.
|
||||
- Go to **Settings** > **Developer** > **Plugin Tester** in the app.
|
||||
- Enter your local server URL (e.g., `http://192.168.1.5:3000/manifest.json`).
|
||||
- You can also test individual provider URLs here.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
nuvio-providers/
|
||||
├── src/ # Source files (multi-file development)
|
||||
│ ├── vixsrc/
|
||||
│ │ ├── index.js # Main entry point
|
||||
│ │ ├── extractor.js # Stream extraction
|
||||
│ │ ├── http.js # HTTP utilities
|
||||
│ │ └── ...
|
||||
│ └── uhdmovies/
|
||||
│ └── ...
|
||||
│
|
||||
├── providers/ # Output directory (ready-to-use files)
|
||||
│ ├── vixsrc.js # Bundled from src/vixsrc/
|
||||
│ ├── uhdmovies.js
|
||||
│ └── ...
|
||||
│
|
||||
├── manifest.json # Provider registry
|
||||
├── build.js # Build script
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
There are two ways to create providers:
|
||||
|
||||
### Option 1: Single-File Provider
|
||||
|
||||
For simple providers, you can create a single JavaScript file directly in the `providers/` directory.
|
||||
|
||||
**Important:** The app's JavaScript engine (Hermes) has limitations with `async/await` in dynamic code.
|
||||
- **Recommended**: Use Promise chains (`.then()`).
|
||||
- **Alternative**: Use `async/await` and run the transpiler command (see below).
|
||||
|
||||
**Example (Promise Chains):**
|
||||
```javascript
|
||||
// providers/myprovider.js
|
||||
|
||||
function getStreams(tmdbId, mediaType, season, episode) {
|
||||
console.log(`[MyProvider] Fetching ${mediaType} ${tmdbId}`);
|
||||
|
||||
return fetch(`https://api.example.com/streams/${tmdbId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
return data.streams.map(s => ({
|
||||
name: "MyProvider",
|
||||
title: s.title,
|
||||
url: s.url,
|
||||
quality: s.quality
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[MyProvider] Error:', error.message);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getStreams };
|
||||
```
|
||||
|
||||
To register the provider, add it to `manifest.json`:
|
||||
```json
|
||||
{
|
||||
"id": "myprovider",
|
||||
"name": "My Provider",
|
||||
"filename": "providers/myprovider.js",
|
||||
"supportedTypes": ["movie", "tv"],
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Multi-File Provider (Recommended)
|
||||
|
||||
For complex providers, use the `src/` directory. This allows you to split code into multiple files. The build script automatically handles bundling and `async/await` transpilation.
|
||||
|
||||
1. **Create source folder:**
|
||||
```bash
|
||||
mkdir -p src/myprovider
|
||||
```
|
||||
|
||||
2. **Create entry point** (`src/myprovider/index.js`):
|
||||
```javascript
|
||||
import { fetchPage } from './http.js';
|
||||
import { extractStreams } from './extractor.js';
|
||||
|
||||
// async/await is fully supported here
|
||||
async function getStreams(tmdbId, mediaType, season, episode) {
|
||||
const page = await fetchPage(tmdbId, mediaType, season, episode);
|
||||
return extractStreams(page);
|
||||
}
|
||||
|
||||
module.exports = { getStreams };
|
||||
```
|
||||
|
||||
3. **Build:**
|
||||
```bash
|
||||
node build.js myprovider
|
||||
```
|
||||
|
||||
This generates `providers/myprovider.js`.
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
### Build Source Providers
|
||||
Bundles files from `src/<provider>/` into `providers/<provider>.js`.
|
||||
|
||||
```bash
|
||||
# Build specific provider
|
||||
node build.js vixsrc
|
||||
|
||||
# Build multiple
|
||||
node build.js vixsrc uhdmovies
|
||||
|
||||
# Build all source providers
|
||||
node build.js
|
||||
```
|
||||
|
||||
### Transpile Single-File Providers
|
||||
If you wrote a single-file provider using `async/await`, you must transpile it for compatibility.
|
||||
|
||||
```bash
|
||||
# Transpile specific file
|
||||
node build.js --transpile myprovider.js
|
||||
|
||||
# Transpile all applicable files in providers/
|
||||
node build.js --transpile
|
||||
```
|
||||
|
||||
### Watch Mode
|
||||
Automatically rebuilds when files change.
|
||||
```bash
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Create a test script to identify issues before loading into the app.
|
||||
|
||||
```javascript
|
||||
// test-myprovider.js
|
||||
const { getStreams } = require('./providers/myprovider.js');
|
||||
|
||||
async function test() {
|
||||
console.log('Testing...');
|
||||
const streams = await getStreams('872585', 'movie'); // Oppenheimer ID
|
||||
console.log('Streams found:', streams.length);
|
||||
}
|
||||
|
||||
test();
|
||||
```
|
||||
|
||||
Run with Node.js:
|
||||
```bash
|
||||
node test-myprovider.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stream Object Format
|
||||
|
||||
Providers must return an array of stream objects:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: "Provider Name", // Provider identifier
|
||||
title: "1080p Stream", // Stream description
|
||||
url: "https://...", // Direct stream URL (m3u8, mp4, mkv)
|
||||
quality: "1080p", // Quality label
|
||||
size: "2.5 GB", // Optional file size
|
||||
headers: { // Optional headers for playback
|
||||
"Referer": "https://source.com",
|
||||
"User-Agent": "Mozilla/5.0..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Available Modules
|
||||
|
||||
Providers have access to these modules via `require()`:
|
||||
|
||||
| Module | Usage |
|
||||
|--------|-------|
|
||||
| `cheerio-without-node-native` | HTML parsing |
|
||||
| `crypto-js` | Encryption/decryption |
|
||||
| `axios` | HTTP requests |
|
||||
|
||||
Native `fetch` and `console` are also available globally.
|
||||
|
||||
---
|
||||
|
||||
## Manifest Options
|
||||
|
||||
The `manifest.json` file controls provider settings.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "unique-id",
|
||||
"name": "Display Name",
|
||||
"description": "Short description",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"supportedTypes": ["movie", "tv"],
|
||||
"filename": "providers/file.js",
|
||||
"enabled": true,
|
||||
"logo": "https://url/to/logo.png",
|
||||
"contentLanguage": ["en", "hi"],
|
||||
"formats": ["mkv", "mp4"],
|
||||
"limited": false,
|
||||
"disabledPlatforms": ["ios"],
|
||||
"supportsExternalPlayer": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create a branch**: `git checkout -b add-myprovider`
|
||||
3. **Develop and test**
|
||||
4. **Build**: `node build.js myprovider`
|
||||
5. **Commit**: `git commit -m "Add MyProvider"`
|
||||
6. **Push and PR**
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the **GNU General Public License v3.0**.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
- **No content is hosted by this repository.**
|
||||
- Providers fetch publicly available content from third-party websites.
|
||||
- Users are responsible for compliance with local laws.
|
||||
- For DMCA concerns, contact the actual content hosts.
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for nuvio-providers
|
||||
*
|
||||
* Bundles each provider from src/<provider>/ into a single file at providers/<provider>.js
|
||||
*
|
||||
* Usage:
|
||||
* node build.js # Build all providers
|
||||
* node build.js vixsrc # Build only vixsrc
|
||||
* node build.js --minify # Build all with minification
|
||||
* node build.js --watch # Watch mode (requires nodemon)
|
||||
* node build.js --transpile # Transpile async/await in providers/
|
||||
*/
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const srcDir = path.join(__dirname, 'src');
|
||||
const outDir = path.join(__dirname, 'providers');
|
||||
|
||||
// Modules that the Nuvio app provides - don't bundle these
|
||||
const EXTERNAL_MODULES = [
|
||||
'cheerio-without-node-native',
|
||||
'react-native-cheerio',
|
||||
'cheerio',
|
||||
'crypto-js',
|
||||
'axios'
|
||||
];
|
||||
|
||||
// Get provider names from command line or discover all
|
||||
function getProvidersToBuild() {
|
||||
const args = process.argv.slice(2).filter(arg => !arg.startsWith('-'));
|
||||
|
||||
if (args.length > 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// Discover all provider folders in src/
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.error('❌ src/ directory not found. Create provider folders in src/<provider>/');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return fs.readdirSync(srcDir, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name);
|
||||
}
|
||||
|
||||
async function buildProvider(providerName, options = {}) {
|
||||
const providerDir = path.join(srcDir, providerName);
|
||||
const entryPoint = path.join(providerDir, 'index.js');
|
||||
const outFile = path.join(outDir, `${providerName}.js`);
|
||||
|
||||
if (!fs.existsSync(entryPoint)) {
|
||||
console.warn(`⚠️ Skipping ${providerName}: no src/${providerName}/index.js found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await esbuild.build({
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
outfile: outFile,
|
||||
format: 'cjs', // CommonJS for module.exports compatibility
|
||||
platform: 'neutral', // Works in both browser and node-like environments
|
||||
target: 'es2016', // Transpile async/await to generators for Hermes
|
||||
minify: options.minify || false, // Minify if --minify flag is set
|
||||
sourcemap: false,
|
||||
external: EXTERNAL_MODULES,
|
||||
banner: {
|
||||
js: `/**\n * ${providerName} - Built from src/${providerName}/\n * Generated: ${new Date().toISOString()}\n */`
|
||||
},
|
||||
logLevel: 'warning'
|
||||
});
|
||||
|
||||
const stats = fs.statSync(outFile);
|
||||
const sizeKB = (stats.size / 1024).toFixed(1);
|
||||
const minifyIndicator = options.minify ? ' (minified)' : '';
|
||||
console.log(`✅ ${providerName}.js (${sizeKB} KB)${minifyIndicator}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to build ${providerName}:`, err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Transpile a single file in providers/ (for developers writing single-file providers with async)
|
||||
async function transpileSingleFile(filename) {
|
||||
const inputPath = path.join(outDir, filename);
|
||||
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
console.warn(`⚠️ File not found: providers/${filename}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read original file
|
||||
const originalContent = fs.readFileSync(inputPath, 'utf-8');
|
||||
|
||||
// Check if it needs transpilation (has async/await)
|
||||
if (!originalContent.includes('async ') && !originalContent.includes('await ')) {
|
||||
console.log(`⏭️ ${filename} - no async/await, skipping`);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await esbuild.transform(originalContent, {
|
||||
loader: 'js',
|
||||
target: 'es2016', // Transpile async/await to generators
|
||||
format: 'cjs'
|
||||
});
|
||||
|
||||
// Write transpiled content back
|
||||
fs.writeFileSync(inputPath, result.code);
|
||||
|
||||
const stats = fs.statSync(inputPath);
|
||||
const sizeKB = (stats.size / 1024).toFixed(1);
|
||||
console.log(`✅ ${filename} transpiled (${sizeKB} KB)`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`❌ Failed to transpile ${filename}:`, err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const shouldMinify = args.includes('--minify');
|
||||
|
||||
// Handle --transpile flag for single-file providers
|
||||
if (args.includes('--transpile')) {
|
||||
const files = args.filter(a => a !== '--transpile' && !a.startsWith('-'));
|
||||
|
||||
if (files.length === 0) {
|
||||
// Transpile all .js files in providers/ that aren't from src/
|
||||
const srcProviders = fs.existsSync(srcDir)
|
||||
? fs.readdirSync(srcDir, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name + '.js')
|
||||
: [];
|
||||
|
||||
const allProviderFiles = fs.readdirSync(outDir)
|
||||
.filter(f => f.endsWith('.js') && !srcProviders.includes(f));
|
||||
|
||||
console.log(`\n🔄 Transpiling ${allProviderFiles.length} single-file provider(s)...\n`);
|
||||
|
||||
for (const file of allProviderFiles) {
|
||||
await transpileSingleFile(file);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n🔄 Transpiling ${files.length} file(s)...\n`);
|
||||
for (const file of files) {
|
||||
const filename = file.endsWith('.js') ? file : file + '.js';
|
||||
await transpileSingleFile(filename);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = getProvidersToBuild();
|
||||
|
||||
if (providers.length === 0) {
|
||||
console.log('No providers found in src/ directory.');
|
||||
console.log('Create a provider: mkdir -p src/myprovider && touch src/myprovider/index.js');
|
||||
return;
|
||||
}
|
||||
|
||||
const minifyLabel = shouldMinify ? ' (minified)' : '';
|
||||
console.log(`\n📦 Building ${providers.length} provider(s)${minifyLabel}...\n`);
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const provider of providers) {
|
||||
const result = await buildProvider(provider, { minify: shouldMinify });
|
||||
if (result) success++;
|
||||
else failed++;
|
||||
}
|
||||
|
||||
console.log(`\n✨ Done! ${success} built, ${failed} skipped/failed\n`);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Build failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "Nuvio Providers",
|
||||
"version": "1.0.4",
|
||||
"scrapers": [
|
||||
{
|
||||
"id": "miruro",
|
||||
"name": "Miruro",
|
||||
"description": "Anime streams from Miruro with subbed and dubbed sources.",
|
||||
"version": "1.0.0",
|
||||
"author": "Jānis L",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/miruro.js",
|
||||
"enabled": true,
|
||||
"hasSettings": false,
|
||||
"formats": [
|
||||
"m3u8",
|
||||
"mp4"
|
||||
],
|
||||
"logo": "https://www.miruro.to/icon-light-1024x1024.png",
|
||||
"contentLanguage": [
|
||||
"en",
|
||||
"ja"
|
||||
],
|
||||
"limited": false,
|
||||
"disabledPlatforms": [],
|
||||
"supportsExternalPlayer": true
|
||||
},
|
||||
{
|
||||
"id": "flixer",
|
||||
"name": "Flixer",
|
||||
"description": "Movies and TV streams from Flixer.",
|
||||
"version": "1.0.2",
|
||||
"author": "Jānis L",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/flixer.js",
|
||||
"enabled": true,
|
||||
"hasSettings": false,
|
||||
"formats": [
|
||||
"m3u8"
|
||||
],
|
||||
"logo": "https://flixer.gd/assets/images/logo.png",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
],
|
||||
"limited": false,
|
||||
"disabledPlatforms": [],
|
||||
"supportsExternalPlayer": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+2235
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "nuvio-providers",
|
||||
"version": "1.0.0",
|
||||
"description": "Providers for Nuvio Streaming",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"build:watch": "nodemon --watch src -e js,json --exec 'node build.js'",
|
||||
"build:provider": "node build.js",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cheerio": "^1.1.2",
|
||||
"cheerio-without-node-native": "^0.20.2",
|
||||
"esbuild": "^0.20.0",
|
||||
"nodemon": "^3.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"nuvio",
|
||||
"streaming",
|
||||
"providers"
|
||||
],
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* _template - Built from src/_template/
|
||||
* Generated: 2026-07-19T17:05:24.210Z
|
||||
*/
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// src/_template/extractor.js
|
||||
var import_cheerio_without_node_native = __toESM(require("cheerio-without-node-native"));
|
||||
function extractStreams(tmdbId, mediaType, season, episode) {
|
||||
return __async(this, null, function* () {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// src/_template/index.js
|
||||
function getStreams(tmdbId, mediaType, season, episode) {
|
||||
return __async(this, null, function* () {
|
||||
try {
|
||||
console.log(`[Template] Request: ${mediaType} ${tmdbId}`);
|
||||
const streams = yield extractStreams(tmdbId, mediaType, season, episode);
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error(`[Template] Error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
module.exports = { getStreams };
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* flixer - Built from src/flixer/
|
||||
* Generated: 2026-07-19T17:05:50.362Z
|
||||
*/
|
||||
var se=Object.defineProperty,ie=Object.defineProperties;var ce=Object.getOwnPropertyDescriptors;var G=Object.getOwnPropertySymbols;var ae=Object.prototype.hasOwnProperty,le=Object.prototype.propertyIsEnumerable;var K=(e,r,t)=>r in e?se(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,B=(e,r)=>{for(var t in r||(r={}))ae.call(r,t)&&K(e,t,r[t]);if(G)for(var t of G(r))le.call(r,t)&&K(e,t,r[t]);return e},S=(e,r)=>ie(e,ce(r));var m=(e,r,t)=>new Promise((n,o)=>{var c=a=>{try{i(t.next(a))}catch(h){o(h)}},s=a=>{try{i(t.throw(a))}catch(h){o(h)}},i=a=>a.done?n(a.value):Promise.resolve(a.value).then(c,s);i((t=t.apply(e,r)).next())});var $="https://flixer.gd",k="https://plsdontscrapemelove.flixer.gd",R={Accept:"text/plain",Origin:$,Referer:`${$}/`,"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"},b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fe=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];function v(e){let r=[];for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n>=55296&&n<=56319&&t+1<e.length){let o=e.charCodeAt(t+1);o>=56320&&o<=57343&&(n=65536+(n-55296<<10)+o-56320,t++)}n<128?r.push(n):n<2048?r.push(192|n>>6,128|n&63):n<65536?r.push(224|n>>12,128|n>>6&63,128|n&63):r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|n&63)}return new Uint8Array(r)}function p(e,r){return e>>>r|e<<32-r}function E(e){let r=e.length*8,t=Math.ceil((e.length+9)/64)*64,n=new Uint8Array(t);n.set(e),n[e.length]=128;let o=Math.floor(r/4294967296),c=r>>>0,s=t-8;n[s]=o>>>24&255,n[s+1]=o>>>16&255,n[s+2]=o>>>8&255,n[s+3]=o&255,n[s+4]=c>>>24&255,n[s+5]=c>>>16&255,n[s+6]=c>>>8&255,n[s+7]=c&255;let i=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],a=new Uint32Array(64);for(let l=0;l<n.length;l+=64){for(let x=0;x<16;x++){let y=l+x*4;a[x]=(n[y]<<24|n[y+1]<<16|n[y+2]<<8|n[y+3])>>>0}for(let x=16;x<64;x++){let y=p(a[x-15],7)^p(a[x-15],18)^a[x-15]>>>3,L=p(a[x-2],17)^p(a[x-2],19)^a[x-2]>>>10;a[x]=a[x-16]+y+a[x-7]+L>>>0}let[A,w,C,d,f,u,g,U]=i;for(let x=0;x<64;x++){let y=p(f,6)^p(f,11)^p(f,25),L=f&u^~f&g,j=U+y+L+fe[x]+a[x]>>>0,ne=p(A,2)^p(A,13)^p(A,22),re=A&w^A&C^w&C,oe=ne+re>>>0;U=g,g=u,u=f,f=d+j>>>0,d=C,C=w,w=A,A=j+oe>>>0}i[0]=i[0]+A>>>0,i[1]=i[1]+w>>>0,i[2]=i[2]+C>>>0,i[3]=i[3]+d>>>0,i[4]=i[4]+f>>>0,i[5]=i[5]+u>>>0,i[6]=i[6]+g>>>0,i[7]=i[7]+U>>>0}let h=new Uint8Array(32);for(let l=0;l<i.length;l++)h[l*4]=i[l]>>>24&255,h[l*4+1]=i[l]>>>16&255,h[l*4+2]=i[l]>>>8&255,h[l*4+3]=i[l]&255;return h}function P(e,r){let t=e instanceof Uint8Array?e:v(String(e));t.length>64&&(t=E(t));let n=r instanceof Uint8Array?r:v(String(r)),o=new Uint8Array(64+n.length),c=new Uint8Array(96);for(let s=0;s<64;s++){let i=s<t.length?t[s]:0;o[s]=i^54,c[s]=i^92}return o.set(n,64),c.set(E(o),64),E(c)}function q(e){let r="";for(let t=0;t<e.length;t+=3){let n=t+1<e.length,o=t+2<e.length,c=e[t]<<16|(n?e[t+1]:0)<<8|(o?e[t+2]:0);r+=b[c>>>18&63],r+=b[c>>>12&63],r+=n?b[c>>>6&63]:"=",r+=o?b[c&63]:"="}return r}function ue(){let e=new Uint8Array(16);for(let r=0;r<e.length;r++)e[r]=Math.floor(Math.random()*256);return q(e).replace(/[/+=]/g,"").slice(0,22)}var N=0,I=0;function xe(){return m(this,null,function*(){let e=Date.now();if(I&&e-I<3e5)return Math.floor((e+N)/1e3);try{let r=Date.now(),t=yield fetch(`${k}/api/time?t=${r}`,{headers:S(B({},R),{"Cache-Control":"no-cache"})});if(!t.ok)throw new Error(`HTTP ${t.status}`);let n=yield t.json();if(!n||typeof n.timestamp!="number")throw new Error("invalid response");let o=Date.now();return N=n.timestamp*1e3+(o-r)/2-o,I=o,Math.floor((o+N)/1e3)}catch(r){return Math.floor(Date.now()/1e3)}})}function H(e,r){return m(this,null,function*(){let t=yield xe(),n=ue(),o=`${e}:${t}:${n}:${r}`;return{"X-Api-Key":e,"X-Request-Timestamp":String(t),"X-Request-Nonce":n,"X-Request-Signature":q(P(e,o)),"X-Client-Fingerprint":"zerlgt","X-Fingerprint-Lite":"e9136c41504646444"}})}function _(t){return m(this,arguments,function*(e,r={}){let n=yield fetch(e,S(B({},r),{headers:B(B({},R),r.headers||{})}));if(!n.ok)throw new Error(`HTTP ${n.status} for ${e}`);return yield n.text()})}var he="iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAYAAAAZUZThAAAQAE";function Ae(...e){let r=0;for(let o of e)r+=o.length;let t=new Uint8Array(r),n=0;for(let o of e)t.set(o,n),n+=o.length;return t}function z(e){let r="";for(let t of e)r+=t.Line truncated
|
||||
@@ -0,0 +1,867 @@
|
||||
/**
|
||||
* miruro - Built from src/miruro/
|
||||
* Generated: 2026-07-19T17:01:21.823Z
|
||||
*/
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defProps = Object.defineProperties;
|
||||
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
||||
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __spreadValues = (a, b) => {
|
||||
for (var prop in b || (b = {}))
|
||||
if (__hasOwnProp.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
if (__getOwnPropSymbols)
|
||||
for (var prop of __getOwnPropSymbols(b)) {
|
||||
if (__propIsEnum.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
||||
var __async = (__this, __arguments, generator) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var fulfilled = (value) => {
|
||||
try {
|
||||
step(generator.next(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var rejected = (value) => {
|
||||
try {
|
||||
step(generator.throw(value));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||
step((generator = generator.apply(__this, __arguments)).next());
|
||||
});
|
||||
};
|
||||
|
||||
// node_modules/fflate/esm/browser.js
|
||||
var u8 = Uint8Array;
|
||||
var u16 = Uint16Array;
|
||||
var i32 = Int32Array;
|
||||
var fleb = new u8([
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
0,
|
||||
/* unused */
|
||||
0,
|
||||
0,
|
||||
/* impossible */
|
||||
0
|
||||
]);
|
||||
var fdeb = new u8([
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
5,
|
||||
5,
|
||||
6,
|
||||
6,
|
||||
7,
|
||||
7,
|
||||
8,
|
||||
8,
|
||||
9,
|
||||
9,
|
||||
10,
|
||||
10,
|
||||
11,
|
||||
11,
|
||||
12,
|
||||
12,
|
||||
13,
|
||||
13,
|
||||
/* unused */
|
||||
0,
|
||||
0
|
||||
]);
|
||||
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
||||
var freb = function(eb, start) {
|
||||
var b = new u16(31);
|
||||
for (var i = 0; i < 31; ++i) {
|
||||
b[i] = start += 1 << eb[i - 1];
|
||||
}
|
||||
var r = new i32(b[30]);
|
||||
for (var i = 1; i < 30; ++i) {
|
||||
for (var j = b[i]; j < b[i + 1]; ++j) {
|
||||
r[j] = j - b[i] << 5 | i;
|
||||
}
|
||||
}
|
||||
return { b, r };
|
||||
};
|
||||
var _a = freb(fleb, 2);
|
||||
var fl = _a.b;
|
||||
var revfl = _a.r;
|
||||
fl[28] = 258, revfl[258] = 28;
|
||||
var _b = freb(fdeb, 0);
|
||||
var fd = _b.b;
|
||||
var revfd = _b.r;
|
||||
var rev = new u16(32768);
|
||||
for (i = 0; i < 32768; ++i) {
|
||||
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
||||
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
||||
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
||||
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
||||
}
|
||||
var x;
|
||||
var i;
|
||||
var hMap = function(cd, mb, r) {
|
||||
var s = cd.length;
|
||||
var i = 0;
|
||||
var l = new u16(mb);
|
||||
for (; i < s; ++i) {
|
||||
if (cd[i])
|
||||
++l[cd[i] - 1];
|
||||
}
|
||||
var le = new u16(mb);
|
||||
for (i = 1; i < mb; ++i) {
|
||||
le[i] = le[i - 1] + l[i - 1] << 1;
|
||||
}
|
||||
var co;
|
||||
if (r) {
|
||||
co = new u16(1 << mb);
|
||||
var rvb = 15 - mb;
|
||||
for (i = 0; i < s; ++i) {
|
||||
if (cd[i]) {
|
||||
var sv = i << 4 | cd[i];
|
||||
var r_1 = mb - cd[i];
|
||||
var v = le[cd[i] - 1]++ << r_1;
|
||||
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
||||
co[rev[v] >> rvb] = sv;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
co = new u16(s);
|
||||
for (i = 0; i < s; ++i) {
|
||||
if (cd[i]) {
|
||||
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return co;
|
||||
};
|
||||
var flt = new u8(288);
|
||||
for (i = 0; i < 144; ++i)
|
||||
flt[i] = 8;
|
||||
var i;
|
||||
for (i = 144; i < 256; ++i)
|
||||
flt[i] = 9;
|
||||
var i;
|
||||
for (i = 256; i < 280; ++i)
|
||||
flt[i] = 7;
|
||||
var i;
|
||||
for (i = 280; i < 288; ++i)
|
||||
flt[i] = 8;
|
||||
var i;
|
||||
var fdt = new u8(32);
|
||||
for (i = 0; i < 32; ++i)
|
||||
fdt[i] = 5;
|
||||
var i;
|
||||
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
||||
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
||||
var max = function(a) {
|
||||
var m = a[0];
|
||||
for (var i = 1; i < a.length; ++i) {
|
||||
if (a[i] > m)
|
||||
m = a[i];
|
||||
}
|
||||
return m;
|
||||
};
|
||||
var bits = function(d, p, m) {
|
||||
var o = p / 8 | 0;
|
||||
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
||||
};
|
||||
var bits16 = function(d, p) {
|
||||
var o = p / 8 | 0;
|
||||
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
||||
};
|
||||
var shft = function(p) {
|
||||
return (p + 7) / 8 | 0;
|
||||
};
|
||||
var slc = function(v, s, e) {
|
||||
if (s == null || s < 0)
|
||||
s = 0;
|
||||
if (e == null || e > v.length)
|
||||
e = v.length;
|
||||
return new u8(v.subarray(s, e));
|
||||
};
|
||||
var ec = [
|
||||
"unexpected EOF",
|
||||
"invalid block type",
|
||||
"invalid length/literal",
|
||||
"invalid distance",
|
||||
"stream finished",
|
||||
"no stream handler",
|
||||
,
|
||||
// determined by compression function
|
||||
"no callback",
|
||||
"invalid UTF-8 data",
|
||||
"extra field too long",
|
||||
"date not in range 1980-2099",
|
||||
"filename too long",
|
||||
"stream finishing",
|
||||
"invalid zip data"
|
||||
// determined by unknown compression method
|
||||
];
|
||||
var err = function(ind, msg, nt) {
|
||||
var e = new Error(msg || ec[ind]);
|
||||
e.code = ind;
|
||||
if (Error.captureStackTrace)
|
||||
Error.captureStackTrace(e, err);
|
||||
if (!nt)
|
||||
throw e;
|
||||
return e;
|
||||
};
|
||||
var inflt = function(dat, st, buf, dict) {
|
||||
var sl = dat.length, dl = dict ? dict.length : 0;
|
||||
if (!sl || st.f && !st.l)
|
||||
return buf || new u8(0);
|
||||
var noBuf = !buf;
|
||||
var resize = noBuf || st.i != 2;
|
||||
var noSt = st.i;
|
||||
if (noBuf)
|
||||
buf = new u8(sl * 3);
|
||||
var cbuf = function(l2) {
|
||||
var bl = buf.length;
|
||||
if (l2 > bl) {
|
||||
var nbuf = new u8(Math.max(bl * 2, l2));
|
||||
nbuf.set(buf);
|
||||
buf = nbuf;
|
||||
}
|
||||
};
|
||||
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
||||
var tbts = sl * 8;
|
||||
do {
|
||||
if (!lm) {
|
||||
final = bits(dat, pos, 1);
|
||||
var type = bits(dat, pos + 1, 3);
|
||||
pos += 3;
|
||||
if (!type) {
|
||||
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
||||
if (t > sl) {
|
||||
if (noSt)
|
||||
err(0);
|
||||
break;
|
||||
}
|
||||
if (resize)
|
||||
cbuf(bt + l);
|
||||
buf.set(dat.subarray(s, t), bt);
|
||||
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
||||
continue;
|
||||
} else if (type == 1)
|
||||
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
||||
else if (type == 2) {
|
||||
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
||||
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
||||
pos += 14;
|
||||
var ldt = new u8(tl);
|
||||
var clt = new u8(19);
|
||||
for (var i = 0; i < hcLen; ++i) {
|
||||
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
||||
}
|
||||
pos += hcLen * 3;
|
||||
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
||||
var clm = hMap(clt, clb, 1);
|
||||
for (var i = 0; i < tl; ) {
|
||||
var r = clm[bits(dat, pos, clbmsk)];
|
||||
pos += r & 15;
|
||||
var s = r >> 4;
|
||||
if (s < 16) {
|
||||
ldt[i++] = s;
|
||||
} else {
|
||||
var c = 0, n = 0;
|
||||
if (s == 16)
|
||||
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
||||
else if (s == 17)
|
||||
n = 3 + bits(dat, pos, 7), pos += 3;
|
||||
else if (s == 18)
|
||||
n = 11 + bits(dat, pos, 127), pos += 7;
|
||||
while (n--)
|
||||
ldt[i++] = c;
|
||||
}
|
||||
}
|
||||
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
||||
lbt = max(lt);
|
||||
dbt = max(dt);
|
||||
lm = hMap(lt, lbt, 1);
|
||||
dm = hMap(dt, dbt, 1);
|
||||
} else
|
||||
err(1);
|
||||
if (pos > tbts) {
|
||||
if (noSt)
|
||||
err(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resize)
|
||||
cbuf(bt + 131072);
|
||||
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
||||
var lpos = pos;
|
||||
for (; ; lpos = pos) {
|
||||
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
||||
pos += c & 15;
|
||||
if (pos > tbts) {
|
||||
if (noSt)
|
||||
err(0);
|
||||
break;
|
||||
}
|
||||
if (!c)
|
||||
err(2);
|
||||
if (sym < 256)
|
||||
buf[bt++] = sym;
|
||||
else if (sym == 256) {
|
||||
lpos = pos, lm = null;
|
||||
break;
|
||||
} else {
|
||||
var add = sym - 254;
|
||||
if (sym > 264) {
|
||||
var i = sym - 257, b = fleb[i];
|
||||
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
||||
pos += b;
|
||||
}
|
||||
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
||||
if (!d)
|
||||
err(3);
|
||||
pos += d & 15;
|
||||
var dt = fd[dsym];
|
||||
if (dsym > 3) {
|
||||
var b = fdeb[dsym];
|
||||
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
||||
}
|
||||
if (pos > tbts) {
|
||||
if (noSt)
|
||||
err(0);
|
||||
break;
|
||||
}
|
||||
if (resize)
|
||||
cbuf(bt + 131072);
|
||||
var end = bt + add;
|
||||
if (bt < dt) {
|
||||
var shift = dl - dt, dend = Math.min(dt, end);
|
||||
if (shift + bt < 0)
|
||||
err(3);
|
||||
for (; bt < dend; ++bt)
|
||||
buf[bt] = dict[shift + bt];
|
||||
}
|
||||
for (; bt < end; ++bt)
|
||||
buf[bt] = buf[bt - dt];
|
||||
}
|
||||
}
|
||||
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
||||
if (lm)
|
||||
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
||||
} while (!final);
|
||||
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
||||
};
|
||||
var et = /* @__PURE__ */ new u8(0);
|
||||
var gzs = function(d) {
|
||||
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
||||
err(6, "invalid gzip data");
|
||||
var flg = d[3];
|
||||
var st = 10;
|
||||
if (flg & 4)
|
||||
st += (d[10] | d[11] << 8) + 2;
|
||||
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
||||
;
|
||||
return st + (flg & 2);
|
||||
};
|
||||
var gzl = function(d) {
|
||||
var l = d.length;
|
||||
return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
|
||||
};
|
||||
function gunzipSync(data, opts) {
|
||||
var st = gzs(data);
|
||||
if (st + 8 > data.length)
|
||||
err(6, "invalid gzip data");
|
||||
return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
|
||||
}
|
||||
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
||||
var tds = 0;
|
||||
try {
|
||||
td.decode(et, { stream: true });
|
||||
tds = 1;
|
||||
} catch (e) {
|
||||
}
|
||||
var dutf8 = function(d) {
|
||||
for (var r = "", i = 0; ; ) {
|
||||
var c = d[i++];
|
||||
var eb = (c > 127) + (c > 223) + (c > 239);
|
||||
if (i + eb > d.length)
|
||||
return { s: r, r: slc(d, i - 1) };
|
||||
if (!eb)
|
||||
r += String.fromCharCode(c);
|
||||
else if (eb == 3) {
|
||||
c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
||||
} else if (eb & 1)
|
||||
r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
|
||||
else
|
||||
r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
|
||||
}
|
||||
};
|
||||
function strFromU8(dat, latin1) {
|
||||
if (latin1) {
|
||||
var r = "";
|
||||
for (var i = 0; i < dat.length; i += 16384)
|
||||
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
|
||||
return r;
|
||||
} else if (td) {
|
||||
return td.decode(dat);
|
||||
} else {
|
||||
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
||||
if (r.length)
|
||||
err(8);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// src/miruro/http.js
|
||||
var BASE_URL = "https://www.miruro.to";
|
||||
var HEADERS = {
|
||||
"Accept": "*/*",
|
||||
"Referer": `${BASE_URL}/`,
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/140.0.0.0 Mobile Safari/537.36",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin"
|
||||
};
|
||||
var PIPE_OBFUSCATION_KEY = hexToBytes("71951034f8fbcf53d89db52ceb3dc22c");
|
||||
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
function hexToBytes(value) {
|
||||
const bytes = new Uint8Array(value.length / 2);
|
||||
for (let index = 0; index < bytes.length; index++) {
|
||||
bytes[index] = parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
function encodeBase64Url(value) {
|
||||
let result = "";
|
||||
for (let index = 0; index < value.length; index += 3) {
|
||||
const first = value.charCodeAt(index);
|
||||
const hasSecond = index + 1 < value.length;
|
||||
const hasThird = index + 2 < value.length;
|
||||
const second = hasSecond ? value.charCodeAt(index + 1) : 0;
|
||||
const third = hasThird ? value.charCodeAt(index + 2) : 0;
|
||||
const chunk = first << 16 | second << 8 | third;
|
||||
result += BASE64_ALPHABET[chunk >> 18 & 63];
|
||||
result += BASE64_ALPHABET[chunk >> 12 & 63];
|
||||
result += hasSecond ? BASE64_ALPHABET[chunk >> 6 & 63] : "=";
|
||||
result += hasThird ? BASE64_ALPHABET[chunk & 63] : "=";
|
||||
}
|
||||
return result.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
function decodeBase64Url(value) {
|
||||
const normalized = value.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, "");
|
||||
const outputLength = Math.floor(normalized.length * 6 / 8);
|
||||
const output = new Uint8Array(outputLength);
|
||||
let buffer = 0;
|
||||
let bits2 = 0;
|
||||
let outputIndex = 0;
|
||||
for (let index = 0; index < normalized.length; index++) {
|
||||
const code = BASE64_ALPHABET.indexOf(normalized[index]);
|
||||
if (code < 0) {
|
||||
continue;
|
||||
}
|
||||
buffer = buffer << 6 | code;
|
||||
bits2 += 6;
|
||||
if (bits2 >= 8) {
|
||||
bits2 -= 8;
|
||||
output[outputIndex++] = buffer >> bits2 & 255;
|
||||
}
|
||||
}
|
||||
return outputIndex === output.length ? output : output.slice(0, outputIndex);
|
||||
}
|
||||
function decodeSecurePayload(raw, obfuscationVersion) {
|
||||
if (!obfuscationVersion) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
const bytes = decodeBase64Url(raw);
|
||||
if (obfuscationVersion === "2") {
|
||||
for (let index = 0; index < bytes.length; index++) {
|
||||
bytes[index] ^= PIPE_OBFUSCATION_KEY[index % PIPE_OBFUSCATION_KEY.length];
|
||||
}
|
||||
}
|
||||
return JSON.parse(strFromU8(gunzipSync(bytes)));
|
||||
}
|
||||
function mergeHeaders(headers) {
|
||||
return __spreadValues(__spreadValues({}, HEADERS), headers || {});
|
||||
}
|
||||
function fetchText(_0) {
|
||||
return __async(this, arguments, function* (url, options = {}) {
|
||||
const response = yield fetch(url, __spreadProps(__spreadValues({}, options), {
|
||||
headers: mergeHeaders(options.headers)
|
||||
}));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} for ${url}`);
|
||||
}
|
||||
return yield response.text();
|
||||
});
|
||||
}
|
||||
function fetchJson(_0) {
|
||||
return __async(this, arguments, function* (url, options = {}) {
|
||||
const raw = yield fetchText(url, options);
|
||||
return JSON.parse(raw);
|
||||
});
|
||||
}
|
||||
function secureGet(_0) {
|
||||
return __async(this, arguments, function* (path, query = {}) {
|
||||
const envelope = encodeBase64Url(JSON.stringify({
|
||||
path,
|
||||
method: "GET",
|
||||
query,
|
||||
body: null
|
||||
}));
|
||||
const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`;
|
||||
let response;
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
response = yield fetch(url, { headers: HEADERS });
|
||||
if (response.ok || response.status !== 403 && response.status !== 429 && response.status < 500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!response || !response.ok) {
|
||||
throw new Error(`Miruro API returned HTTP ${response ? response.status : "unknown"}`);
|
||||
}
|
||||
const raw = yield response.text();
|
||||
const version = response.headers && response.headers.get ? response.headers.get("x-obfuscated") : null;
|
||||
return decodeSecurePayload(raw.trim(), version);
|
||||
});
|
||||
}
|
||||
|
||||
// src/miruro/extractor.js
|
||||
var ANIMAP_URL = "https://animap.id/api/map";
|
||||
var ANILIST_URL = "https://graphql.anilist.co";
|
||||
var PROVIDERS = [
|
||||
{ id: "bonk", label: "Bonk", subCategory: "ssub" },
|
||||
{ id: "kiwi", label: "Kiwi", subCategory: "sub" },
|
||||
{ id: "hop", label: "Hop", subCategory: "ssub" },
|
||||
{ id: "ally", label: "Ally", subCategory: "sub" },
|
||||
{ id: "pewe", label: "Pewe", subCategory: "sub" },
|
||||
{ id: "bee", label: "Bee", subCategory: "ssub" },
|
||||
{ id: "moo", label: "Moo", subCategory: "sub" }
|
||||
];
|
||||
function asNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
function parseIdentifier(value) {
|
||||
const id = String(value || "").trim();
|
||||
if (/^tt\d+$/i.test(id)) {
|
||||
return { service: "imdb", value: id.toLowerCase() };
|
||||
}
|
||||
if (/^\d+$/.test(id)) {
|
||||
return { service: "tmdb", value: id };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function recordMatchesIdentifier(record, identifier, mediaType) {
|
||||
const expectedType = mediaType === "movie" ? "movie" : "tv";
|
||||
if (identifier.service === "imdb") {
|
||||
const imdbMatches = (record.imdb_id || []).some((id) => String(id).toLowerCase() === identifier.value);
|
||||
const typeMatches = (record.tmdb_id || []).length === 0 || (record.tmdb_id || []).some((mapping) => mapping.type === expectedType);
|
||||
return imdbMatches && typeMatches;
|
||||
}
|
||||
return (record.tmdb_id || []).some((mapping) => String(mapping.id) === identifier.value && mapping.type === expectedType);
|
||||
}
|
||||
function getAniListIds(records, identifier, mediaType) {
|
||||
const ids = [];
|
||||
for (const record of Array.isArray(records) ? records : []) {
|
||||
if (!recordMatchesIdentifier(record, identifier, mediaType)) {
|
||||
continue;
|
||||
}
|
||||
for (const id of record.anilist_id || []) {
|
||||
const numericId = asNumber(id);
|
||||
if (numericId && !ids.includes(numericId)) {
|
||||
ids.push(numericId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
function getLocalEpisode(mappings, mediaType, season, episode) {
|
||||
if (!mappings) {
|
||||
return null;
|
||||
}
|
||||
if (mediaType === "movie") {
|
||||
return 1;
|
||||
}
|
||||
const requestedSeason = asNumber(season) || 1;
|
||||
const requestedEpisode = asNumber(episode);
|
||||
if (!requestedEpisode || requestedEpisode < 1) {
|
||||
return null;
|
||||
}
|
||||
const mappedSeason = mappings.tmdbSeason || mappings.defaultTvdbSeason;
|
||||
if (mappedSeason === "absolute") {
|
||||
return requestedSeason === 1 ? requestedEpisode : null;
|
||||
}
|
||||
if (mappedSeason != null && String(mappedSeason) !== String(requestedSeason)) {
|
||||
return null;
|
||||
}
|
||||
const offset = asNumber(mappings.tmdbOffset) || asNumber(mappings.episodeOffset) || 0;
|
||||
const episodeCount = asNumber(mappings.episodes);
|
||||
if (requestedEpisode <= offset || episodeCount && requestedEpisode > offset + episodeCount) {
|
||||
return null;
|
||||
}
|
||||
return requestedEpisode - offset;
|
||||
}
|
||||
function getAniListSequel(anilistId) {
|
||||
return __async(this, null, function* () {
|
||||
const response = yield fetchJson(ANILIST_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query ($id: Int!) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
relations {
|
||||
edges {
|
||||
relationType
|
||||
node {
|
||||
id
|
||||
type
|
||||
format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: anilistId }
|
||||
})
|
||||
});
|
||||
const edges = response && response.data && response.data.Media && response.data.Media.relations ? response.data.Media.relations.edges : [];
|
||||
const sequel = (edges || []).find((edge) => edge.relationType === "SEQUEL" && edge.node && edge.node.type === "ANIME" && ["TV", "TV_SHORT", "ONA"].includes(edge.node.format));
|
||||
return sequel ? asNumber(sequel.node.id) : null;
|
||||
});
|
||||
}
|
||||
function getSeasonAniListId(anilistId, season) {
|
||||
return __async(this, null, function* () {
|
||||
const requestedSeason = asNumber(season) || 1;
|
||||
let currentId = anilistId;
|
||||
for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) {
|
||||
const sequelId = yield getAniListSequel(currentId);
|
||||
if (!sequelId || sequelId === currentId) {
|
||||
return null;
|
||||
}
|
||||
currentId = sequelId;
|
||||
}
|
||||
return currentId;
|
||||
});
|
||||
}
|
||||
function getSequelEpisode(payload, mediaType, season, episode) {
|
||||
return getLocalEpisode(payload.mappings, mediaType, season, episode) || getLocalEpisode(payload.mappings, mediaType, 1, episode);
|
||||
}
|
||||
function resolveEpisodePayload(mediaId, mediaType, season, episode) {
|
||||
return __async(this, null, function* () {
|
||||
const identifier = parseIdentifier(mediaId);
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
let mappingRecords;
|
||||
try {
|
||||
mappingRecords = yield fetchJson(
|
||||
`${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`,
|
||||
{ headers: { "Accept": "application/json" } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
const aniListIds = getAniListIds(mappingRecords, identifier, mediaType);
|
||||
if (aniListIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (aniListIds.length === 1) {
|
||||
const anilistId = aniListIds[0];
|
||||
if (mediaType === "tv" && (asNumber(season) || 1) > 1) {
|
||||
try {
|
||||
const seasonAniListId = yield getSeasonAniListId(anilistId, season);
|
||||
if (seasonAniListId && seasonAniListId !== anilistId) {
|
||||
const payload2 = yield secureGet("episodes", { anilistId: seasonAniListId });
|
||||
const localEpisode2 = getSequelEpisode(payload2, mediaType, season, episode);
|
||||
if (localEpisode2) {
|
||||
return { anilistId: seasonAniListId, localEpisode: localEpisode2, payload: payload2 };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] Season ${season} mapping fallback failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
const payload = yield secureGet("episodes", { anilistId });
|
||||
const localEpisode = getLocalEpisode(payload.mappings, mediaType, season, episode);
|
||||
return localEpisode ? { anilistId, localEpisode, payload } : null;
|
||||
}
|
||||
for (const anilistId of aniListIds) {
|
||||
try {
|
||||
const info = yield secureGet(`info/${anilistId}`);
|
||||
const localEpisode = getLocalEpisode(info.mappings, mediaType, season, episode);
|
||||
if (localEpisode) {
|
||||
const payload = yield secureGet("episodes", { anilistId });
|
||||
return { anilistId, localEpisode, payload };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] Skipping AniList ${anilistId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
function findEpisode(providerPayload, variant, episodeNumber) {
|
||||
const episodes = providerPayload && providerPayload.episodes ? providerPayload.episodes[variant] : null;
|
||||
if (!Array.isArray(episodes)) {
|
||||
return null;
|
||||
}
|
||||
return episodes.find((item) => Number(item.number) === Number(episodeNumber)) || null;
|
||||
}
|
||||
function getOrigin(url) {
|
||||
const match = typeof url === "string" ? url.match(/^(https?:\/\/[^/]+)/i) : null;
|
||||
return match ? match[1] : BASE_URL;
|
||||
}
|
||||
function getQuality(stream) {
|
||||
const value = String(stream.quality || stream.server || "HD");
|
||||
const resolution = value.match(/(2160|1440|1080|720|480|360)p?/i);
|
||||
if (resolution) {
|
||||
return `${resolution[1]}p`;
|
||||
}
|
||||
return /4k/i.test(value) ? "4K" : "HD";
|
||||
}
|
||||
function isPlayableStream(stream) {
|
||||
const type = String(stream && stream.type || "").toLowerCase();
|
||||
return !!(stream && stream.url) && (type === "hls" || type === "m3u8" || type === "mp4" || type === "application/x-mpegurl");
|
||||
}
|
||||
function normalizeStreams(sourcePayload, provider, variant) {
|
||||
const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0;
|
||||
const language = variant === "dub" ? "DUB" : "SUB";
|
||||
return (sourcePayload.streams || []).filter(isPlayableStream).map((stream) => {
|
||||
const referer = stream.referer || BASE_URL;
|
||||
const server = stream.server || getQuality(stream);
|
||||
const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : "";
|
||||
return {
|
||||
// Nuvio's source picker prominently displays `name`, so include the
|
||||
// Miruro server and audio variant here instead of only in `title`.
|
||||
name: `Miruro \xB7 ${provider.label} \xB7 ${language}`,
|
||||
title: `[${language}] ${provider.label} \xB7 ${server}${subtitleLabel}`,
|
||||
url: stream.url,
|
||||
quality: getQuality(stream),
|
||||
headers: {
|
||||
"Referer": referer,
|
||||
"Origin": getOrigin(referer),
|
||||
"User-Agent": HEADERS["User-Agent"]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {
|
||||
return __async(this, null, function* () {
|
||||
const requests = [];
|
||||
const subEpisode = findEpisode(providerPayload, "sub", episodeNumber);
|
||||
const dubEpisode = findEpisode(providerPayload, "dub", episodeNumber);
|
||||
if (subEpisode) {
|
||||
requests.push({ episode: subEpisode, variant: "sub", category: provider.subCategory });
|
||||
}
|
||||
if (dubEpisode) {
|
||||
requests.push({ episode: dubEpisode, variant: "dub", category: "dub" });
|
||||
}
|
||||
const results = yield Promise.all(requests.map((request) => __async(this, null, function* () {
|
||||
try {
|
||||
const sources = yield secureGet("sources", {
|
||||
episodeId: request.episode.id,
|
||||
provider: provider.id,
|
||||
category: request.category,
|
||||
anilistId
|
||||
});
|
||||
return normalizeStreams(sources, provider, request.variant);
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] ${provider.label} ${request.variant} failed: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
})));
|
||||
return results.flat();
|
||||
});
|
||||
}
|
||||
function extractStreams(tmdbId, mediaType, season, episode) {
|
||||
return __async(this, null, function* () {
|
||||
if (!tmdbId || mediaType !== "movie" && mediaType !== "tv") {
|
||||
return [];
|
||||
}
|
||||
const resolved = yield resolveEpisodePayload(tmdbId, mediaType, season, episode);
|
||||
if (!resolved || !resolved.payload || !resolved.payload.providers) {
|
||||
return [];
|
||||
}
|
||||
const providerResults = yield Promise.all(PROVIDERS.map((provider) => {
|
||||
const payload = resolved.payload.providers[provider.id];
|
||||
return payload ? getProviderStreams(provider, payload, resolved.anilistId, resolved.localEpisode) : [];
|
||||
}));
|
||||
const streams = providerResults.flat();
|
||||
const seen = /* @__PURE__ */ new Set();
|
||||
return streams.filter((stream) => {
|
||||
if (seen.has(stream.url)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(stream.url);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// src/miruro/index.js
|
||||
function getStreams(tmdbId, mediaType, season, episode) {
|
||||
return __async(this, null, function* () {
|
||||
try {
|
||||
const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : "";
|
||||
console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`);
|
||||
const streams = yield extractStreams(tmdbId, mediaType, season, episode);
|
||||
console.log(`[Miruro] Found ${streams.length} stream(s)`);
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error(`[Miruro] Error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
module.exports = { getStreams };
|
||||
@@ -0,0 +1,89 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const PORT = 3000;
|
||||
|
||||
function getLocalIp() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
const mimeTypes = {
|
||||
'.json': 'application/json',
|
||||
'.js': 'application/javascript',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.ico': 'image/x-icon',
|
||||
'.html': 'text/html',
|
||||
};
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`${req.method} ${req.url}`);
|
||||
|
||||
// Handle CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare file path
|
||||
let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url);
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
if (!filePath.startsWith(__dirname)) {
|
||||
res.writeHead(403);
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov'];
|
||||
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = mimeTypes[extname] || 'application/octet-stream';
|
||||
if (videoExtensions.includes(extname)) {
|
||||
contentType = "video/mp4"; // Defaulting to mp4 for video files for simplicity
|
||||
}
|
||||
|
||||
|
||||
fs.readFile(filePath, (err, content) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// If asking for root and index.html doesn't exist, allow checking specific files
|
||||
if (req.url === '/') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Nuvio Providers Server Running. Access /manifest.json to see the manifest.');
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end(`File not found: ${req.url}`);
|
||||
} else {
|
||||
res.writeHead(500);
|
||||
res.end(`Server Error: ${err.code}`);
|
||||
}
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
const ip = getLocalIp();
|
||||
console.log(`\n🚀 Server running at: http://${ip}:${PORT}/`);
|
||||
console.log(`📝 Manifest URL: http://${ip}:${PORT}/manifest.json`);
|
||||
console.log('Press Ctrl+C to stop\n');
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Extractor Logic
|
||||
* This file handles parsing HTML/JSON to find video streams.
|
||||
*/
|
||||
|
||||
import { fetchText, HEADERS } from './http.js';
|
||||
import cheerio from 'cheerio-without-node-native';
|
||||
|
||||
export async function extractStreams(tmdbId, mediaType, season, episode) {
|
||||
// 1. Construct the search or video URL
|
||||
// const url = `https://example.com/watch/${tmdbId}`;
|
||||
|
||||
// 2. Fetch content
|
||||
// const html = await fetchText(url);
|
||||
|
||||
// 3. Parse with Cheerio
|
||||
// const $ = cheerio.load(html);
|
||||
// const videoUrl = $('video source').attr('src');
|
||||
|
||||
// 4. Return streams
|
||||
// if (videoUrl) {
|
||||
// return [{
|
||||
// name: "MyTemplateProvider",
|
||||
// title: "1080p Stream",
|
||||
// url: videoUrl,
|
||||
// quality: "1080p",
|
||||
// headers: HEADERS
|
||||
// }];
|
||||
// }
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* HTTP Utilities
|
||||
* Use this file for network requests and headers.
|
||||
*/
|
||||
|
||||
export const HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
|
||||
// Add other common headers like 'Referer' if needed
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch text content from a URL
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
*/
|
||||
export async function fetchText(url, options = {}) {
|
||||
console.log(`[Template] Fetching: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
...HEADERS,
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error ${response.status} for ${url}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON content from a URL
|
||||
* @param {string} url
|
||||
* @param {object} options
|
||||
*/
|
||||
export async function fetchJson(url, options = {}) {
|
||||
const raw = await fetchText(url, options);
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Template Provider
|
||||
* Main entry point.
|
||||
*/
|
||||
|
||||
import { extractStreams } from './extractor.js';
|
||||
|
||||
/**
|
||||
* Main function called by Nuvio
|
||||
* @param {string} tmdbId - TMDB ID of the media
|
||||
* @param {string} mediaType - 'movie' or 'tv'
|
||||
* @param {number} season - Season number (for TV)
|
||||
* @param {number} episode - Episode number (for TV)
|
||||
*/
|
||||
async function getStreams(tmdbId, mediaType, season, episode) {
|
||||
try {
|
||||
console.log(`[Template] Request: ${mediaType} ${tmdbId}`);
|
||||
|
||||
// Call your extraction logic
|
||||
const streams = await extractStreams(tmdbId, mediaType, season, episode);
|
||||
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error(`[Template] Error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getStreams };
|
||||
@@ -0,0 +1,281 @@
|
||||
/** Pure-JavaScript replacement for Flixer's img_data WebAssembly module. */
|
||||
|
||||
import { encodeUtf8, hmacSha256, sha256 } from './http.js';
|
||||
|
||||
const CANVAS_FINGERPRINT = 'iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAYAAAAZUZThAAAQAE';
|
||||
|
||||
function concatBytes(...values) {
|
||||
let length = 0;
|
||||
for (const value of values) length += value.length;
|
||||
const result = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
for (const value of values) {
|
||||
result.set(value, offset);
|
||||
offset += value.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function toHex(bytes) {
|
||||
let result = '';
|
||||
for (const value of bytes) result += value.toString(16).padStart(2, '0');
|
||||
return result;
|
||||
}
|
||||
|
||||
function decodeBase64(value) {
|
||||
const binary = atob(value);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function decodeUtf8(bytes) {
|
||||
let result = '';
|
||||
for (let index = 0; index < bytes.length;) {
|
||||
const first = bytes[index++];
|
||||
let codePoint;
|
||||
if (first < 0x80) codePoint = first;
|
||||
else if ((first & 0xE0) === 0xC0) codePoint = ((first & 0x1F) << 6) | (bytes[index++] & 0x3F);
|
||||
else if ((first & 0xF0) === 0xE0) {
|
||||
codePoint = ((first & 0x0F) << 12) | ((bytes[index++] & 0x3F) << 6) | (bytes[index++] & 0x3F);
|
||||
} else {
|
||||
codePoint = ((first & 7) << 18) | ((bytes[index++] & 0x3F) << 12) |
|
||||
((bytes[index++] & 0x3F) << 6) | (bytes[index++] & 0x3F);
|
||||
}
|
||||
if (codePoint <= 0xFFFF) result += String.fromCharCode(codePoint);
|
||||
else {
|
||||
codePoint -= 0x10000;
|
||||
result += String.fromCharCode(0xD800 | (codePoint >>> 10), 0xDC00 | (codePoint & 0x3FF));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function multiplyByte(first, second) {
|
||||
let result = 0;
|
||||
for (let index = 0; index < 8; index++) {
|
||||
if (second & 1) result ^= first;
|
||||
first = ((first << 1) ^ ((first & 0x80) ? 0x11B : 0)) & 255;
|
||||
second >>>= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function inverseByte(value) {
|
||||
if (!value) return 0;
|
||||
let result = 1;
|
||||
let base = value;
|
||||
let power = 254;
|
||||
while (power) {
|
||||
if (power & 1) result = multiplyByte(result, base);
|
||||
base = multiplyByte(base, base);
|
||||
power >>>= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function rotateByte(value, amount) {
|
||||
return ((value << amount) | (value >>> (8 - amount))) & 255;
|
||||
}
|
||||
|
||||
const AES_SBOX = (() => {
|
||||
const result = new Uint8Array(256);
|
||||
for (let index = 0; index < 256; index++) {
|
||||
const value = inverseByte(index);
|
||||
result[index] = value ^ rotateByte(value, 1) ^ rotateByte(value, 2) ^
|
||||
rotateByte(value, 3) ^ rotateByte(value, 4) ^ 0x63;
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
function expandAes256Key(key) {
|
||||
if (key.length !== 32) throw new Error('Invalid AES-256 key');
|
||||
const words = new Uint32Array(60);
|
||||
for (let index = 0; index < 8; index++) {
|
||||
const offset = index * 4;
|
||||
words[index] = ((key[offset] << 24) | (key[offset + 1] << 16) |
|
||||
(key[offset + 2] << 8) | key[offset + 3]) >>> 0;
|
||||
}
|
||||
|
||||
let roundConstant = 1;
|
||||
for (let index = 8; index < words.length; index++) {
|
||||
let temporary = words[index - 1];
|
||||
if (index % 8 === 0) {
|
||||
temporary = ((temporary << 8) | (temporary >>> 24)) >>> 0;
|
||||
temporary = ((AES_SBOX[temporary >>> 24] << 24) | (AES_SBOX[(temporary >>> 16) & 255] << 16) |
|
||||
(AES_SBOX[(temporary >>> 8) & 255] << 8) | AES_SBOX[temporary & 255]) >>> 0;
|
||||
temporary ^= roundConstant << 24;
|
||||
roundConstant = ((roundConstant << 1) ^ ((roundConstant & 0x80) ? 0x11B : 0)) & 255;
|
||||
} else if (index % 8 === 4) {
|
||||
temporary = ((AES_SBOX[temporary >>> 24] << 24) | (AES_SBOX[(temporary >>> 16) & 255] << 16) |
|
||||
(AES_SBOX[(temporary >>> 8) & 255] << 8) | AES_SBOX[temporary & 255]) >>> 0;
|
||||
}
|
||||
words[index] = (words[index - 8] ^ temporary) >>> 0;
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
function addRoundKey(state, words, round) {
|
||||
for (let column = 0; column < 4; column++) {
|
||||
const word = words[round * 4 + column];
|
||||
state[column * 4] ^= word >>> 24;
|
||||
state[column * 4 + 1] ^= (word >>> 16) & 255;
|
||||
state[column * 4 + 2] ^= (word >>> 8) & 255;
|
||||
state[column * 4 + 3] ^= word & 255;
|
||||
}
|
||||
}
|
||||
|
||||
function encryptAesBlock(input, words) {
|
||||
const state = new Uint8Array(input);
|
||||
const shifted = new Uint8Array(16);
|
||||
addRoundKey(state, words, 0);
|
||||
|
||||
for (let round = 1; round <= 14; round++) {
|
||||
for (let index = 0; index < 16; index++) state[index] = AES_SBOX[state[index]];
|
||||
for (let row = 0; row < 4; row++) {
|
||||
for (let column = 0; column < 4; column++) {
|
||||
shifted[row + column * 4] = state[row + ((column + row) & 3) * 4];
|
||||
}
|
||||
}
|
||||
state.set(shifted);
|
||||
|
||||
if (round !== 14) {
|
||||
for (let column = 0; column < 4; column++) {
|
||||
const offset = column * 4;
|
||||
const first = state[offset];
|
||||
const second = state[offset + 1];
|
||||
const third = state[offset + 2];
|
||||
const fourth = state[offset + 3];
|
||||
const combined = first ^ second ^ third ^ fourth;
|
||||
state[offset] ^= combined ^ multiplyByte(first ^ second, 2);
|
||||
state[offset + 1] ^= combined ^ multiplyByte(second ^ third, 2);
|
||||
state[offset + 2] ^= combined ^ multiplyByte(third ^ fourth, 2);
|
||||
state[offset + 3] ^= combined ^ multiplyByte(fourth ^ first, 2);
|
||||
}
|
||||
}
|
||||
addRoundKey(state, words, round);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function bytesToWords(bytes) {
|
||||
const result = new Uint32Array(4);
|
||||
for (let index = 0; index < 4; index++) {
|
||||
const offset = index * 4;
|
||||
result[index] = ((bytes[offset] << 24) | (bytes[offset + 1] << 16) |
|
||||
(bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function wordsToBytes(words) {
|
||||
const result = new Uint8Array(16);
|
||||
for (let index = 0; index < 4; index++) {
|
||||
result[index * 4] = words[index] >>> 24;
|
||||
result[index * 4 + 1] = (words[index] >>> 16) & 255;
|
||||
result[index * 4 + 2] = (words[index] >>> 8) & 255;
|
||||
result[index * 4 + 3] = words[index] & 255;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function multiplyGhash(first, second) {
|
||||
const result = new Uint32Array(4);
|
||||
const value = new Uint32Array(second);
|
||||
for (let bit = 0; bit < 128; bit++) {
|
||||
if ((first[bit >>> 5] >>> (31 - (bit & 31))) & 1) {
|
||||
for (let index = 0; index < 4; index++) result[index] ^= value[index];
|
||||
}
|
||||
const lowBit = value[3] & 1;
|
||||
value[3] = ((value[3] >>> 1) | ((value[2] & 1) << 31)) >>> 0;
|
||||
value[2] = ((value[2] >>> 1) | ((value[1] & 1) << 31)) >>> 0;
|
||||
value[1] = ((value[1] >>> 1) | ((value[0] & 1) << 31)) >>> 0;
|
||||
value[0] >>>= 1;
|
||||
if (lowBit) value[0] = (value[0] ^ 0xE1000000) >>> 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function calculateGhash(ciphertext, hashKey) {
|
||||
let state = new Uint32Array(4);
|
||||
for (let offset = 0; offset < ciphertext.length; offset += 16) {
|
||||
const block = new Uint8Array(16);
|
||||
block.set(ciphertext.subarray(offset, Math.min(offset + 16, ciphertext.length)));
|
||||
const words = bytesToWords(block);
|
||||
for (let index = 0; index < 4; index++) state[index] ^= words[index];
|
||||
state = multiplyGhash(state, hashKey);
|
||||
}
|
||||
|
||||
const lengthBlock = new Uint8Array(16);
|
||||
const bitLength = ciphertext.length * 8;
|
||||
const view = new DataView(lengthBlock.buffer);
|
||||
view.setUint32(12, bitLength >>> 0);
|
||||
view.setUint32(8, Math.floor(bitLength / 0x100000000));
|
||||
const lengths = bytesToWords(lengthBlock);
|
||||
for (let index = 0; index < 4; index++) state[index] ^= lengths[index];
|
||||
return multiplyGhash(state, hashKey);
|
||||
}
|
||||
|
||||
function incrementCounter(counter) {
|
||||
for (let index = 15; index >= 12; index--) {
|
||||
counter[index] = (counter[index] + 1) & 255;
|
||||
if (counter[index]) break;
|
||||
}
|
||||
}
|
||||
|
||||
function decryptAesGcm(payload, key) {
|
||||
if (payload.length < 28) throw new Error('Invalid encrypted payload');
|
||||
const nonce = payload.subarray(0, 12);
|
||||
const ciphertext = payload.subarray(12, payload.length - 16);
|
||||
const expectedTag = payload.subarray(payload.length - 16);
|
||||
const words = expandAes256Key(key);
|
||||
const zero = new Uint8Array(16);
|
||||
const hashKey = bytesToWords(encryptAesBlock(zero, words));
|
||||
const initialCounter = new Uint8Array(16);
|
||||
initialCounter.set(nonce);
|
||||
initialCounter[15] = 1;
|
||||
|
||||
const authentication = wordsToBytes(calculateGhash(ciphertext, hashKey));
|
||||
const encryptedCounter = encryptAesBlock(initialCounter, words);
|
||||
let mismatch = 0;
|
||||
for (let index = 0; index < 16; index++) mismatch |= (authentication[index] ^ encryptedCounter[index]) ^ expectedTag[index];
|
||||
if (mismatch) throw new Error('Invalid encrypted payload tag');
|
||||
|
||||
const output = new Uint8Array(ciphertext.length);
|
||||
const counter = new Uint8Array(initialCounter);
|
||||
for (let offset = 0; offset < ciphertext.length; offset += 16) {
|
||||
incrementCounter(counter);
|
||||
const stream = encryptAesBlock(counter, words);
|
||||
const length = Math.min(16, ciphertext.length - offset);
|
||||
for (let index = 0; index < length; index++) output[offset + index] = ciphertext[offset + index] ^ stream[index];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function derivePayloadKey(apiKey, now) {
|
||||
const hour = Math.floor(now / 3600000);
|
||||
const dynamic = new Uint8Array(16);
|
||||
for (let index = 0; index < dynamic.length; index++) {
|
||||
dynamic[index] = (index + 1) ^ (Math.floor(hour / (2 ** (index & 7))) & 255);
|
||||
}
|
||||
|
||||
let derived = concatBytes(encodeUtf8(apiKey), dynamic);
|
||||
for (let iteration = 0; iteration < 600; iteration++) derived = sha256(derived);
|
||||
const timeBucket = Math.floor(now / 300000);
|
||||
return hmacSha256(derived, `${timeBucket}${apiKey}`);
|
||||
}
|
||||
|
||||
export function initDecoder() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function get_img_key() {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const fingerprint = `1365x768:24:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36:Linux x86_64:en-US:-180:${timestamp}:${CANVAS_FINGERPRINT}`;
|
||||
return toHex(sha256(encodeUtf8(toHex(sha256(encodeUtf8(fingerprint))))));
|
||||
}
|
||||
|
||||
export function process_img_data(value, apiKey) {
|
||||
const key = derivePayloadKey(apiKey, Date.now());
|
||||
return decodeUtf8(decryptAesGcm(decodeBase64(value), key));
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/** Resolve a TMDB title through Flixer's signed source API. */
|
||||
|
||||
import { get_img_key, initDecoder, process_img_data } from './decoder.js';
|
||||
import { API_URL, createSecureHeaders, fetchText, HEADERS, SITE_URL } from './http.js';
|
||||
|
||||
const NATO_ORDER = [
|
||||
'alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india',
|
||||
'juliet', 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo',
|
||||
'sierra', 'tango', 'uniform', 'victor', 'whiskey', 'xray', 'yankee', 'zulu'
|
||||
];
|
||||
|
||||
const SERVER_LABELS = {
|
||||
bravo: 'Balder', delta: 'Dionysus', echo: 'Eros', golf: 'Gaia', hotel: 'Hades',
|
||||
india: 'Isis', juliet: 'Juno', kilo: 'Kronos', lima: 'Loki'
|
||||
};
|
||||
|
||||
const TMDB_ID_CACHE = {};
|
||||
|
||||
async function resolveTmdbId(id, mediaType) {
|
||||
if (/^\d+$/.test(id)) return id;
|
||||
if (!/^tt\d+$/i.test(id)) return null;
|
||||
|
||||
const cacheKey = `${mediaType}:${id.toLowerCase()}`;
|
||||
if (TMDB_ID_CACHE[cacheKey]) return TMDB_ID_CACHE[cacheKey];
|
||||
|
||||
const catalogType = mediaType === 'movie' ? 'movie' : 'series';
|
||||
try {
|
||||
const raw = await fetchText(`https://v3-cinemeta.strem.io/meta/${catalogType}/${encodeURIComponent(id)}.json`, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const payload = JSON.parse(raw);
|
||||
const resolved = payload && payload.meta && payload.meta.moviedb_id;
|
||||
if (!resolved || !/^\d+$/.test(String(resolved))) return null;
|
||||
|
||||
TMDB_ID_CACHE[cacheKey] = String(resolved);
|
||||
console.log(`[Flixer] Resolved ${id} to TMDB ${resolved}`);
|
||||
return TMDB_ID_CACHE[cacheKey];
|
||||
} catch (error) {
|
||||
console.warn(`[Flixer] Could not resolve IMDb ID ${id}: ${error && error.message ? error.message : String(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildApiPath(tmdbId, mediaType, season, episode) {
|
||||
if (mediaType === 'movie') return `/api/tmdb/movie/${encodeURIComponent(tmdbId)}/images`;
|
||||
return `/api/tmdb/tv/${encodeURIComponent(tmdbId)}/season/${season}/episode/${episode}/images`;
|
||||
}
|
||||
|
||||
async function decryptPayload(encrypted, apiKey) {
|
||||
const clearText = await process_img_data(encrypted, apiKey);
|
||||
return JSON.parse(clearText);
|
||||
}
|
||||
|
||||
function getServers(payload) {
|
||||
const values = [];
|
||||
if (payload && Array.isArray(payload.sources)) {
|
||||
for (const source of payload.sources) {
|
||||
const server = source && String(source.server || '').toLowerCase();
|
||||
if (server && !values.includes(server)) values.push(server);
|
||||
}
|
||||
}
|
||||
if (payload && payload.servers && typeof payload.servers === 'object') {
|
||||
for (const server of Object.keys(payload.servers)) {
|
||||
const normalized = server.toLowerCase();
|
||||
if (!values.includes(normalized)) values.push(normalized);
|
||||
}
|
||||
}
|
||||
return NATO_ORDER.filter(server => values.includes(server)).concat(values.filter(server => !NATO_ORDER.includes(server)));
|
||||
}
|
||||
|
||||
function findUrl(payload, server) {
|
||||
if (payload && Array.isArray(payload.sources)) {
|
||||
const exact = payload.sources.find(source => source && String(source.server || '').toLowerCase() === server);
|
||||
const candidate = exact || payload.sources.find(source => source && source.url);
|
||||
return candidate && typeof candidate.url === 'string' ? candidate.url.trim() : '';
|
||||
}
|
||||
if (payload && payload.sources && typeof payload.sources === 'object') {
|
||||
const value = payload.sources.file || payload.sources.url;
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getQuality(url) {
|
||||
const match = String(url).match(/(?:^|[^0-9])(2160|1440|1080|720|480|360)p?(?:[^0-9]|$)/i);
|
||||
return match ? `${match[1]}p` : 'Auto';
|
||||
}
|
||||
|
||||
async function fetchServerPayload(apiUrl, apiPath, apiKey, server) {
|
||||
const secureHeaders = await createSecureHeaders(apiKey, apiPath);
|
||||
return await fetchText(apiUrl, {
|
||||
headers: {
|
||||
...secureHeaders,
|
||||
'X-Only-Sources': '1',
|
||||
'X-Server': server
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function extractStreams(tmdbId, mediaType, season, episode) {
|
||||
const id = String(tmdbId || '').trim();
|
||||
if (mediaType !== 'movie' && mediaType !== 'tv') return [];
|
||||
if (mediaType === 'tv' && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1)) return [];
|
||||
|
||||
const resolvedId = await resolveTmdbId(id, mediaType);
|
||||
if (!resolvedId) return [];
|
||||
|
||||
initDecoder();
|
||||
const apiKey = get_img_key();
|
||||
const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode));
|
||||
const apiUrl = `${API_URL}${apiPath}`;
|
||||
const secureHeaders = await createSecureHeaders(apiKey, apiPath);
|
||||
const serverPayload = await fetchText(apiUrl, {
|
||||
headers: { ...secureHeaders, bW90aGFmYWth: '1' }
|
||||
});
|
||||
const servers = getServers(await decryptPayload(serverPayload, apiKey));
|
||||
// Stagger requests as the website does, but allow slow/dead mirrors to run
|
||||
// concurrently so they do not add their timeout to every other mirror.
|
||||
const encryptedResponses = await Promise.all(servers.map(async server => {
|
||||
try {
|
||||
return { server, encrypted: await fetchServerPayload(apiUrl, apiPath, apiKey, server) };
|
||||
} catch (error) {
|
||||
console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`);
|
||||
return { server, encrypted: '' };
|
||||
}
|
||||
}));
|
||||
|
||||
const streams = [];
|
||||
const seenUrls = {};
|
||||
for (const response of encryptedResponses) {
|
||||
if (!response.encrypted) continue;
|
||||
|
||||
try {
|
||||
const url = findUrl(await decryptPayload(response.encrypted, apiKey), response.server);
|
||||
if (!url || seenUrls[url]) continue;
|
||||
seenUrls[url] = true;
|
||||
const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1);
|
||||
const quality = getQuality(url);
|
||||
streams.push({
|
||||
name: `Flixer · ${label}`,
|
||||
title: `${label} · ${quality}`,
|
||||
url,
|
||||
quality,
|
||||
headers: {
|
||||
'Referer': `${SITE_URL}/`,
|
||||
'Origin': SITE_URL,
|
||||
'User-Agent': HEADERS['User-Agent']
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error && error.message ? error.message : String(error);
|
||||
if (!/^E5[678]$/.test(message)) {
|
||||
console.warn(`[Flixer] ${response.server} decode failed: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/** HTTP and request-signing helpers for Flixer's source API. */
|
||||
|
||||
export const SITE_URL = 'https://flixer.gd';
|
||||
export const API_URL = 'https://plsdontscrapemelove.flixer.gd';
|
||||
|
||||
export const HEADERS = {
|
||||
'Accept': 'text/plain',
|
||||
'Origin': SITE_URL,
|
||||
'Referer': `${SITE_URL}/`,
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'
|
||||
};
|
||||
|
||||
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
const SHA256_CONSTANTS = [
|
||||
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
||||
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
||||
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
||||
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
||||
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
||||
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
||||
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
||||
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
|
||||
];
|
||||
|
||||
export function encodeUtf8(value) {
|
||||
const bytes = [];
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
let codePoint = value.charCodeAt(index);
|
||||
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && index + 1 < value.length) {
|
||||
const trailing = value.charCodeAt(index + 1);
|
||||
if (trailing >= 0xDC00 && trailing <= 0xDFFF) {
|
||||
codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + trailing - 0xDC00;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (codePoint < 0x80) bytes.push(codePoint);
|
||||
else if (codePoint < 0x800) bytes.push(0xC0 | (codePoint >> 6), 0x80 | (codePoint & 63));
|
||||
else if (codePoint < 0x10000) bytes.push(0xE0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
||||
else bytes.push(0xF0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 63), 0x80 | ((codePoint >> 6) & 63), 0x80 | (codePoint & 63));
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function rotateRight(value, amount) {
|
||||
return (value >>> amount) | (value << (32 - amount));
|
||||
}
|
||||
|
||||
export function sha256(input) {
|
||||
const bitLength = input.length * 8;
|
||||
const paddedLength = Math.ceil((input.length + 9) / 64) * 64;
|
||||
const bytes = new Uint8Array(paddedLength);
|
||||
bytes.set(input);
|
||||
bytes[input.length] = 0x80;
|
||||
|
||||
const high = Math.floor(bitLength / 0x100000000);
|
||||
const low = bitLength >>> 0;
|
||||
const end = paddedLength - 8;
|
||||
bytes[end] = (high >>> 24) & 255;
|
||||
bytes[end + 1] = (high >>> 16) & 255;
|
||||
bytes[end + 2] = (high >>> 8) & 255;
|
||||
bytes[end + 3] = high & 255;
|
||||
bytes[end + 4] = (low >>> 24) & 255;
|
||||
bytes[end + 5] = (low >>> 16) & 255;
|
||||
bytes[end + 6] = (low >>> 8) & 255;
|
||||
bytes[end + 7] = low & 255;
|
||||
|
||||
const hash = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
|
||||
const words = new Uint32Array(64);
|
||||
|
||||
for (let offset = 0; offset < bytes.length; offset += 64) {
|
||||
for (let index = 0; index < 16; index++) {
|
||||
const position = offset + index * 4;
|
||||
words[index] = ((bytes[position] << 24) | (bytes[position + 1] << 16) |
|
||||
(bytes[position + 2] << 8) | bytes[position + 3]) >>> 0;
|
||||
}
|
||||
for (let index = 16; index < 64; index++) {
|
||||
const first = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ (words[index - 15] >>> 3);
|
||||
const second = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ (words[index - 2] >>> 10);
|
||||
words[index] = (words[index - 16] + first + words[index - 7] + second) >>> 0;
|
||||
}
|
||||
|
||||
let [a, b, c, d, e, f, g, h] = hash;
|
||||
for (let index = 0; index < 64; index++) {
|
||||
const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
||||
const choice = (e & f) ^ (~e & g);
|
||||
const temporary1 = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
|
||||
const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
||||
const majority = (a & b) ^ (a & c) ^ (b & c);
|
||||
const temporary2 = (sum0 + majority) >>> 0;
|
||||
h = g; g = f; f = e; e = (d + temporary1) >>> 0;
|
||||
d = c; c = b; b = a; a = (temporary1 + temporary2) >>> 0;
|
||||
}
|
||||
|
||||
hash[0] = (hash[0] + a) >>> 0;
|
||||
hash[1] = (hash[1] + b) >>> 0;
|
||||
hash[2] = (hash[2] + c) >>> 0;
|
||||
hash[3] = (hash[3] + d) >>> 0;
|
||||
hash[4] = (hash[4] + e) >>> 0;
|
||||
hash[5] = (hash[5] + f) >>> 0;
|
||||
hash[6] = (hash[6] + g) >>> 0;
|
||||
hash[7] = (hash[7] + h) >>> 0;
|
||||
}
|
||||
|
||||
const output = new Uint8Array(32);
|
||||
for (let index = 0; index < hash.length; index++) {
|
||||
output[index * 4] = (hash[index] >>> 24) & 255;
|
||||
output[index * 4 + 1] = (hash[index] >>> 16) & 255;
|
||||
output[index * 4 + 2] = (hash[index] >>> 8) & 255;
|
||||
output[index * 4 + 3] = hash[index] & 255;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function hmacSha256(key, message) {
|
||||
let keyBytes = key instanceof Uint8Array ? key : encodeUtf8(String(key));
|
||||
if (keyBytes.length > 64) keyBytes = sha256(keyBytes);
|
||||
|
||||
const messageBytes = message instanceof Uint8Array ? message : encodeUtf8(String(message));
|
||||
const inner = new Uint8Array(64 + messageBytes.length);
|
||||
const outer = new Uint8Array(96);
|
||||
for (let index = 0; index < 64; index++) {
|
||||
const value = index < keyBytes.length ? keyBytes[index] : 0;
|
||||
inner[index] = value ^ 0x36;
|
||||
outer[index] = value ^ 0x5C;
|
||||
}
|
||||
inner.set(messageBytes, 64);
|
||||
outer.set(sha256(inner), 64);
|
||||
return sha256(outer);
|
||||
}
|
||||
|
||||
function encodeBase64(bytes) {
|
||||
let output = '';
|
||||
for (let index = 0; index < bytes.length; index += 3) {
|
||||
const secondExists = index + 1 < bytes.length;
|
||||
const thirdExists = index + 2 < bytes.length;
|
||||
const value = (bytes[index] << 16) | ((secondExists ? bytes[index + 1] : 0) << 8) |
|
||||
(thirdExists ? bytes[index + 2] : 0);
|
||||
output += BASE64_ALPHABET[(value >>> 18) & 63];
|
||||
output += BASE64_ALPHABET[(value >>> 12) & 63];
|
||||
output += secondExists ? BASE64_ALPHABET[(value >>> 6) & 63] : '=';
|
||||
output += thirdExists ? BASE64_ALPHABET[value & 63] : '=';
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function createNonce() {
|
||||
const bytes = new Uint8Array(16);
|
||||
for (let index = 0; index < bytes.length; index++) bytes[index] = Math.floor(Math.random() * 256);
|
||||
return encodeBase64(bytes).replace(/[/+=]/g, '').slice(0, 22);
|
||||
}
|
||||
|
||||
let serverTimeOffset = 0;
|
||||
let serverTimeFetchedAt = 0;
|
||||
|
||||
async function getServerTimestamp() {
|
||||
const now = Date.now();
|
||||
if (serverTimeFetchedAt && now - serverTimeFetchedAt < 300000) {
|
||||
return Math.floor((now + serverTimeOffset) / 1000);
|
||||
}
|
||||
|
||||
try {
|
||||
const startedAt = Date.now();
|
||||
const response = await fetch(`${API_URL}/api/time?t=${startedAt}`, {
|
||||
headers: { ...HEADERS, 'Cache-Control': 'no-cache' }
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
if (!payload || typeof payload.timestamp !== 'number') throw new Error('invalid response');
|
||||
const finishedAt = Date.now();
|
||||
serverTimeOffset = payload.timestamp * 1000 + (finishedAt - startedAt) / 2 - finishedAt;
|
||||
serverTimeFetchedAt = finishedAt;
|
||||
return Math.floor((finishedAt + serverTimeOffset) / 1000);
|
||||
} catch (_) {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSecureHeaders(apiKey, path) {
|
||||
const timestamp = await getServerTimestamp();
|
||||
const nonce = createNonce();
|
||||
const message = `${apiKey}:${timestamp}:${nonce}:${path}`;
|
||||
|
||||
return {
|
||||
'X-Api-Key': apiKey,
|
||||
'X-Request-Timestamp': String(timestamp),
|
||||
'X-Request-Nonce': nonce,
|
||||
'X-Request-Signature': encodeBase64(hmacSha256(apiKey, message)),
|
||||
'X-Client-Fingerprint': 'zerlgt',
|
||||
'X-Fingerprint-Lite': 'e9136c41504646444'
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchText(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: { ...HEADERS, ...(options.headers || {}) }
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
|
||||
return await response.text();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Flixer provider entry point. */
|
||||
|
||||
import { extractStreams } from './extractor.js';
|
||||
|
||||
async function getStreams(tmdbId, mediaType, season, episode) {
|
||||
try {
|
||||
console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`);
|
||||
const streams = await extractStreams(tmdbId, mediaType, season, episode);
|
||||
console.log(`[Flixer] Found ${streams.length} stream(s)`);
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error(`[Flixer] Error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getStreams };
|
||||
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Resolve TMDB media to AniList, then retrieve Miruro episode sources.
|
||||
*/
|
||||
|
||||
import { BASE_URL, fetchJson, HEADERS, secureGet } from './http.js';
|
||||
|
||||
const ANIMAP_URL = 'https://animap.id/api/map';
|
||||
const ANILIST_URL = 'https://graphql.anilist.co';
|
||||
|
||||
// Miruro's visible native-player providers and their preferred subtitle mode.
|
||||
const PROVIDERS = [
|
||||
{ id: 'bonk', label: 'Bonk', subCategory: 'ssub' },
|
||||
{ id: 'kiwi', label: 'Kiwi', subCategory: 'sub' },
|
||||
{ id: 'hop', label: 'Hop', subCategory: 'ssub' },
|
||||
{ id: 'ally', label: 'Ally', subCategory: 'sub' },
|
||||
{ id: 'pewe', label: 'Pewe', subCategory: 'sub' },
|
||||
{ id: 'bee', label: 'Bee', subCategory: 'ssub' },
|
||||
{ id: 'moo', label: 'Moo', subCategory: 'sub' }
|
||||
];
|
||||
|
||||
function asNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function parseIdentifier(value) {
|
||||
const id = String(value || '').trim();
|
||||
|
||||
if (/^tt\d+$/i.test(id)) {
|
||||
return { service: 'imdb', value: id.toLowerCase() };
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(id)) {
|
||||
return { service: 'tmdb', value: id };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function recordMatchesIdentifier(record, identifier, mediaType) {
|
||||
const expectedType = mediaType === 'movie' ? 'movie' : 'tv';
|
||||
|
||||
if (identifier.service === 'imdb') {
|
||||
const imdbMatches = (record.imdb_id || []).some((id) => (
|
||||
String(id).toLowerCase() === identifier.value
|
||||
));
|
||||
const typeMatches = (record.tmdb_id || []).length === 0 || (record.tmdb_id || []).some((mapping) => (
|
||||
mapping.type === expectedType
|
||||
));
|
||||
|
||||
return imdbMatches && typeMatches;
|
||||
}
|
||||
|
||||
return (record.tmdb_id || []).some((mapping) => (
|
||||
String(mapping.id) === identifier.value && mapping.type === expectedType
|
||||
));
|
||||
}
|
||||
|
||||
function getAniListIds(records, identifier, mediaType) {
|
||||
const ids = [];
|
||||
|
||||
for (const record of Array.isArray(records) ? records : []) {
|
||||
if (!recordMatchesIdentifier(record, identifier, mediaType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const id of record.anilist_id || []) {
|
||||
const numericId = asNumber(id);
|
||||
|
||||
if (numericId && !ids.includes(numericId)) {
|
||||
ids.push(numericId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
function getLocalEpisode(mappings, mediaType, season, episode) {
|
||||
if (!mappings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mediaType === 'movie') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const requestedSeason = asNumber(season) || 1;
|
||||
const requestedEpisode = asNumber(episode);
|
||||
|
||||
if (!requestedEpisode || requestedEpisode < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mappedSeason = mappings.tmdbSeason || mappings.defaultTvdbSeason;
|
||||
|
||||
if (mappedSeason === 'absolute') {
|
||||
// Absolute mappings cannot reliably translate later TMDB seasons without
|
||||
// season lengths. Season one still maps directly.
|
||||
return requestedSeason === 1 ? requestedEpisode : null;
|
||||
}
|
||||
|
||||
if (mappedSeason != null && String(mappedSeason) !== String(requestedSeason)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const offset = asNumber(mappings.tmdbOffset) || asNumber(mappings.episodeOffset) || 0;
|
||||
const episodeCount = asNumber(mappings.episodes);
|
||||
|
||||
if (requestedEpisode <= offset || (episodeCount && requestedEpisode > offset + episodeCount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return requestedEpisode - offset;
|
||||
}
|
||||
|
||||
async function getAniListSequel(anilistId) {
|
||||
const response = await fetchJson(ANILIST_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query ($id: Int!) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
relations {
|
||||
edges {
|
||||
relationType
|
||||
node {
|
||||
id
|
||||
type
|
||||
format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: anilistId }
|
||||
})
|
||||
});
|
||||
const edges = response && response.data && response.data.Media && response.data.Media.relations
|
||||
? response.data.Media.relations.edges
|
||||
: [];
|
||||
const sequel = (edges || []).find((edge) => (
|
||||
edge.relationType === 'SEQUEL' &&
|
||||
edge.node &&
|
||||
edge.node.type === 'ANIME' &&
|
||||
['TV', 'TV_SHORT', 'ONA'].includes(edge.node.format)
|
||||
));
|
||||
|
||||
return sequel ? asNumber(sequel.node.id) : null;
|
||||
}
|
||||
|
||||
async function getSeasonAniListId(anilistId, season) {
|
||||
const requestedSeason = asNumber(season) || 1;
|
||||
let currentId = anilistId;
|
||||
|
||||
for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) {
|
||||
const sequelId = await getAniListSequel(currentId);
|
||||
|
||||
if (!sequelId || sequelId === currentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
currentId = sequelId;
|
||||
}
|
||||
|
||||
return currentId;
|
||||
}
|
||||
|
||||
function getSequelEpisode(payload, mediaType, season, episode) {
|
||||
// A sequel has its own AniList episode list, so its first episode is local
|
||||
// episode 1 even when the caller identifies it as TMDB/IMDb season 2+. Keep
|
||||
// an explicit TMDB season mapping when Miruro provides one, then fall back
|
||||
// to numbering relative to the selected AniList entry.
|
||||
return getLocalEpisode(payload.mappings, mediaType, season, episode) ||
|
||||
getLocalEpisode(payload.mappings, mediaType, 1, episode);
|
||||
}
|
||||
|
||||
async function resolveEpisodePayload(mediaId, mediaType, season, episode) {
|
||||
const identifier = parseIdentifier(mediaId);
|
||||
|
||||
if (!identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mappingRecords;
|
||||
|
||||
try {
|
||||
mappingRecords = await fetchJson(
|
||||
`${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`,
|
||||
{ headers: { 'Accept': 'application/json' } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const aniListIds = getAniListIds(mappingRecords, identifier, mediaType);
|
||||
|
||||
if (aniListIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A single mapping can be validated using the episode response itself.
|
||||
if (aniListIds.length === 1) {
|
||||
const anilistId = aniListIds[0];
|
||||
|
||||
if (mediaType === 'tv' && (asNumber(season) || 1) > 1) {
|
||||
try {
|
||||
const seasonAniListId = await getSeasonAniListId(anilistId, season);
|
||||
|
||||
if (seasonAniListId && seasonAniListId !== anilistId) {
|
||||
const payload = await secureGet('episodes', { anilistId: seasonAniListId });
|
||||
const localEpisode = getSequelEpisode(payload, mediaType, season, episode);
|
||||
|
||||
if (localEpisode) {
|
||||
return { anilistId: seasonAniListId, localEpisode, payload };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// AniList is a fallback for mappings that only identify season
|
||||
// one. If it is unavailable, retain Miruro's normal mapping.
|
||||
console.warn(`[Miruro] Season ${season} mapping fallback failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await secureGet('episodes', { anilistId });
|
||||
const localEpisode = getLocalEpisode(payload.mappings, mediaType, season, episode);
|
||||
|
||||
return localEpisode ? { anilistId, localEpisode, payload } : null;
|
||||
}
|
||||
|
||||
// Franchises can map multiple AniList cours to one TMDB show. Miruro's info
|
||||
// metadata identifies both the TMDB season and the cour's episode offset.
|
||||
for (const anilistId of aniListIds) {
|
||||
try {
|
||||
const info = await secureGet(`info/${anilistId}`);
|
||||
const localEpisode = getLocalEpisode(info.mappings, mediaType, season, episode);
|
||||
|
||||
if (localEpisode) {
|
||||
const payload = await secureGet('episodes', { anilistId });
|
||||
return { anilistId, localEpisode, payload };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] Skipping AniList ${anilistId}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findEpisode(providerPayload, variant, episodeNumber) {
|
||||
const episodes = providerPayload && providerPayload.episodes
|
||||
? providerPayload.episodes[variant]
|
||||
: null;
|
||||
|
||||
if (!Array.isArray(episodes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return episodes.find((item) => Number(item.number) === Number(episodeNumber)) || null;
|
||||
}
|
||||
|
||||
function getOrigin(url) {
|
||||
const match = typeof url === 'string' ? url.match(/^(https?:\/\/[^/]+)/i) : null;
|
||||
return match ? match[1] : BASE_URL;
|
||||
}
|
||||
|
||||
function getQuality(stream) {
|
||||
const value = String(stream.quality || stream.server || 'HD');
|
||||
const resolution = value.match(/(2160|1440|1080|720|480|360)p?/i);
|
||||
|
||||
if (resolution) {
|
||||
return `${resolution[1]}p`;
|
||||
}
|
||||
|
||||
return /4k/i.test(value) ? '4K' : 'HD';
|
||||
}
|
||||
|
||||
function isPlayableStream(stream) {
|
||||
const type = String(stream && stream.type || '').toLowerCase();
|
||||
return !!(stream && stream.url) && (
|
||||
type === 'hls' ||
|
||||
type === 'm3u8' ||
|
||||
type === 'mp4' ||
|
||||
type === 'application/x-mpegurl'
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeStreams(sourcePayload, provider, variant) {
|
||||
const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0;
|
||||
const language = variant === 'dub' ? 'DUB' : 'SUB';
|
||||
|
||||
return (sourcePayload.streams || []).filter(isPlayableStream).map((stream) => {
|
||||
const referer = stream.referer || BASE_URL;
|
||||
const server = stream.server || getQuality(stream);
|
||||
const subtitleLabel = subtitleCount > 0 && variant !== 'dub' ? ' · CC' : '';
|
||||
|
||||
return {
|
||||
// Nuvio's source picker prominently displays `name`, so include the
|
||||
// Miruro server and audio variant here instead of only in `title`.
|
||||
name: `Miruro · ${provider.label} · ${language}`,
|
||||
title: `[${language}] ${provider.label} · ${server}${subtitleLabel}`,
|
||||
url: stream.url,
|
||||
quality: getQuality(stream),
|
||||
headers: {
|
||||
'Referer': referer,
|
||||
'Origin': getOrigin(referer),
|
||||
'User-Agent': HEADERS['User-Agent']
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {
|
||||
const requests = [];
|
||||
const subEpisode = findEpisode(providerPayload, 'sub', episodeNumber);
|
||||
const dubEpisode = findEpisode(providerPayload, 'dub', episodeNumber);
|
||||
|
||||
if (subEpisode) {
|
||||
requests.push({ episode: subEpisode, variant: 'sub', category: provider.subCategory });
|
||||
}
|
||||
|
||||
if (dubEpisode) {
|
||||
requests.push({ episode: dubEpisode, variant: 'dub', category: 'dub' });
|
||||
}
|
||||
|
||||
const results = await Promise.all(requests.map(async (request) => {
|
||||
try {
|
||||
const sources = await secureGet('sources', {
|
||||
episodeId: request.episode.id,
|
||||
provider: provider.id,
|
||||
category: request.category,
|
||||
anilistId
|
||||
});
|
||||
|
||||
return normalizeStreams(sources, provider, request.variant);
|
||||
} catch (error) {
|
||||
console.warn(`[Miruro] ${provider.label} ${request.variant} failed: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
|
||||
return results.flat();
|
||||
}
|
||||
|
||||
export async function extractStreams(tmdbId, mediaType, season, episode) {
|
||||
if (!tmdbId || (mediaType !== 'movie' && mediaType !== 'tv')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resolved = await resolveEpisodePayload(tmdbId, mediaType, season, episode);
|
||||
|
||||
if (!resolved || !resolved.payload || !resolved.payload.providers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const providerResults = await Promise.all(PROVIDERS.map((provider) => {
|
||||
const payload = resolved.payload.providers[provider.id];
|
||||
return payload
|
||||
? getProviderStreams(provider, payload, resolved.anilistId, resolved.localEpisode)
|
||||
: [];
|
||||
}));
|
||||
const streams = providerResults.flat();
|
||||
const seen = new Set();
|
||||
|
||||
return streams.filter((stream) => {
|
||||
if (seen.has(stream.url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(stream.url);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* HTTP helpers for Miruro's public read API.
|
||||
*/
|
||||
|
||||
import { gunzipSync, strFromU8 } from 'fflate';
|
||||
|
||||
export const BASE_URL = 'https://www.miruro.to';
|
||||
|
||||
export const HEADERS = {
|
||||
'Accept': '*/*',
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/140.0.0.0 Mobile Safari/537.36',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin'
|
||||
};
|
||||
|
||||
const PIPE_OBFUSCATION_KEY = hexToBytes('71951034f8fbcf53d89db52ceb3dc22c');
|
||||
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
function hexToBytes(value) {
|
||||
const bytes = new Uint8Array(value.length / 2);
|
||||
|
||||
for (let index = 0; index < bytes.length; index++) {
|
||||
bytes[index] = parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function encodeBase64Url(value) {
|
||||
let result = '';
|
||||
|
||||
for (let index = 0; index < value.length; index += 3) {
|
||||
const first = value.charCodeAt(index);
|
||||
const hasSecond = index + 1 < value.length;
|
||||
const hasThird = index + 2 < value.length;
|
||||
const second = hasSecond ? value.charCodeAt(index + 1) : 0;
|
||||
const third = hasThird ? value.charCodeAt(index + 2) : 0;
|
||||
const chunk = (first << 16) | (second << 8) | third;
|
||||
|
||||
result += BASE64_ALPHABET[(chunk >> 18) & 63];
|
||||
result += BASE64_ALPHABET[(chunk >> 12) & 63];
|
||||
result += hasSecond ? BASE64_ALPHABET[(chunk >> 6) & 63] : '=';
|
||||
result += hasThird ? BASE64_ALPHABET[chunk & 63] : '=';
|
||||
}
|
||||
|
||||
return result.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function decodeBase64Url(value) {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
|
||||
const outputLength = Math.floor(normalized.length * 6 / 8);
|
||||
const output = new Uint8Array(outputLength);
|
||||
let buffer = 0;
|
||||
let bits = 0;
|
||||
let outputIndex = 0;
|
||||
|
||||
for (let index = 0; index < normalized.length; index++) {
|
||||
const code = BASE64_ALPHABET.indexOf(normalized[index]);
|
||||
|
||||
if (code < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer = (buffer << 6) | code;
|
||||
bits += 6;
|
||||
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
output[outputIndex++] = (buffer >> bits) & 255;
|
||||
}
|
||||
}
|
||||
|
||||
return outputIndex === output.length ? output : output.slice(0, outputIndex);
|
||||
}
|
||||
|
||||
function decodeSecurePayload(raw, obfuscationVersion) {
|
||||
if (!obfuscationVersion) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
const bytes = decodeBase64Url(raw);
|
||||
|
||||
if (obfuscationVersion === '2') {
|
||||
for (let index = 0; index < bytes.length; index++) {
|
||||
bytes[index] ^= PIPE_OBFUSCATION_KEY[index % PIPE_OBFUSCATION_KEY.length];
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(strFromU8(gunzipSync(bytes)));
|
||||
}
|
||||
|
||||
function mergeHeaders(headers) {
|
||||
return {
|
||||
...HEADERS,
|
||||
...(headers || {})
|
||||
};
|
||||
}
|
||||
|
||||
/** Fetch text and throw a useful error for non-success responses. */
|
||||
export async function fetchText(url, options = {}) {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: mergeHeaders(options.headers)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} for ${url}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
/** Fetch a regular JSON endpoint. */
|
||||
export async function fetchJson(url, options = {}) {
|
||||
const raw = await fetchText(url, options);
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the compact, read-only request used by Miruro's secure API. Miruro
|
||||
* returns base64url-encoded gzip JSON, optionally XOR-obfuscated.
|
||||
*/
|
||||
export async function secureGet(path, query = {}) {
|
||||
const envelope = encodeBase64Url(JSON.stringify({
|
||||
path,
|
||||
method: 'GET',
|
||||
query,
|
||||
body: null
|
||||
}));
|
||||
const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`;
|
||||
let response;
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
response = await fetch(url, { headers: HEADERS });
|
||||
|
||||
if (response.ok || (response.status !== 403 && response.status !== 429 && response.status < 500)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response || !response.ok) {
|
||||
throw new Error(`Miruro API returned HTTP ${response ? response.status : 'unknown'}`);
|
||||
}
|
||||
|
||||
const raw = await response.text();
|
||||
const version = response.headers && response.headers.get
|
||||
? response.headers.get('x-obfuscated')
|
||||
: null;
|
||||
|
||||
return decodeSecurePayload(raw.trim(), version);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Miruro Provider
|
||||
* Main entry point.
|
||||
*/
|
||||
|
||||
import { extractStreams } from './extractor.js';
|
||||
|
||||
/**
|
||||
* Main function called by Nuvio
|
||||
* @param {string} tmdbId - TMDB numeric ID or IMDb tt-prefixed ID
|
||||
* @param {string} mediaType - 'movie' or 'tv'
|
||||
* @param {number} season - Season number (for TV)
|
||||
* @param {number} episode - Episode number (for TV)
|
||||
*/
|
||||
async function getStreams(tmdbId, mediaType, season, episode) {
|
||||
try {
|
||||
const episodeLabel = mediaType === 'tv' ? ` S${season}E${episode}` : '';
|
||||
console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`);
|
||||
|
||||
const streams = await extractStreams(tmdbId, mediaType, season, episode);
|
||||
console.log(`[Miruro] Found ${streams.length} stream(s)`);
|
||||
return streams;
|
||||
} catch (error) {
|
||||
console.error(`[Miruro] Error: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getStreams };
|
||||
Reference in new issue
Block a user