mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Viren070
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"projectName": "stremio-aiostreams",
|
||||
"lastCommit": ""
|
||||
}
|
||||
Generated
+4533
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "aiostreams",
|
||||
"version": "1.0.0",
|
||||
"description": "Stremio addon to combine streams into one addon",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"test": "npm run test --workspaces",
|
||||
"format": "prettier --write .",
|
||||
"build": "npm -w packages/types run build && npm -w packages/parser run build && npm -w packages/formatters run build && npm -w packages/wrappers run build && npm -w packages/addon run build",
|
||||
"build:watch": "tsc --build --watch",
|
||||
"start:addon": "npm -w packages/addon start",
|
||||
"start:addon:dev": "npm -w packages/addon run start:dev",
|
||||
"start:cloudflare-worker:dev": "npm -w packages/cloudflare-worker run dev",
|
||||
"deploy:beamup": "beamup",
|
||||
"deploy:cloudflare-worker": "npm -w packages/cloudflare-worker run deploy",
|
||||
"version": "npm version $npm_package_version --workspaces && git add **/package.json"
|
||||
},
|
||||
"author": "Viren070",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
"beamup-cli": "^1.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"prettier": "^3.3.2",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@aiostreams/addon",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest watch",
|
||||
"build": "tsc",
|
||||
"prepublish": "npm run build",
|
||||
"start": "node dist/server.js",
|
||||
"start:dev": "cross-env NODE_ENV=dev tsx watch src/server.ts"
|
||||
},
|
||||
"description": "Ps",
|
||||
"dependencies": {
|
||||
"@aiostreams/wrappers": "^1.0.0",
|
||||
"@aiostreams/formatters": "^1.0.0",
|
||||
"@aiostreams/types": "^1.0.0",
|
||||
"express": "^4.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configure Addon</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Configure Addon</h1>
|
||||
<form id="configForm">
|
||||
<label for="apiKey">API Key:</label>
|
||||
<input type="text" id="apiKey" name="apiKey" required>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
document.getElementById('configForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const apiKey = document.getElementById('apiKey').value;
|
||||
alert(`API Key submitted: ${apiKey}`);
|
||||
// You can add more logic here to handle the form submission
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import { BaseWrapper, Torbox, Torrentio } from '@aiostreams/wrappers';
|
||||
import { Stream, ParsedStream, StreamRequest, Config } from '@aiostreams/types';
|
||||
import { gdriveFormat } from '@aiostreams/formatters';
|
||||
|
||||
export class AIOStreams {
|
||||
private config: Config;
|
||||
|
||||
constructor(config: any) {
|
||||
this.config = config;
|
||||
this.configValidator();
|
||||
}
|
||||
|
||||
private configValidator() {
|
||||
if (!this.config) {
|
||||
throw new Error('No config provided');
|
||||
}
|
||||
if (!this.config.resolutions) {
|
||||
throw new Error('No resolutions provided');
|
||||
}
|
||||
if (!this.config.qualities) {
|
||||
throw new Error('No qualities provided');
|
||||
}
|
||||
if (!this.config.visualTags) {
|
||||
throw new Error('No visualTags provided');
|
||||
}
|
||||
}
|
||||
|
||||
public async getStreams (streamRequest: StreamRequest): Promise<Stream[]> {
|
||||
const streams: Stream[] = [];
|
||||
|
||||
const parsedStreams = await this.getParsedStreams(streamRequest);
|
||||
console.log(`Got ${parsedStreams.length} streams`);
|
||||
// Apply filtering
|
||||
const filteredResults = parsedStreams.filter(
|
||||
(parsedStream) =>
|
||||
this.config.resolutions.includes(parsedStream.resolution) &&
|
||||
this.config.qualities.includes(parsedStream.quality) &&
|
||||
parsedStream.visualTags.every((tag) => this.config.visualTags.includes(tag))// &&
|
||||
//(!parsedStream.provider || this.config.onlyShowCachedStreams || parsedStream.provider.cached)
|
||||
);
|
||||
console.log(`Filtered to ${filteredResults.length} streams`);
|
||||
// Apply sorting
|
||||
|
||||
|
||||
// initially sort by filename
|
||||
filteredResults.sort((a, b) => a.filename.localeCompare(b.filename));
|
||||
|
||||
// then apply our this.config sorting
|
||||
filteredResults.sort((a, b) => {
|
||||
const languageComparison = this.compareLanguages(a, b);
|
||||
if (languageComparison !== 0) return languageComparison;
|
||||
|
||||
for (const sortByField of this.config.sortBy) {
|
||||
const fieldComparison = this.compareByField(a, b, sortByField);
|
||||
if (fieldComparison !== 0) return fieldComparison;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
console.log("Sorted streams")
|
||||
|
||||
// Create stream objects
|
||||
filteredResults.map((parsedStream) => {
|
||||
streams.push(this.createStreamObject(parsedStream));
|
||||
});
|
||||
|
||||
console.log("Created stream objects")
|
||||
return streams;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private createStreamObject(parsedStream: ParsedStream): Stream {
|
||||
|
||||
let name: string = '';
|
||||
let description: string = '';
|
||||
console.log(`Creating stream object for ${parsedStream.filename} with formatter ${this.config.formatter}`);
|
||||
switch (this.config.formatter) {
|
||||
case 'gdrive': {
|
||||
const { name: _name, description: _description } = gdriveFormat(parsedStream);
|
||||
name = _name;
|
||||
description = _description;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unsupported formatter');
|
||||
}
|
||||
}
|
||||
|
||||
const combinedTags = [
|
||||
parsedStream.resolution,
|
||||
parsedStream.quality,
|
||||
parsedStream.encode,
|
||||
...parsedStream.visualTags,
|
||||
...parsedStream.audioTags,
|
||||
...parsedStream.languages,
|
||||
];
|
||||
|
||||
let stream: Stream;
|
||||
|
||||
stream = {
|
||||
url: parsedStream.url,
|
||||
externalUrl: parsedStream.externalUrl,
|
||||
infoHash: parsedStream.torrent?.infoHash,
|
||||
fileIdx: parsedStream.torrent?.fileIdx,
|
||||
name: name,
|
||||
description: description,
|
||||
subtitles: parsedStream.stream?.subtitles,
|
||||
sources: parsedStream.torrent?.sources,
|
||||
behaviorHints: {
|
||||
videoSize: Math.floor(parsedStream.size || 0),
|
||||
filename: parsedStream.filename,
|
||||
bingeGroup: `${parsedStream.addonName}|${combinedTags.join('|')}`,
|
||||
proxyHeaders: parsedStream.stream?.behaviorHints?.proxyHeaders,
|
||||
notWebReady: parsedStream.stream?.behaviorHints?.notWebReady,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
return stream;
|
||||
|
||||
}
|
||||
|
||||
private compareLanguages(a: ParsedStream, b: ParsedStream) {
|
||||
if (this.config.prioritiseLanguage) {
|
||||
const aHasPrioritisedLanguage = a.languages.includes(
|
||||
this.config.prioritiseLanguage
|
||||
);
|
||||
const bHasPrioritisedLanguage = b.languages.includes(
|
||||
this.config.prioritiseLanguage
|
||||
);
|
||||
const aHasMultiLanguage = a.languages.includes('Multi');
|
||||
const bHasMultiLanguage = b.languages.includes('Multi');
|
||||
|
||||
if (aHasPrioritisedLanguage && !bHasPrioritisedLanguage) return -1;
|
||||
if (!aHasPrioritisedLanguage && bHasPrioritisedLanguage) return 1;
|
||||
|
||||
if (aHasMultiLanguage && !bHasMultiLanguage) return -1;
|
||||
if (!aHasMultiLanguage && bHasMultiLanguage) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private compareByField(a: ParsedStream, b: ParsedStream, field: string) {
|
||||
if (field === 'resolution') {
|
||||
return (
|
||||
this.config.resolutions.indexOf(a.resolution) -
|
||||
this.config.resolutions.indexOf(b.resolution)
|
||||
);
|
||||
} else if (field === 'cached') {
|
||||
let aCanbeCached = a.provider
|
||||
let bCanbeCached = b.provider;
|
||||
let aCached = a.provider?.cached
|
||||
let bCached = b.provider?.cached;
|
||||
|
||||
// prioritise non debrid/usenet p2p over uncached
|
||||
if (aCanbeCached && !bCanbeCached && !aCached) return 1;
|
||||
if (!aCanbeCached && bCanbeCached && !bCached) return -1;
|
||||
if (aCanbeCached && bCanbeCached) {
|
||||
if (aCached === bCached) return 0;
|
||||
return aCached ? -1 : 1;
|
||||
}
|
||||
} else if (field === 'hasProvider') {
|
||||
// files from a provider should be prioritised and then
|
||||
let aHasProvider = a.provider;
|
||||
let bHasProvider = b.provider;
|
||||
if (aHasProvider && !bHasProvider) return -1;
|
||||
if (!aHasProvider && bHasProvider) return 1;
|
||||
} else if (field === 'provider') {
|
||||
// sort files with providers by name
|
||||
let aProvider = a.provider?.name;
|
||||
let bProvider = b.provider?.name;
|
||||
|
||||
if (aProvider && bProvider) {
|
||||
return aProvider.localeCompare(bProvider);
|
||||
}
|
||||
} else if (field === 'size') {
|
||||
return (b.size || 0) - (a.size || 0);
|
||||
} else if (field === 'seeders') {
|
||||
if (a.torrent?.seeders && b.torrent?.seeders) {
|
||||
return b.torrent.seeders - a.torrent.seeders;
|
||||
}
|
||||
} else if (field === 'quality') {
|
||||
return (
|
||||
this.config.qualities.indexOf(a.quality) - this.config.qualities.indexOf(b.quality)
|
||||
);
|
||||
} else if (field === 'visualTag') {
|
||||
// Find the highest priority visual tag in each file
|
||||
const getIndexOfTag = (tag: string) =>
|
||||
this.config.considerHdrTagsAsEqual && tag.startsWith('HDR')
|
||||
? this.config.visualTags.indexOf('HDR10+')
|
||||
: this.config.visualTags.indexOf(tag);
|
||||
const aVisualTagIndex = a.visualTags.reduce(
|
||||
(minIndex, tag) => Math.min(minIndex, getIndexOfTag(tag)),
|
||||
this.config.visualTags.length
|
||||
);
|
||||
|
||||
const bVisualTagIndex = b.visualTags.reduce(
|
||||
(minIndex, tag) => Math.min(minIndex, getIndexOfTag(tag)),
|
||||
this.config.visualTags.length
|
||||
);
|
||||
// Sort by the visual tag index
|
||||
return aVisualTagIndex - bVisualTagIndex;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
private async getParsedStreams(streamRequest: StreamRequest): Promise<ParsedStream[]> {
|
||||
const parsedStreams: ParsedStream[] = [];
|
||||
for (const addon of this.config.addons) {
|
||||
switch (addon) {
|
||||
case ('gdrive'): {
|
||||
break;
|
||||
}
|
||||
case ('torbox'): {
|
||||
try {
|
||||
if (!this.config.apiKeys.torbox) {
|
||||
throw new Error('No torbox api key provided');
|
||||
}
|
||||
const wrapper = new Torbox(this.config.apiKeys.torbox);
|
||||
const streams = await wrapper.getParsedStreams(streamRequest);
|
||||
console.log(`Got streams from ${addon}: ${streams.length}`);
|
||||
parsedStreams.push(...streams);
|
||||
} catch (e) {
|
||||
console.error(`Error getting streams from ${addon}: ${e}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ('torrentio'): {
|
||||
try {
|
||||
const wrapper = new Torrentio(this.config.apiKeys);
|
||||
const streams = await wrapper.getParsedStreams(streamRequest);
|
||||
console.log(`Got streams from ${addon}: ${streams.length}`);
|
||||
parsedStreams.push(...streams);
|
||||
} catch (e) {
|
||||
console.error(`Error getting streams from ${addon}: ${e}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
try {
|
||||
const wrapper = new BaseWrapper("unknown", addon);
|
||||
const streams = await wrapper.getParsedStreams(streamRequest);
|
||||
console.log(`Got streams from ${addon}: ${streams.length}`);
|
||||
|
||||
parsedStreams.push(...streams);
|
||||
} catch (e) {
|
||||
console.error(`Error getting streams from ${addon}: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsedStreams;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './addon';
|
||||
@@ -0,0 +1,104 @@
|
||||
import express, { Request, Response} from 'express';
|
||||
import path from 'path';
|
||||
import { AIOStreams } from './addon';
|
||||
import { StreamRequest } from '@aiostreams/types';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Built-in middleware for parsing JSON
|
||||
app.use(express.json());
|
||||
|
||||
// Built-in middleware for parsing URL-encoded data
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.redirect('/configure');
|
||||
});
|
||||
|
||||
app.get('/configure', (req, res) => {
|
||||
// Render a form for configuring the addon
|
||||
res.sendFile(path.join(__dirname, '..', 'public', 'configure.html'));
|
||||
});
|
||||
|
||||
app.get('/stream/:type/:id', (req, res) => {
|
||||
const response = {
|
||||
streams: [
|
||||
{
|
||||
url: 'https://example.com',
|
||||
name: 'Missing Config',
|
||||
description: 'You must configure this addon to use it',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
res.send(JSON.stringify(response, null, 2)); // 2 spaces for indentation
|
||||
});
|
||||
|
||||
app.get('/:config/stream/:type/:id', (req: Request, res: Response) => {
|
||||
const config = req.params.config;
|
||||
|
||||
// Decode Base64 encoded JSON config
|
||||
const decodedConfig = Buffer.from(config, 'base64').toString('utf-8');
|
||||
const configJson = JSON.parse(decodedConfig);
|
||||
|
||||
console.log(`Config: ${JSON.stringify(configJson, null, 2)}`);
|
||||
|
||||
const streamMatch = new RegExp(`/${config}/stream/(movie|series)/tt([0-9]{7,})(?::([0-9]+):([0-9]+))?\.json`).exec(req.path);
|
||||
|
||||
if (!streamMatch) {
|
||||
res.status(400).send('Invalid request');
|
||||
return;
|
||||
}
|
||||
|
||||
const [type, id, season, episode] = streamMatch.slice(1);
|
||||
|
||||
console.log(`Type: ${type}, ID: ${id}, Season: ${season}, Episode: ${episode}`);
|
||||
|
||||
let streamRequest: StreamRequest;
|
||||
|
||||
switch (type) {
|
||||
case 'series':
|
||||
if (!season || !episode) {
|
||||
res.status(400).send('Invalid request');
|
||||
return;
|
||||
}
|
||||
streamRequest = {
|
||||
id,
|
||||
type: 'series',
|
||||
season,
|
||||
episode,
|
||||
};
|
||||
break;
|
||||
case 'movie':
|
||||
if (season || episode) {
|
||||
res.status(400).send('Invalid request');
|
||||
return;
|
||||
}
|
||||
streamRequest = {
|
||||
id,
|
||||
type: 'movie',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
res.status(400).send('Invalid request');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const aioStreams = new AIOStreams(configJson);
|
||||
|
||||
|
||||
aioStreams.getStreams({ id, type, season, episode }).then((streams) => {
|
||||
res.send(streams);
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running at http://localhost:${port}`);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../wrappers"
|
||||
},
|
||||
{
|
||||
"path": "../formatters"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@aiostreams/cloudflare-worker",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"dev": "wrangler dev src/index.ts",
|
||||
"deploy": "wrangler deploy --minify src/index.ts",
|
||||
"build": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aiostreams/addon": "^1.0.0",
|
||||
"hono": "^4.5.3",
|
||||
"hono-stremio": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getRouter } from 'hono-stremio';
|
||||
import { addonInterface, landingHTML } from '@aiostreams/addon';
|
||||
|
||||
const addonRouter = getRouter(addonInterface, { landingHTML });
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.route('/', addonRouter);
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNext"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../addon"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
name = "aiostreams-cloudflare-worker"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2024-12-02"
|
||||
workers_dev = true
|
||||
preview_urls = true
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
[observability.logs]
|
||||
enabled = true
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@aiostreams/formatters",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"build": "tsc"
|
||||
},
|
||||
"description": "Library to take parsed information and return a formatted Stremio stream name and description",
|
||||
"dependencies": {
|
||||
"@aiostreams/types": "1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ParsedStream } from '@aiostreams/types';
|
||||
import { formatSize } from './utils';
|
||||
|
||||
export function gdriveFormat(stream: ParsedStream): { name: string, description: string} {
|
||||
let name: string = '';
|
||||
|
||||
if (stream.provider) {
|
||||
name += stream.provider.cached ? `[${stream.provider.name}⚡]\n` : `[${stream.provider.name}⏳]\n`;
|
||||
}
|
||||
|
||||
if (stream.torrent?.infoHash) {
|
||||
name += `[P2P]\n`;
|
||||
}
|
||||
|
||||
name += `${stream.addonName} ${stream.resolution}`;
|
||||
|
||||
let description: string = `🎥 ${stream.quality} ${stream.encode ? '🎞️ ' + stream.encode : ''}`;
|
||||
|
||||
if (stream.visualTags.length > 0 || stream.audioTags.length > 0) {
|
||||
description += '\n';
|
||||
|
||||
description += stream.visualTags.length > 0 ? `📺 ${stream.visualTags.join(' | ')} ` : '';
|
||||
description += stream.audioTags.length > 0 ? `🎧 ${stream.audioTags.join(' | ')}` : '';
|
||||
}
|
||||
if (stream.size || stream.torrent?.seeders || stream.usenet?.age) {
|
||||
description += '\n';
|
||||
|
||||
description += `📦 ${formatSize(stream.size || 0)}`;
|
||||
|
||||
description += stream.torrent?.seeders ? `👥 ${stream.torrent.seeders} ` : '';
|
||||
|
||||
description += stream.usenet?.age ? `📅 ${stream.usenet.age}d` : '';
|
||||
}
|
||||
if (stream.languages.length !== 0) {
|
||||
description += `\n🔊 ${stream.languages.join(' | ')}`;
|
||||
}
|
||||
|
||||
description += `\n📄 ${stream.filename}`;
|
||||
|
||||
return { name, description };
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './gdrive';
|
||||
export * from './utils';
|
||||
@@ -0,0 +1,7 @@
|
||||
export function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1000;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@aiostreams/parser",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"build": "tsc"
|
||||
},
|
||||
"description": "Library to parse a torrent filename ",
|
||||
"dependencies": {
|
||||
"@aiostreams/types": "1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './parser';
|
||||
export * from './regex';
|
||||
export * from './utils';
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ParsedNameData } from '@aiostreams/types';
|
||||
import { PARSE_REGEX } from './regex';
|
||||
|
||||
const DEFAULT_RESOLUTION = 'Unknown';
|
||||
const DEFAULT_QUALITY = 'Unknown';
|
||||
|
||||
function matchPattern(
|
||||
filename: string,
|
||||
patterns: Record<string, RegExp>
|
||||
): string {
|
||||
return (
|
||||
Object.entries(patterns).find(([_, pattern]) =>
|
||||
pattern.test(filename)
|
||||
)?.[0] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
function matchMultiplePatterns(
|
||||
filename: string,
|
||||
patterns: Record<string, RegExp>
|
||||
): string[] {
|
||||
return Object.entries(patterns)
|
||||
.filter(([_, pattern]) => pattern.test(filename))
|
||||
.map(([tag]) => tag);
|
||||
}
|
||||
|
||||
export function parseFilename(filename: string): ParsedNameData {
|
||||
const resolution =
|
||||
matchPattern(filename, PARSE_REGEX.resolutions) || DEFAULT_RESOLUTION;
|
||||
const quality =
|
||||
matchPattern(filename, PARSE_REGEX.qualities) || DEFAULT_QUALITY;
|
||||
let visualTags = matchMultiplePatterns(filename, PARSE_REGEX.visualTags);
|
||||
const audioTags = matchMultiplePatterns(filename, PARSE_REGEX.audioTags);
|
||||
const encode = matchPattern(filename, PARSE_REGEX.encodes);
|
||||
const languages = matchMultiplePatterns(filename, PARSE_REGEX.languages);
|
||||
|
||||
return {
|
||||
resolution,
|
||||
quality,
|
||||
languages,
|
||||
encode,
|
||||
audioTags,
|
||||
visualTags,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
export const PARSE_REGEX = {
|
||||
resolutions: {
|
||||
'2160p': /(?<![^ [_\-.])(4k|2160p|uhd)(?=[ \]_.-]|$)/i,
|
||||
'1080p': /(?<![^ [_\-.])(1080p|fhd)(?=[ \]_.-]|$)/i,
|
||||
'720p': /(?<![^ [_\-.])(720p|hd)(?=[ \]_.-]|$)/i,
|
||||
'480p': /(?<![^ [_\-.])(480p|sd)(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
qualities: {
|
||||
'BluRay REMUX':
|
||||
/(?<![^ [_\-.])((blu[ .\-_]?ray|bd|br|b)[ .\-_]?remux)(?=[ \]_.-]|$)/i,
|
||||
BluRay:
|
||||
/(?<![^ [_\-.])(blu[ .\-_]?ray|((bd|br|b)[ .\-_]?(rip|r)?))(?![ .\-_]?remux)(?=[ \]_.-]|$)/i,
|
||||
'WEB-DL':
|
||||
/(?<![^ [_\-.])(web[ .\-_]?(dl)?)(?![ .\-_]?DLRip)(?=[ \]_.-]|$)/i,
|
||||
WEBRip: /(?<![^ [_\-.])(web[ .\-_]?rip)(?=[ \]_.-]|$)/i,
|
||||
HDRip:
|
||||
/(?<![^ [_\-.])(hd[ .\-_]?rip|web[ .\-_]?dl[ .\-_]?rip)(?=[ \]_.-]|$)/i,
|
||||
'HC HD-Rip': /(?<![^ [_\-.])(hc|hd[ .\-_]?rip)(?=[ \]_.-]|$)/i,
|
||||
DVDRip: /(?<![^ [_\-.])(dvd[ .\-_]?(rip|mux|r|full|5|9))(?=[ \]_.-]|$)/i,
|
||||
HDTV: /(?<![^ [_\-.])((hd|pd)tv|tv[ .\-_]?rip|hdtv[ .\-_]?rip|dsr(ip)?|sat[ .\-_]?rip)(?=[ \]_.-]|$)/i,
|
||||
CAM: /(?<![^ [_\-.])(cam|hdcam|cam[ .\-_]?rip)(?=[ \]_.-]|$)/i,
|
||||
TS: /(?<![^ [_\-.])(telesync|ts|hd[ .\-_]?ts|pdvd|predvdrip)(?=[ \]_.-]|$)/i,
|
||||
TC: /(?<![^ [_\-.])(telecine|tc|hd[ .\-_]?tc)(?=[ \]_.-]|$)/i,
|
||||
SCR: /(?<![^ [_\-.])(((dvd|bd|web)?[ .\-_]?)?(scr(eener)?))(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
visualTags: {
|
||||
'HDR10+': /(?<![^ [_\-.])(hdr10[ .\-_]?[+]|hdr10plus)(?=[ \]_.-]|$)/i,
|
||||
HDR10: /(?<![^ [_\-.])(hdr10)(?=[ \]_.-]|$)/i,
|
||||
HDR: /(?<![^ [_\-.])(hdr)(?=[ \]_.-]|$)/i,
|
||||
DV: /(?<![^ [_\-.])(dolby[ .\-_]?vision(?:[ .\-_]?atmos)?|dv)(?=[ \]_.-]|$)/i,
|
||||
IMAX: /(?<![^ [_\-.])(imax)(?=[ \]_.-]|$)/i,
|
||||
AI: /(?<![^ [_\-.])(ai[ .\-_]?(upscale|enhanced|remaster))(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
audioTags: {
|
||||
Atmos: /(?<![^ [_\-.])(atmos)(?=[ \]_.-]|$)/i,
|
||||
'DD+':
|
||||
/(?<![^ [_\-.])((?:ddp|dolby[ .\-_]?digital[ .\-_]?plus)(?:[ .\-_]?(5\.1|7\.1))?)(?=[ \]_.-]|$)/i,
|
||||
DD: /(?<![^ [_\-.])((?:dd|dolby[ .\-_]?digital)(?:[ .\-_]?(5\.1|7\.1))?)(?=[ \]_.-]|$)/i,
|
||||
'DTS-HD': /(?<![^ [_\-.])(dts[ .\-_]?hd[ .\-_]?ma?)(?=[ \]_.-]|$)/i,
|
||||
DTS: /(?<![^ [_\-.])(dts)(?=[ \]_.-]|$)/i,
|
||||
TrueHD: /(?<![^ [_\-.])(truehd)(?=[ \]_.-]|$)/i,
|
||||
5.1: /(?<![^ [_\-.])((?:ddp|dd)?[ .\-_]?5\.1)(?=[ \]_.-]|$)/i,
|
||||
7.1: /(?<![^ [_\-.])((?:ddp|dd)?[ .\-_]?7\.1)(?=[ \]_.-]|$)/i,
|
||||
AC3: /(?<![^ [_\-.])(ac[ .\-_]?3)(?=[ \]_.-]|$)/i,
|
||||
AAC: /(?<![^ [_\-.])(aac)(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
encodes: {
|
||||
x265: /(?<![^ [_\-.])(x265|h265|h\.265)(?=[ \]_.-]|$)/i,
|
||||
x264: /(?<![^ [_\-.])(x264|h264|h\.264)(?=[ \]_.-]|$)/i,
|
||||
HEVC: /(?<![^ [_\-.])(hevc)(?=[ \]_.-]|$)/i,
|
||||
AVC: /(?<![^ [_\-.])(avc)(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
languages: {
|
||||
English: /(?<![^ [_\-.])(english|eng)(?=[ \]_.-]|$)/i,
|
||||
Hindi: /(?<![^ [_\-.])(hindi|hin)(?=[ \]_.-]|$)/i,
|
||||
Tamil: /(?<![^ [_\-.])(tamil|tam)(?=[ \]_.-]|$)/i,
|
||||
Telugu: /(?<![^ [_\-.])(telugu)(?=[ \]_.-]|$)/i,
|
||||
Malayalam: /(?<![^ [_\-.])(malayalam)(?=[ \]_.-]|$)/i,
|
||||
Kannada: /(?<![^ [_\-.])(kannada)(?=[ \]_.-]|$)/i,
|
||||
Bengali: /(?<![^ [_\-.])(bengali)(?=[ \]_.-]|$)/i,
|
||||
Punjabi: /(?<![^ [_\-.])(punjabi)(?=[ \]_.-]|$)/i,
|
||||
Marathi: /(?<![^ [_\-.])(marathi)(?=[ \]_.-]|$)/i,
|
||||
French: /(?<![^ [_\-.])(french|fra)(?=[ \]_.-]|$)/i,
|
||||
Spanish: /(?<![^ [_\-.])(spanish|spa)(?=[ \]_.-]|$)/i,
|
||||
German: /(?<![^ [_\-.])(german|deu)(?=[ \]_.-]|$)/i,
|
||||
Italian: /(?<![^ [_\-.])(italian|ita)(?=[ \]_.-]|$)/i,
|
||||
Japanese: /(?<![^ [_\-.])(japanese|jpn)(?=[ \]_.-]|$)/i,
|
||||
Korean: /(?<![^ [_\-.])(korean|kor)(?=[ \]_.-]|$)/i,
|
||||
Chinese: /(?<![^ [_\-.])(chinese|chn)(?=[ \]_.-]|$)/i,
|
||||
Russian: /(?<![^ [_\-.])(russian|rus)(?=[ \]_.-]|$)/i,
|
||||
Multi: /(?<![^ [_\-.])(MULTi)(?=[ \]_.-]|$)/i,
|
||||
'Dual Audio': /(?<![^ [_\-.])(dual[ .\-_]?audio)(?=[ \]_.-]|$)/i,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
export function extractSizeInBytes(string: string, k: number): number {
|
||||
const sizePattern = /(\d+(\.\d+)?)\s?(KB|MB|GB)/;
|
||||
const match = string.match(sizePattern);
|
||||
if (!match) return 0;
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[3];
|
||||
|
||||
switch (unit) {
|
||||
case "GB":
|
||||
return value * k * k * k;
|
||||
case "MB":
|
||||
return value * k * k;
|
||||
case "KB":
|
||||
return value * k;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@aiostreams/types",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"build": "tsc"
|
||||
},
|
||||
"description": "Library to parse a torrent filename "
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './types';
|
||||
@@ -0,0 +1,113 @@
|
||||
// the data that is parsed from a filename
|
||||
export interface ParsedNameData {
|
||||
resolution: string;
|
||||
quality: string;
|
||||
encode: string;
|
||||
visualTags: string[];
|
||||
audioTags: string[];
|
||||
languages: string[];
|
||||
}
|
||||
|
||||
// the parsed stream data which is to be used to create the final stream object
|
||||
export interface ParsedStream extends ParsedNameData {
|
||||
addonName: string;
|
||||
filename: string;
|
||||
size?: number;
|
||||
provider?: {
|
||||
name: string;
|
||||
cached: boolean;
|
||||
};
|
||||
torrent?: {
|
||||
infoHash?: string;
|
||||
fileIdx?: number;
|
||||
seeders?: number;
|
||||
sources?: string[];
|
||||
};
|
||||
usenet?: {
|
||||
age?: number;
|
||||
};
|
||||
url?: string;
|
||||
externalUrl?: string;
|
||||
|
||||
stream?: {
|
||||
subtitles?: Subtitle[];
|
||||
behaviorHints?: {
|
||||
countryWhitelist?: string[];
|
||||
notWebReady?: boolean;
|
||||
proxyHeaders?: {
|
||||
request?: { [key: string]: string };
|
||||
response?: { [key: string]: string };
|
||||
};
|
||||
videoHash?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface Stream {
|
||||
url?: string;
|
||||
externalUrl?: string;
|
||||
infoHash?: string;
|
||||
fileIdx?: number;
|
||||
name?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
subtitles?: Subtitle[];
|
||||
sources?: string[];
|
||||
behaviorHints?: {
|
||||
countryWhitelist?: string[];
|
||||
notWebReady?: boolean;
|
||||
bingeGroup?: string;
|
||||
proxyHeaders?: {
|
||||
request?: { [key: string]: string };
|
||||
response?: { [key: string]: string };
|
||||
};
|
||||
videoHash?: string;
|
||||
videoSize?: number;
|
||||
filename?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface Subtitle {
|
||||
id: string;
|
||||
url: string;
|
||||
lang: string;
|
||||
}
|
||||
|
||||
interface MovieRequest {
|
||||
id: string;
|
||||
type: 'movie';
|
||||
}
|
||||
|
||||
interface SeriesRequest {
|
||||
id: string;
|
||||
type: 'series';
|
||||
season: string;
|
||||
episode: string;
|
||||
}
|
||||
|
||||
export type StreamRequest = MovieRequest | SeriesRequest;
|
||||
|
||||
|
||||
|
||||
export interface Config {
|
||||
resolutions: string[];
|
||||
qualities: string[];
|
||||
visualTags: string[];
|
||||
sortBy: string[];
|
||||
onlyShowCachedStreams: boolean;
|
||||
considerHdrTagsAsEqual: boolean;
|
||||
prioritiseLanguage: string | null;
|
||||
addons: string[];
|
||||
formatter: string;
|
||||
apiKeys: {
|
||||
realDebrid: string;
|
||||
torbox: string;
|
||||
debridLink: string;
|
||||
allDebrid: string;
|
||||
premiumize: string;
|
||||
offcloud: string;
|
||||
putIo: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@aiostreams/wrappers",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"build": "tsc"
|
||||
},
|
||||
"description": "Library with wrappers for different addons",
|
||||
"devDependencies": {
|
||||
"@aiostreams/parser": "^1.0.0",
|
||||
"@aiostreams/types": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
Stream,
|
||||
ParsedStream,
|
||||
StreamRequest,
|
||||
ParsedNameData,
|
||||
} from '@aiostreams/types';
|
||||
import { parseFilename } from '@aiostreams/parser';
|
||||
/*
|
||||
|
||||
1. wrapper gets the streams from the addon using the StreamRequest object which contains the type, id, season, and episode
|
||||
This returns a dictionary with a streams key that contains an array of Stream objects
|
||||
|
||||
2. wrapper parses each stream, and returns an object that contains all the necessary information
|
||||
*/
|
||||
export class BaseWrapper {
|
||||
private readonly streamPath: string = 'stream/{type}/tt{id}.json';
|
||||
private indexerTimeout: number;
|
||||
private addonName: string;
|
||||
private addonUrl: string;
|
||||
constructor(addonName: string, addonUrl: string, indexerTimeout?: number) {
|
||||
this.addonName = addonName;
|
||||
this.addonUrl = addonUrl;
|
||||
this.indexerTimeout = indexerTimeout || 3000;
|
||||
}
|
||||
|
||||
public async getParsedStreams(
|
||||
streamRequest: StreamRequest
|
||||
): Promise<ParsedStream[]> {
|
||||
const streams: Stream[] = await this.getStreams(streamRequest);
|
||||
console.log('Streams:', streams);
|
||||
const parsedStreams: ParsedStream[] = streams
|
||||
.map((stream) => this.parseStream(stream))
|
||||
.filter((parsedStream) => parsedStream !== undefined);
|
||||
return parsedStreams;
|
||||
}
|
||||
|
||||
private getStreamUrl(streamRequest: StreamRequest) {
|
||||
let finalId =
|
||||
streamRequest.type === 'series'
|
||||
? `${streamRequest.id}:${streamRequest.season}:${streamRequest.episode}`
|
||||
: streamRequest.id;
|
||||
return (
|
||||
this.addonUrl +
|
||||
this.streamPath
|
||||
.replace('{type}', streamRequest.type)
|
||||
.replace('{id}', finalId)
|
||||
);
|
||||
}
|
||||
|
||||
protected async getStreams(streamRequest: StreamRequest): Promise<Stream[]> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, this.indexerTimeout);
|
||||
|
||||
console.log('Fetching streams from', this.getStreamUrl(streamRequest), 'with timeout', this.indexerTimeout);
|
||||
let url = this.getStreamUrl(streamRequest);
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
return Promise.reject(new Error(await response.text()));
|
||||
}
|
||||
|
||||
let results: { streams: Stream[] } = await response.json();
|
||||
return results.streams;
|
||||
}
|
||||
|
||||
protected createParsedResult(
|
||||
parsedInfo: ParsedNameData,
|
||||
stream: Stream,
|
||||
filename: string,
|
||||
size?: number
|
||||
): ParsedStream {
|
||||
return {
|
||||
...parsedInfo,
|
||||
addonName: this.addonName,
|
||||
filename: filename,
|
||||
size: size,
|
||||
url: stream.url,
|
||||
externalUrl: stream.externalUrl,
|
||||
torrent: {
|
||||
infoHash: stream.infoHash,
|
||||
fileIdx: stream.fileIdx,
|
||||
sources: stream.sources,
|
||||
},
|
||||
stream: {
|
||||
subtitles: stream.subtitles,
|
||||
behaviorHints: {
|
||||
countryWhitelist: stream.behaviorHints?.countryWhitelist,
|
||||
notWebReady: stream.behaviorHints?.notWebReady,
|
||||
proxyHeaders: {
|
||||
request: stream.behaviorHints?.proxyHeaders?.request,
|
||||
response: stream.behaviorHints?.proxyHeaders?.response,
|
||||
},
|
||||
videoHash: stream.behaviorHints?.videoHash,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected parseStream(stream: Stream): ParsedStream | undefined {
|
||||
// attempt to look for filename in behaviorHints.filename, return undefined if not found
|
||||
let filename = stream.behaviorHints?.filename || undefined;
|
||||
|
||||
if (!filename) {
|
||||
return undefined;
|
||||
}
|
||||
// parse the filename using our parser.
|
||||
let parsedInfo: ParsedNameData = parseFilename(filename);
|
||||
|
||||
// see if size is present in behaviorHints
|
||||
let size: number | undefined;
|
||||
let formattedSize: string | undefined;
|
||||
if (stream.behaviorHints?.videoSize) {
|
||||
size = stream.behaviorHints.videoSize;
|
||||
}
|
||||
|
||||
return this.createParsedResult(parsedInfo, stream, filename, size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './base';
|
||||
export * from './torbox';
|
||||
export * from './torrentio';
|
||||
@@ -0,0 +1,73 @@
|
||||
import { BaseWrapper } from "./base";
|
||||
import { ParsedNameData, ParsedStream } from "@aiostreams/types";
|
||||
import { parseFilename } from "@aiostreams/parser";
|
||||
|
||||
interface TorboxStream {
|
||||
name: string;
|
||||
description: string;
|
||||
size: number;
|
||||
url: string;
|
||||
type: string;
|
||||
seeders: number;
|
||||
is_cached: boolean;
|
||||
}
|
||||
|
||||
export class Torbox extends BaseWrapper {
|
||||
private readonly name: string = "Torbox";
|
||||
constructor(apiKey: string) {
|
||||
super("Torbox", "https://stremio.torbox.app/" + apiKey + "/", 10000);
|
||||
}
|
||||
|
||||
protected parseStream(stream: TorboxStream): ParsedStream | undefined {
|
||||
|
||||
if (stream.name.includes("Your Media")) {
|
||||
return undefined;
|
||||
}
|
||||
let type = stream.type;
|
||||
const [quality, filename, _, language, ageOrSeeders] = stream.description.split("\n").map(
|
||||
(field: string) => {
|
||||
if (field.startsWith("Type")) {
|
||||
const [typeField, ageOrSeeders] = field.split("|")
|
||||
if (!["torrent", "usenet"].includes(type)) {
|
||||
type = typeField.split(":")[1].trim().toLowerCase();
|
||||
}
|
||||
const [_, value] = ageOrSeeders.split(":");
|
||||
return value.trim();
|
||||
}
|
||||
const [_, value] = field.split(":");
|
||||
return value.trim();
|
||||
}
|
||||
)
|
||||
|
||||
const parsedFilename: ParsedNameData = parseFilename(filename);
|
||||
|
||||
if (parsedFilename.quality === "Unknown" && quality !== "Unknown") {
|
||||
parsedFilename.quality = quality;
|
||||
}
|
||||
if (!parsedFilename.languages.some((lang: string) => lang.toLowerCase() === language.toLowerCase()) && language !== "Unknown") {
|
||||
if (!(language === "BENGALI" && RegExp(/(?<![^ [_\-.])(ben[ _\-.]?the[ _\-.]?men)(?=[ \]_.-]|$)/i).test(filename))) {
|
||||
parsedFilename.languages.push(language);
|
||||
}
|
||||
}
|
||||
|
||||
const sizeInBytes = stream.size;
|
||||
|
||||
return {
|
||||
...parsedFilename,
|
||||
filename,
|
||||
size: sizeInBytes,
|
||||
addonName: this.name,
|
||||
url: stream.url,
|
||||
torrent: type === "torrent" ? {
|
||||
seeders: stream.seeders,
|
||||
} : undefined,
|
||||
usenet: type === "usenet" ? {
|
||||
age: parseInt(ageOrSeeders)
|
||||
} : undefined,
|
||||
provider: {
|
||||
name: "TB",
|
||||
cached: stream.is_cached
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ParsedNameData } from "@aiostreams/types";
|
||||
import { parseFilename, extractSizeInBytes } from "@aiostreams/parser";
|
||||
import { ParsedStream, Stream } from "@aiostreams/types";
|
||||
import { BaseWrapper } from "./base";
|
||||
|
||||
export class Torrentio extends BaseWrapper {
|
||||
private readonly name: string = "Torrentio";
|
||||
|
||||
constructor(apiKeys: Record<string, string>) {
|
||||
// create a string of name=apikey pairs separated by |
|
||||
super("Torrentio", "https://torrentio.strem.fun/" + Object.entries(apiKeys).map(([name, apiKey]) => `${name.toLowerCase()}=${apiKey}`).join("|") + "/");
|
||||
|
||||
}
|
||||
|
||||
protected parseStream(stream: Stream): ParsedStream {
|
||||
const filename = stream?.behaviorHints?.filename ? stream.behaviorHints.filename.trim() : stream.title!.split("\n")[0];
|
||||
const parsedFilename: ParsedNameData = parseFilename(filename);
|
||||
const sizeInBytes = stream.title ? extractSizeInBytes(stream.title, 1024) : 0;
|
||||
const debridMatch = RegExp(/^\[([a-zA-Z]{2})(\+| download)\]/).exec(stream.name!);
|
||||
const debrid = debridMatch ? {
|
||||
provider: debridMatch[1],
|
||||
cached: debridMatch[2] === "+"
|
||||
} : undefined;
|
||||
const seedersMatch = RegExp(/👤 (\d+)/).exec(stream.title!)
|
||||
const seeders = seedersMatch ? parseInt(seedersMatch[1]) : undefined;
|
||||
|
||||
return {
|
||||
...parsedFilename,
|
||||
filename,
|
||||
size: sizeInBytes,
|
||||
addonName: this.name,
|
||||
url: stream.url,
|
||||
externalUrl: stream.externalUrl,
|
||||
torrent: {
|
||||
infoHash: stream.infoHash,
|
||||
fileIdx: stream.fileIdx,
|
||||
seeders: seeders,
|
||||
sources: stream.sources
|
||||
},
|
||||
provider: debrid ? {
|
||||
name: debrid.provider,
|
||||
cached: debrid.cached
|
||||
} : undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true,
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../parser"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"incremental": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"references": [
|
||||
{
|
||||
"path": "./packages/addon"
|
||||
},
|
||||
{
|
||||
"path": "./packages/wrappers"
|
||||
},
|
||||
{
|
||||
"path": "./packages/parser"
|
||||
},
|
||||
{
|
||||
"path": "./packages/formatters"
|
||||
},
|
||||
{
|
||||
"path": "./packages/types"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user