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