rework removing providers filters, and clean up project a little

Also wraps in pm2, and introduces linting, and dev watch
This commit is contained in:
iPromKnight
2024-02-02 13:27:15 +00:00
parent c1169a15ee
commit 188ffd10f3
39 changed files with 4294 additions and 312 deletions

View File

@@ -0,0 +1 @@
*.ts

View File

@@ -0,0 +1,39 @@
/** @type {import("eslint").ESLint.Options} */
module.exports = {
env: {
es2024: true,
node: true,
},
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
},
parserOptions: {
sourceType: "module",
},
plugins: ["import", "import-helpers"],
rules: {
"default-case": "off",
"import/no-duplicates": "off",
"import/no-extraneous-dependencies": ["off", { devDependencies: ["backend", "frontend", "mobile"] }],
"import/order": "off",
"import-helpers/order-imports": [
"warn",
{
alphabetize: {
order: "asc",
},
},
],
"lines-between-class-members": ["error", "always", { exceptAfterSingleLine: true }],
"no-continue": "off",
"no-param-reassign": "off",
"no-plusplus": ["error", { allowForLoopAfterthoughts: true }],
"no-restricted-syntax": "off",
"no-unused-expressions": ["off", { allowShortCircuit: true }],
"no-unused-vars": "off",
"no-use-before-define": "off",
"one-var": ["error", { uninitialized: "consecutive" }],
"prefer-destructuring": "warn",
},
};

View File

@@ -1,17 +1,29 @@
FROM node:16-alpine
# --- Build Stage ---
FROM node:lts-alpine AS builder
RUN apk update && apk upgrade && \
apk add --no-cache nodejs npm git curl && \
rm -fr /var/cache/apk/*
apk add --no-cache git
WORKDIR /app
COPY package*.json .
RUN npm ci --only-production
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# --- Runtime Stage ---
FROM node:lts-alpine
# Install pm2
RUN npm install pm2 -g
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app ./
RUN npm prune --omit=dev
EXPOSE 7000
CMD ["/usr/bin/node", "--insecure-http-parser", "/app/index.js" ]
ENTRYPOINT [ "pm2-runtime", "start", "ecosystem.config.cjs"]

View File

@@ -0,0 +1,14 @@
module.exports = {
apps: [
{
name: "torrentio-selfhostio",
script: "npm start",
cwd: "/app",
watch: ["./dist/index.cjs"],
autorestart: true,
env: {
...process.env
},
},
],
};

68
src/node/addon/esbuild.js Normal file
View File

@@ -0,0 +1,68 @@
import { build } from "esbuild";
import { copy } from 'esbuild-plugin-copy';
import { readFileSync, rmSync } from "fs";
const { devDependencies } = JSON.parse(readFileSync("./package.json", "utf8"));
const start = Date.now();
try {
const outdir = "dist";
rmSync(outdir, { recursive: true, force: true });
build({
bundle: true,
entryPoints: [
"./src/index.js",
// "./src/**/*.css",
// "./src/**/*.hbs",
// "./src/**/*.html"
],
external: [...(devDependencies && Object.keys(devDependencies))],
keepNames: true,
loader: {
".css": "copy",
".hbs": "copy",
".html": "copy",
},
minify: true,
outbase: "./src",
outdir,
outExtension: {
".js": ".cjs",
},
platform: "node",
plugins: [
{
name: "populate-import-meta",
setup: ({ onLoad }) => {
onLoad({ filter: new RegExp(`${import.meta.dirname}/src/.*.(js|ts)$`) }, args => {
const contents = readFileSync(args.path, "utf8");
const transformedContents = contents
.replace(/import\.meta/g, `{dirname:__dirname,filename:__filename}`)
.replace(/import\.meta\.filename/g, "__filename")
.replace(/import\.meta\.dirname/g, "__dirname");
return { contents: transformedContents, loader: "default" };
});
},
},
copy({
assets: [
{
from: ['./static/**'],
to: ['./static'],
},
],
})
],
}).then(() => {
// biome-ignore lint/style/useTemplate: <explanation>
console.log("⚡ " + "\x1b[32m" + `Done in ${Date.now() - start}ms`);
});
} catch (e) {
console.log(e);
process.exit(1);
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"baseUrl": "./src",
"checkJs": true,
"isolatedModules": true,
"lib": ["es6"],
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"pretty": true,
"removeComments": true,
"resolveJsonModule": true,
"rootDir": "./src",
"skipLibCheck": true,
"sourceMap": true,
"target": "ES6",
"types": ["node"],
"typeRoots": ["node_modules/@types", "src/@types"]
},
"exclude": ["node_modules"]
}

View File

@@ -1,32 +0,0 @@
import { DebridOptions } from '../moch/options.js';
import { QualityFilter, Providers, SizeFilter } from './filter.js';
import { LanguageOptions } from './languages.js';
const keysToSplit = [Providers.key, LanguageOptions.key, QualityFilter.key, SizeFilter.key, DebridOptions.key];
const keysToUppercase = [SizeFilter.key];
export function parseConfiguration(configuration) {
if (!configuration) {
return undefined;
}
const configValues = configuration.split('|')
.reduce((map, next) => {
const parameterParts = next.split('=');
if (parameterParts.length === 2) {
map[parameterParts[0].toLowerCase()] = parameterParts[1];
}
return map;
}, {});
keysToSplit
.filter(key => configValues[key])
.forEach(key => configValues[key] = configValues[key].split(',')
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
return configValues;
}
function configValue(config) {
return Object.entries(config)
.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
.join('|');
}

View File

@@ -1,49 +0,0 @@
import fs from 'fs';
import path from 'path';
import requestIp from 'request-ip';
import ip from 'ip';
const filePath = path.join(process.cwd(), 'allowed_ips.json');
let ALLOWED_ADDRESSES = [];
let ALLOWED_SUBNETS = [];
if (fs.existsSync(filePath)) {
const allowedAddresses = JSON.parse(fs.readFileSync(filePath, 'utf8'));
allowedAddresses.forEach(address => {
if (address.indexOf('/') === -1) {
ALLOWED_ADDRESSES.push(address);
} else {
ALLOWED_SUBNETS.push(address);
}
});
}
const IpIsAllowed = function(ipAddress) {
if (ALLOWED_ADDRESSES.indexOf(ipAddress) > -1) {
return true;
}
for (let i = 0; i < ALLOWED_SUBNETS.length; i++) {
if (ip.cidrSubnet(ALLOWED_SUBNETS[i]).contains(ipAddress)) {
return true;
}
}
return false;
};
export const ipFilter = function (req, res, next) {
const ipAddress = requestIp.getClientIp(req);
if (ALLOWED_ADDRESSES.length === 0 && ALLOWED_SUBNETS.length === 0) {
return next();
}
if (IpIsAllowed(ipAddress)) {
return next();
} else {
console.log(`IP ${ipAddress} is not allowed`);
res.status(404).send(null);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,14 @@
{
"name": "selfhostio-selfhostio",
"version": "1.0.0",
"name": "selfhostio-addon",
"version": "0.0.1",
"exports": "./index.js",
"type": "module",
"scripts": {
"start": "node index.js"
"build": "node esbuild.js",
"dev": "tsx watch --ignore node_modules src/index.js",
"start": "node dist/index.cjs",
"lint": "eslint . --ext .ts,.js"
},
"license": "MIT",
"dependencies": {
"@putdotio/api-client": "^8.42.0",
"all-debrid-api": "^1.1.0",
@@ -35,5 +37,15 @@
"swagger-stats": "^0.99.7",
"ua-parser-js": "^1.0.36",
"user-agents": "^1.0.1444"
},
"devDependencies": {
"@types/node": "^20.11.6",
"@types/stremio-addon-sdk": "^1.6.10",
"esbuild": "^0.19.12",
"esbuild-plugin-copy": "^2.1.1",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import-helpers": "^1.3.1",
"tsx": "^4.7.0"
}
}

View File

@@ -1,11 +1,11 @@
import Bottleneck from 'bottleneck';
import { addonBuilder } from 'stremio-addon-sdk';
import { Type } from './lib/types.js';
import { dummyManifest } from './lib/manifest.js';
import { cacheWrapStream } from './lib/cache.js';
import { toStreamInfo, applyStaticInfo } from './lib/streamInfo.js';
import { dummyManifest } from './lib/manifest.js';
import * as repository from './lib/repository.js';
import applySorting from './lib/sort.js';
import { toStreamInfo, applyStaticInfo } from './lib/streamInfo.js';
import { Type } from './lib/types.js';
import { applyMochs, getMochCatalog, getMochItemMeta } from './moch/moch.js';
import StaticLinks from './moch/static.js';
@@ -79,27 +79,24 @@ async function streamHandler(args) {
}
async function seriesRecordsHandler(args) {
if (args.id.match(/^tt\d+:\d+:\d+$/)) {
const parts = args.id.split(':');
const imdbId = parts[0];
const season = parts[1] !== undefined ? parseInt(parts[1], 10) : 1;
const episode = parts[2] !== undefined ? parseInt(parts[2], 10) : 1;
return repository.getImdbIdSeriesEntries(imdbId, season, episode);
} else if (args.id.match(/^kitsu:\d+(?::\d+)?$/i)) {
const parts = args.id.split(':');
const kitsuId = parts[1];
const episode = parts[2] !== undefined ? parseInt(parts[2], 10) : undefined;
return episode !== undefined
? repository.getKitsuIdSeriesEntries(kitsuId, episode)
: repository.getKitsuIdMovieEntries(kitsuId);
}
return Promise.resolve([]);
if (args.id.match(/^tt\d+:\d+:\d+$/)) {
const [imdbId, season = "1", episode = "1"] = args.id.split(':');
const parsedSeason = parseInt(season, 10);
const parsedEpisode = parseInt(episode, 10);
return repository.getImdbIdSeriesEntries(imdbId, parsedSeason, parsedEpisode);
} else if (args.id.match(/^kitsu:\d+(?::\d+)?$/i)) {
const [, kitsuId, episodePart] = args.id.split(':');
const episode = episodePart !== undefined ? parseInt(episodePart, 10) : undefined;
return episode !== undefined
? repository.getKitsuIdSeriesEntries(kitsuId, episode)
: repository.getKitsuIdMovieEntries(kitsuId);
}
return Promise.resolve([]);
}
async function movieRecordsHandler(args) {
if (args.id.match(/^tt\d+$/)) {
const parts = args.id.split(':');
const imdbId = parts[0];
const [imdbId] = args.id.split(':');
console.log("imdbId", imdbId);
return repository.getImdbIdMovieEntries(imdbId);
} else if (args.id.match(/^kitsu:\d+(?::\d+)?$/i)) {

View File

@@ -1,11 +1,10 @@
import express from 'express';
import serverless from './serverless.js';
import { initBestTrackers } from './lib/magnetHelper.js';
import {ipFilter} from "./lib/ipFilter.js";
import serverless from './serverless.js';
const app = express();
app.enable('trust proxy');
app.use(ipFilter);
app.use(express.static('static', { maxAge: '1y' }));
app.use((req, res, next) => serverless(req, res, next));
app.listen(process.env.PORT || 7000, () => {

View File

@@ -0,0 +1,28 @@
import { DebridOptions } from '../moch/options.js';
import { QualityFilter, SizeFilter } from './filter.js';
import { LanguageOptions } from './languages.js';
const keysToSplit = [LanguageOptions.key, QualityFilter.key, SizeFilter.key, DebridOptions.key];
const keysToUppercase = [SizeFilter.key];
export function parseConfiguration(configuration) {
if (!configuration) {
return undefined;
}
const configValues = configuration.split('|')
.reduce((map, next) => {
const [key, value] = next.split('=');
if (key && value) {
map[key.toLowerCase()] = value;
}
return map;
}, {});
keysToSplit
.filter(key => configValues[key])
.forEach(key => configValues[key] = configValues[key].split(',')
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
return configValues;
}

View File

@@ -0,0 +1,72 @@
const VIDEO_EXTENSIONS = [
"3g2",
"3gp",
"avi",
"flv",
"mkv",
"mk3d",
"mov",
"mp2",
"mp4",
"m4v",
"mpe",
"mpeg",
"mpg",
"mpv",
"webm",
"wmv",
"ogm",
"ts",
"m2ts"
];
const SUBTITLE_EXTENSIONS = [
"aqt",
"gsub",
"jss",
"sub",
"ttxt",
"pjs",
"psb",
"rt",
"smi",
"slt",
"ssf",
"srt",
"ssa",
"ass",
"usf",
"idx",
"vtt"
];
const DISK_EXTENSIONS = [
"iso",
"m2ts",
"ts",
"vob"
]
const ARCHIVE_EXTENSIONS = [
"rar",
"zip"
]
export function isVideo(filename) {
return isExtension(filename, VIDEO_EXTENSIONS);
}
export function isSubtitle(filename) {
return isExtension(filename, SUBTITLE_EXTENSIONS);
}
export function isDisk(filename) {
return isExtension(filename, DISK_EXTENSIONS);
}
export function isArchive(filename) {
return isExtension(filename, ARCHIVE_EXTENSIONS);
}
export function isExtension(filename, extensions) {
const extensionMatch = filename?.match(/\.(\w{2,4})$/);
return extensionMatch && extensions.includes(extensionMatch[1].toLowerCase());
}

View File

@@ -1,30 +1,5 @@
import { extractProvider, parseSize, extractSize } from './titleHelper.js';
import { parseSize, extractSize } from './titleHelper.js';
import { Type } from './types.js';
export const Providers = {
key: 'providers',
options: [
{
key: 'YTS',
label: 'YTS'
},
{
key: 'EZTV',
label: 'EZTV'
},
{
key: 'DMM',
label: 'DMM'
},
{
key: 'TPB',
label: 'TPB'
},
{
key: 'TorrentGalaxy',
label: 'TorrentGalaxy'
}
]
};
export const QualityFilter = {
key: 'qualityfilter',
options: [
@@ -121,27 +96,14 @@ export const QualityFilter = {
export const SizeFilter = {
key: 'sizefilter'
}
const defaultProviderKeys = Providers.options.map(provider => provider.key);
export default function applyFilters(streams, config) {
return [
filterByProvider,
filterByQuality,
filterBySize
].reduce((filteredStreams, filter) => filter(filteredStreams, config), streams);
}
function filterByProvider(streams, config) {
const providers = config.providers || defaultProviderKeys;
if (!providers?.length) {
return streams;
}
return streams.filter(stream => {
const provider = extractProvider(stream.title)
return providers.includes(provider);
})
}
function filterByQuality(streams, config) {
const filters = config[QualityFilter.key];
if (!filters) {
@@ -149,7 +111,7 @@ function filterByQuality(streams, config) {
}
const filterOptions = QualityFilter.options.filter(option => filters.includes(option.key));
return streams.filter(stream => {
const streamQuality = stream.name.split('\n')[1];
const [ , streamQuality] = stream.name.split('\n');
const bingeGroup = stream.behaviorHints?.bingeGroup;
return !filterOptions.some(option => option.test(streamQuality, bingeGroup));
});

View File

@@ -184,14 +184,13 @@ a.install-link {
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
}
`;
import { Providers, QualityFilter, SizeFilter } from './filter.js';
import { SortOptions } from './sort.js';
import { LanguageOptions } from './languages.js';
import { DebridOptions } from '../moch/options.js';
import { MochOptions } from '../moch/moch.js';
import { DebridOptions } from '../moch/options.js';
import { QualityFilter, SizeFilter } from './filter.js';
import { LanguageOptions } from './languages.js';
import { SortOptions } from './sort.js';
export default function landingTemplate(manifest, config = {}) {
const providers = config[Providers.key] || Providers.options.map(provider => provider.key);
const sort = config[SortOptions.key] || SortOptions.options.qualitySeeders.key;
const languages = config[LanguageOptions.key] || [];
const qualityFilters = config[QualityFilter.key] || [];
@@ -211,9 +210,7 @@ export default function landingTemplate(manifest, config = {}) {
const background = manifest.background || 'https://dl.strem.io/addon-background.jpg';
const logo = manifest.logo || 'https://dl.strem.io/addon-logo.png';
const providersHTML = Providers.options
.map(provider => `<option value="${provider.key}">${provider.foreign ? provider.foreign + ' ' : ''}${provider.label}</option>`)
.join('\n');
const sortOptionsHTML = Object.values(SortOptions.options)
.map((option, i) => `<option value="${option.key}" ${i === 0 ? 'selected' : ''}>${option.description}</option>`)
.join('\n');
@@ -268,11 +265,6 @@ export default function landingTemplate(manifest, config = {}) {
<div class="separator"></div>
<label class="label" for="iProviders">Providers:</label>
<select id="iProviders" class="input" onchange="generateInstallLink()" name="providers[]" multiple="multiple">
${providersHTML}
</select>
<label class="label" for="iSort">Sorting:</label>
<select id="iSort" class="input" onchange="sortModeChange()">
${sortOptionsHTML}
@@ -356,12 +348,6 @@ export default function landingTemplate(manifest, config = {}) {
const isTvAgent = /\\b(?:tv|wv)\\b/i.test(navigator.userAgent)
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
$('#iProviders').multiselect({
nonSelectedText: 'All providers',
buttonTextAlignment: 'left',
onChange: () => generateInstallLink()
});
$('#iProviders').multiselect('select', [${providers.map(provider => '"' + provider + '"')}]);
$('#iLanguages').multiselect({
nonSelectedText: 'None',
buttonTextAlignment: 'left',
@@ -381,7 +367,6 @@ export default function landingTemplate(manifest, config = {}) {
});
$('#iDebridOptions').multiselect('select', [${debridOptions.map(option => '"' + option + '"')}]);
} else {
$('#iProviders').val([${providers.map(provider => '"' + provider + '"')}]);
$('#iLanguages').val([${languages.map(language => '"' + language + '"')}]);
$('#iQualityFilter').val([${qualityFilters.map(filter => '"' + filter + '"')}]);
$('#iDebridOptions').val([${debridOptions.map(option => '"' + option + '"')}]);
@@ -422,8 +407,6 @@ export default function landingTemplate(manifest, config = {}) {
}
function generateInstallLink() {
const providersList = $('#iProviders').val() || [];
const providersValue = providersList.join(',');
const qualityFilterValue = $('#iQualityFilter').val().join(',') || '';
const sortValue = $('#iSort').val() || '';
const languagesValue = $('#iLanguages').val().join(',') || [];
@@ -439,8 +422,6 @@ export default function landingTemplate(manifest, config = {}) {
const putioClientIdValue = $('#iPutioClientId').val() || '';
const putioTokenValue = $('#iPutioToken').val() || '';
const providers = providersList.length && providersList.length < ${Providers.options.length} && providersValue;
const qualityFilters = qualityFilterValue.length && qualityFilterValue;
const sort = sortValue !== '${SortOptions.options.qualitySeeders.key}' && sortValue;
const languages = languagesValue.length && languagesValue;
@@ -456,7 +437,6 @@ export default function landingTemplate(manifest, config = {}) {
const putio = putioClientIdValue.length && putioTokenValue.length && putioClientIdValue.trim() + '@' + putioTokenValue.trim();
let configurationValue = [
['${Providers.key}', providers],
['${SortOptions.key}', sort],
['${LanguageOptions.key}', languages],
['${QualityFilter.key}', qualityFilters],

View File

@@ -1,10 +1,8 @@
import axios from 'axios';
import magnet from 'magnet-uri';
import { getRandomUserAgent } from './requestHelper.js';
import { getTorrent } from './repository.js';
import { getRandomUserAgent } from './requestHelper.js';
import { Type } from './types.js';
import { extractProvider } from "./titleHelper.js";
import { Providers } from "./filter.js";
const TRACKERS_URL = 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt';
const DEFAULT_TRACKERS = [
@@ -44,17 +42,7 @@ const RUSSIAN_TRACKERS = [
"http://bt3.t-ru.org/ann?magnet",
"http://bt4.t-ru.org/ann?magnet",
];
// Some trackers have limits on original torrent trackers,
// where downloading ip has to seed the torrents for some amount of time,
// thus it doesn't work on mochs.
// So it's better to exclude them and try to download through DHT,
// as the torrent won't start anyway.
const RUSSIAN_PROVIDERS = Providers.options
.filter(provider => provider.foreign === '🇷🇺')
.map(provider => provider.label);
const ANIME_PROVIDERS = Providers.options
.filter(provider => provider.anime)
.map(provider => provider.label);
let BEST_TRACKERS = [];
let ALL_ANIME_TRACKERS = [];
let ALL_RUSSIAN_TRACKERS = [];
@@ -63,8 +51,7 @@ export async function getMagnetLink(infoHash) {
const torrent = await getTorrent(infoHash).catch(() => ({ infoHash }));
const torrentTrackers = torrent?.trackers?.split(',') || [];
const animeTrackers = torrent.type === Type.ANIME ? ALL_ANIME_TRACKERS : [];
const providerTrackers = RUSSIAN_PROVIDERS.includes(torrent.provider) && ALL_RUSSIAN_TRACKERS || [];
const trackers = unique([].concat(torrentTrackers).concat(animeTrackers).concat(providerTrackers));
const trackers = unique([].concat(torrentTrackers).concat(animeTrackers));
return magnet.encode({ infoHash: infoHash, name: torrent.title, announce: trackers });
}
@@ -96,19 +83,6 @@ export function getSources(trackersInput, infoHash) {
return trackers.map(tracker => `tracker:${tracker}`).concat(`dht:${infoHash}`);
}
export function enrichStreamSources(stream) {
const provider = extractProvider(stream.title);
if (ANIME_PROVIDERS.includes(provider)) {
const sources = getSources(ALL_ANIME_TRACKERS, stream.infoHash);
return { ...stream, sources };
}
if (RUSSIAN_PROVIDERS.includes(provider)) {
const sources = unique([].concat(stream.sources || []).concat(getSources(ALL_RUSSIAN_TRACKERS, stream.infoHash)));
return { ...stream, sources };
}
return stream;
}
function unique(array) {
return Array.from(new Set(array));
}

View File

@@ -0,0 +1,20 @@
/**
* Delay promise
*/
export async function delay(duration) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
/**
* Timeout promise after a set time in ms
*/
export async function timeout(timeoutMs, promise, message = 'Timed out') {
return Promise.race([
promise,
new Promise(function (resolve, reject) {
setTimeout(function () {
reject(message);
}, timeoutMs);
})
]);
}

View File

@@ -1,5 +1,6 @@
import { Sequelize } from 'sequelize';
const Op = Sequelize.Op;
const { Op } = Sequelize;
const DATABASE_URI = process.env.DATABASE_URI || 'postgres://postgres:postgres@localhost:5432/postgres';

View File

@@ -0,0 +1,6 @@
import UserAgent from 'user-agents';
const userAgent = new UserAgent();
export function getRandomUserAgent() {
return userAgent.random().toString();
}

View File

@@ -1,8 +1,8 @@
import titleParser from 'parse-torrent-title';
import { Type } from './types.js';
import { mapLanguages } from './languages.js';
import { enrichStreamSources, getSources } from './magnetHelper.js';
import { getSources } from './magnetHelper.js';
import { getSubtitles } from './subtitles.js';
import { Type } from './types.js';
const ADDON_NAME = 'selfhostio';
const SIZE_DELTA = 0.02;
@@ -108,7 +108,7 @@ export function applyStaticInfo(streams) {
}
function enrichStaticInfo(stream) {
return enrichSubtitles(enrichStreamSources({ ...stream }));
return enrichSubtitles(stream);
}
function enrichSubtitles(stream) {

View File

@@ -1,7 +1,5 @@
import { parse } from 'parse-torrent-title';
import { isExtension } from './extension.js';
import { Providers } from './filter.js';
import { languageFromCode } from './languages.js';
const languageMapping = {
'english': 'eng',
@@ -68,17 +66,12 @@ export function getSubtitles(record) {
function parseLanguage(title, record) {
const subtitlePathParts = title.split('/');
const subtitleFileName = subtitlePathParts.pop();
const subtitleTitleNoExt = title.replace(/\.\w{2,5}$/, '');
const videoFileName = record.title.split('/').pop().replace(/\.\w{2,5}$/, '');
const fileNameLanguage = getSingleLanguage(subtitleFileName.replace(videoFileName, ''));
if (fileNameLanguage) {
return fileNameLanguage;
}
const videoTitleNoExt = record.title.replace(/\.\w{2,5}$/, '');
if (subtitleTitleNoExt === record.title || subtitleTitleNoExt === videoTitleNoExt) {
const provider = Providers.options.find(provider => provider.label === record.torrent.provider);
return provider?.foreign && languageFromCode(provider.foreign) || 'eng';
}
const folderName = subtitlePathParts.join('/');
const folderNameLanguage = getSingleLanguage(folderName.replace(videoFileName, ''));
if (folderNameLanguage) {

View File

@@ -1,8 +1,3 @@
export function extractSeeders(title) {
const seedersMatch = title.match(/👤 (\d+)/);
return seedersMatch && parseInt(seedersMatch[1]) || 0;
}
export function extractSize(title) {
const seedersMatch = title.match(/💾 ([\d.]+ \w+)/);
return seedersMatch && parseSize(seedersMatch[1]) || 0;

View File

@@ -0,0 +1,6 @@
export const Type = {
MOVIE: 'movie',
SERIES: 'series',
ANIME: 'anime',
OTHER: 'other'
};

View File

@@ -1,9 +1,9 @@
import AllDebridClient from 'all-debrid-api';
import { Type } from '../lib/types.js';
import { isVideo, isArchive } from '../lib/extension.js';
import StaticResponse from './static.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { Type } from '../lib/types.js';
import { BadTokenError, AccessDeniedError, sameFilename } from './mochHelper.js';
import StaticResponse from './static.js';
const KEY = 'alldebrid';
const AGENT = 'selfhostio';

View File

@@ -1,9 +1,9 @@
import DebridLinkClient from 'debrid-link-api';
import { Type } from '../lib/types.js';
import { isVideo, isArchive } from '../lib/extension.js';
import StaticResponse from './static.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { Type } from '../lib/types.js';
import { chunkArray, BadTokenError } from './mochHelper.js';
import StaticResponse from './static.js';
const KEY = 'debridlink';

View File

@@ -1,15 +1,15 @@
import namedQueue from 'named-queue';
import * as options from './options.js';
import * as realdebrid from './realdebrid.js';
import * as premiumize from './premiumize.js';
import * as alldebrid from './alldebrid.js';
import * as debridlink from './debridlink.js';
import * as offcloud from './offcloud.js';
import * as putio from './putio.js';
import StaticResponse, { isStaticUrl } from './static.js';
import { cacheWrapResolvedUrl } from '../lib/cache.js';
import { timeout } from '../lib/promises.js';
import * as alldebrid from './alldebrid.js';
import * as debridlink from './debridlink.js';
import { BadTokenError, streamFilename, AccessDeniedError, enrichMeta } from './mochHelper.js';
import * as offcloud from './offcloud.js';
import * as options from './options.js';
import * as premiumize from './premiumize.js';
import * as putio from './putio.js';
import * as realdebrid from './realdebrid.js';
import StaticResponse, { isStaticUrl } from './static.js';
const RESOLVE_TIMEOUT = 2 * 60 * 1000; // 2 minutes
const MIN_API_KEY_SYMBOLS = 15;

View File

@@ -1,10 +1,10 @@
import OffcloudClient from 'offcloud-api';
import magnet from 'magnet-uri';
import { Type } from '../lib/types.js';
import OffcloudClient from 'offcloud-api';
import { isVideo } from '../lib/extension.js';
import StaticResponse from './static.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { Type } from '../lib/types.js';
import { chunkArray, BadTokenError, sameFilename } from './mochHelper.js';
import StaticResponse from './static.js';
const KEY = 'offcloud';

View File

@@ -0,0 +1,29 @@
export const DebridOptions = {
key: 'debridoptions',
options: {
noDownloadLinks: {
key: 'nodownloadlinks',
description: 'Don\'t show download to debrid links'
},
noCatalog: {
key: 'nocatalog',
description: 'Don\'t show debrid catalog'
},
torrentLinks: {
key: 'torrentlinks',
description: 'Show P2P torrent links for uncached'
}
}
}
export function excludeDownloadLinks(config) {
return config[DebridOptions.key]?.includes(DebridOptions.options.noDownloadLinks.key);
}
export function includeTorrentLinks(config) {
return config[DebridOptions.key]?.includes(DebridOptions.options.torrentLinks.key);
}
export function showDebridCatalog(config) {
return !config[DebridOptions.key]?.includes(DebridOptions.options.noCatalog.key);
}

View File

@@ -1,10 +1,10 @@
import PremiumizeClient from 'premiumize-api';
import magnet from 'magnet-uri';
import { Type } from '../lib/types.js';
import PremiumizeClient from 'premiumize-api';
import { isVideo, isArchive } from '../lib/extension.js';
import StaticResponse from './static.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { Type } from '../lib/types.js';
import { BadTokenError, chunkArray, sameFilename } from './mochHelper.js';
import StaticResponse from './static.js';
const KEY = 'premiumize';

View File

@@ -1,11 +1,11 @@
import PutioClient from '@putdotio/api-client'
import { isVideo } from '../lib/extension.js';
import { delay } from '../lib/promises.js';
import StaticResponse from './static.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { Type } from "../lib/types.js";
import { decode } from "magnet-uri";
import { isVideo } from '../lib/extension.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { delay } from '../lib/promises.js';
import { Type } from "../lib/types.js";
import { sameFilename } from "./mochHelper.js";
import StaticResponse from './static.js';
const PutioAPI = PutioClient.default;
const KEY = 'putio';
@@ -198,10 +198,6 @@ function createPutioAPI(apiKey) {
return Putio;
}
export function toCommonError(error) {
return undefined;
}
function statusError(status) {
return ['ERROR'].includes(status);
}

View File

@@ -1,11 +1,11 @@
import RealDebridClient from 'real-debrid-api';
import { Type } from '../lib/types.js';
import { isVideo, isArchive } from '../lib/extension.js';
import { delay } from '../lib/promises.js';
import { cacheAvailabilityResults, getCachedAvailabilityResults } from '../lib/cache.js';
import StaticResponse from './static.js';
import { isVideo, isArchive } from '../lib/extension.js';
import { getMagnetLink } from '../lib/magnetHelper.js';
import { delay } from '../lib/promises.js';
import { Type } from '../lib/types.js';
import { chunkArray, BadTokenError, AccessDeniedError } from './mochHelper.js';
import StaticResponse from './static.js';
const MIN_SIZE = 5 * 1024 * 1024; // 5 MB
const CATALOG_MAX_PAGE = 1;

View File

@@ -0,0 +1,16 @@
const staticVideoUrls = {
DOWNLOADING: `videos/downloading_v2.mp4`,
FAILED_DOWNLOAD: `videos/download_failed_v2.mp4`,
FAILED_ACCESS: `videos/failed_access_v2.mp4`,
FAILED_RAR: `videos/failed_rar_v2.mp4`,
FAILED_OPENING: `videos/failed_opening_v2.mp4`,
FAILED_UNEXPECTED: `videos/failed_unexpected_v2.mp4`,
FAILED_INFRINGEMENT: `videos/failed_infringement_v2.mp4`
}
export function isStaticUrl(url) {
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
}
export default staticVideoUrls

View File

@@ -1,13 +1,13 @@
import Router from 'router';
import cors from 'cors';
import rateLimit from "express-rate-limit";
import qs from 'querystring';
import requestIp from 'request-ip';
import Router from 'router';
import userAgentParser from 'ua-parser-js';
import addonInterface from './addon.js';
import qs from 'querystring';
import { manifest } from './lib/manifest.js';
import { parseConfiguration } from './lib/configuration.js';
import landingTemplate from './lib/landingTemplate.js';
import { manifest } from './lib/manifest.js';
import * as moch from './moch/moch.js';
const router = new Router();
@@ -104,4 +104,4 @@ export default function (req, res) {
res.statusCode = 404;
res.end();
});
};
}