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:
1
src/node/addon/.eslintignore
Normal file
1
src/node/addon/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
*.ts
|
||||
39
src/node/addon/.eslintrc.cjs
Normal file
39
src/node/addon/.eslintrc.cjs
Normal 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",
|
||||
},
|
||||
};
|
||||
@@ -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"]
|
||||
14
src/node/addon/ecosystem.config.cjs
Normal file
14
src/node/addon/ecosystem.config.cjs
Normal 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
68
src/node/addon/esbuild.js
Normal 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);
|
||||
}
|
||||
21
src/node/addon/jsconfig.json
Normal file
21
src/node/addon/jsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -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('|');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
3920
src/node/addon/package-lock.json
generated
3920
src/node/addon/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
@@ -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, () => {
|
||||
28
src/node/addon/src/lib/configuration.js
Normal file
28
src/node/addon/src/lib/configuration.js
Normal 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;
|
||||
}
|
||||
72
src/node/addon/src/lib/extension.js
Normal file
72
src/node/addon/src/lib/extension.js
Normal 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());
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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],
|
||||
@@ -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));
|
||||
}
|
||||
20
src/node/addon/src/lib/promises.js
Normal file
20
src/node/addon/src/lib/promises.js
Normal 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);
|
||||
})
|
||||
]);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
6
src/node/addon/src/lib/requestHelper.js
Normal file
6
src/node/addon/src/lib/requestHelper.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import UserAgent from 'user-agents';
|
||||
const userAgent = new UserAgent();
|
||||
|
||||
export function getRandomUserAgent() {
|
||||
return userAgent.random().toString();
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
6
src/node/addon/src/lib/types.js
Normal file
6
src/node/addon/src/lib/types.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const Type = {
|
||||
MOVIE: 'movie',
|
||||
SERIES: 'series',
|
||||
ANIME: 'anime',
|
||||
OTHER: 'other'
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
29
src/node/addon/src/moch/options.js
Normal file
29
src/node/addon/src/moch/options.js
Normal 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);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
16
src/node/addon/src/moch/static.js
Normal file
16
src/node/addon/src/moch/static.js
Normal 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
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user