mirror of
https://github.com/TheBeastLT/torrentio-scraper.git
synced 2026-01-11 22:40:22 +00:00
add stremthru integration
This commit is contained in:
parent
703c655620
commit
4bd70f5553
6 changed files with 420 additions and 1 deletions
|
|
@ -28,6 +28,22 @@ export const PreConfigurations = {
|
|||
|
||||
const keysToSplit = [Providers.key, LanguageOptions.key, QualityFilter.key, SizeFilter.key, DebridOptions.key];
|
||||
const keysToUppercase = [SizeFilter.key];
|
||||
const keysParser = {
|
||||
stremthru: (val) => {
|
||||
const config = { url: '' , auth: '' };
|
||||
if (val) {
|
||||
const [auth, url] = decodeURIComponent(val).split('@');
|
||||
config.url = url;
|
||||
if (auth.includes(':')) {
|
||||
const [store, token] = auth.split(':');
|
||||
config.auth = { store, token };
|
||||
} else {
|
||||
config.auth = auth;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseConfiguration(configuration) {
|
||||
if (!configuration) {
|
||||
|
|
@ -48,6 +64,11 @@ export function parseConfiguration(configuration) {
|
|||
.filter(key => configValues[key])
|
||||
.forEach(key => configValues[key] = configValues[key].split(',')
|
||||
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
||||
Object.keys(keysParser).forEach((key) => {
|
||||
if (configValues[key]) {
|
||||
configValues[key] = keysParser[key](configValues[key]);
|
||||
}
|
||||
})
|
||||
return configValues;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
const putioKey = config[MochOptions.putio.key] || '';
|
||||
const putioClientId = putioKey.replace(/@.*/, '');
|
||||
const putioToken = putioKey.replace(/.*@/, '');
|
||||
const stremthru = config[MochOptions.stremthru.key] || { url: '', auth: '' };
|
||||
|
||||
const background = manifest.background || 'https://dl.strem.io/addon-background.jpg';
|
||||
const logo = manifest.logo || 'https://dl.strem.io/addon-logo.png';
|
||||
|
|
@ -341,6 +342,12 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
<input type="text" id="iPutioClientId" placeholder="ClientId" onchange="generateInstallLink()" class="input">
|
||||
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
<div id="dStremThru">
|
||||
<label class="label" for="iStremThru">StremThru URL and Credential:</label>
|
||||
<input type="text" id="iStremThruUrl" placeholder="URL: https://" onchange="generateInstallLink()" class="input">
|
||||
<input type="text" id="iStremThruAuth" placeholder="Credential: 'store_name:store_token' / base64 encoded 'username:password'" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
<div id="dDebridOptions">
|
||||
<label class="label" for="iDebridOptions">Debrid options:</label>
|
||||
|
|
@ -405,6 +412,8 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
$('#iTorbox').val("${torboxApiKey}");
|
||||
$('#iPutioClientId').val("${putioClientId}");
|
||||
$('#iPutioToken').val("${putioToken}");
|
||||
$('#iStremThruUrl').val("${stremthru.url}");
|
||||
$('#iStremThruAuth').val("${stremthru.auth}");
|
||||
$('#iSort').val("${sort}");
|
||||
$('#iLimit').val("${limit}");
|
||||
$('#iSizeFilter').val("${sizeFilter}");
|
||||
|
|
@ -431,6 +440,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
||||
$('#dTorbox').toggle(provider === '${MochOptions.torbox.key}');
|
||||
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
||||
$('#dStremThru').toggle(provider === '${MochOptions.stremthru.key}');
|
||||
}
|
||||
|
||||
function generateInstallLink() {
|
||||
|
|
@ -451,6 +461,8 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
const torboxValue = $('#iTorbox').val() || '';
|
||||
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
||||
const putioTokenValue = $('#iPutioToken').val() || '';
|
||||
const stremthruUrl = $('#iStremThruUrl').val() || '';
|
||||
const stremthruAuth = $('#iStremThruAuth').val() || '';
|
||||
|
||||
|
||||
const providers = providersList.length && providersList.length < ${Providers.options.length} && providersValue;
|
||||
|
|
@ -468,6 +480,9 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
const offcloud = offcloudValue.length && offcloudValue.trim();
|
||||
const torbox = torboxValue.length && torboxValue.trim();
|
||||
const putio = putioClientIdValue.length && putioTokenValue.length && putioClientIdValue.trim() + '@' + putioTokenValue.trim();
|
||||
let stremthru = stremthruUrl.trim().length
|
||||
? encodeURIComponent(stremthruAuth.trim() + '@' + stremthruUrl.trim())
|
||||
: '';
|
||||
|
||||
const preConfigurations = {
|
||||
${preConfigurationObject}
|
||||
|
|
@ -486,7 +501,8 @@ export default function landingTemplate(manifest, config = {}) {
|
|||
['${MochOptions.debridlink.key}', debridLink],
|
||||
['${MochOptions.offcloud.key}', offcloud],
|
||||
['${MochOptions.torbox.key}', torbox],
|
||||
['${MochOptions.putio.key}', putio]
|
||||
['${MochOptions.putio.key}', putio],
|
||||
['${MochOptions.stremthru.key}', stremthru]
|
||||
].filter(([_, value]) => value.length).map(([key, value]) => key + '=' + value).join('|');
|
||||
configurationValue = Object.entries(preConfigurations)
|
||||
.filter(([key, value]) => value === configurationValue)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import * as debridlink from './debridlink.js';
|
|||
import * as offcloud from './offcloud.js';
|
||||
import * as torbox from './torbox.js';
|
||||
import * as putio from './putio.js';
|
||||
import * as stremthru from './stremthru.js';
|
||||
import StaticResponse, { isStaticUrl } from './static.js';
|
||||
import { cacheWrapResolvedUrl } from '../lib/cache.js';
|
||||
import { timeout } from '../lib/promises.js';
|
||||
|
|
@ -64,6 +65,13 @@ export const MochOptions = {
|
|||
name: 'Put.io',
|
||||
shortName: 'Putio',
|
||||
catalogs: ['']
|
||||
},
|
||||
stremthru: {
|
||||
key: 'stremthru',
|
||||
instance: stremthru,
|
||||
name: 'StremThru',
|
||||
shortName: 'ST',
|
||||
catalogs: ['']
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
338
addon/moch/stremthru.js
Normal file
338
addon/moch/stremthru.js
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
import { StremThru, StremThruError } from "stremthru";
|
||||
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 {
|
||||
AccessDeniedError,
|
||||
BadTokenError,
|
||||
streamFilename,
|
||||
} from "./mochHelper.js";
|
||||
|
||||
const KEY = "stremthru";
|
||||
const AGENT = "torrentio";
|
||||
|
||||
/**
|
||||
* @typedef {{ url: string, auth: string | { store: string, token: string }}} Conf
|
||||
* @typedef {`${string}@${string}`} EncodedConf
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Conf} conf
|
||||
* @returns {EncodedConf}
|
||||
*/
|
||||
function encodeConf(conf) {
|
||||
let auth = conf.auth;
|
||||
if (typeof auth === "object") {
|
||||
auth = `${auth.store}:${auth.token}`;
|
||||
}
|
||||
return Buffer.from(`${auth}@${conf.url}`).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EncodedConf} encoded
|
||||
* @return {Conf}
|
||||
*/
|
||||
function decodeConf(encoded) {
|
||||
const decoded = Buffer.from(encoded, "base64").toString("ascii");
|
||||
const parts = decoded.split("@");
|
||||
let auth = parts[0];
|
||||
if (auth.includes(":")) {
|
||||
const [store, token] = auth.split(":");
|
||||
auth = { store, token };
|
||||
}
|
||||
return { url: parts.slice(1).join("@"), auth };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Conf} conf
|
||||
* @param {string} ip
|
||||
*/
|
||||
export async function getCachedStreams(streams, conf, ip) {
|
||||
const options = getDefaultOptions(ip);
|
||||
const ST = new StremThru({ ...options, baseUrl: conf.url, auth: conf.auth });
|
||||
const hashes = streams.map((stream) => stream.infoHash);
|
||||
const available = await ST.store
|
||||
.checkMagnet({ magnet: hashes })
|
||||
.catch((error) => {
|
||||
if (toCommonError(error)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
console.warn(
|
||||
`Failed StremThru cached [${hashes[0]}] torrent availability request:`,
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
});
|
||||
const apiKey = encodeConf(conf);
|
||||
return (
|
||||
available &&
|
||||
streams.reduce((mochStreams, stream) => {
|
||||
const cachedEntry = available.data.items.find(
|
||||
(magnet) => stream.infoHash === magnet.hash,
|
||||
);
|
||||
const fileName = streamFilename(stream);
|
||||
mochStreams[`${stream.infoHash}@${stream.fileIdx}`] = {
|
||||
url: `${apiKey}/${stream.infoHash}/${fileName}/${stream.fileIdx}`,
|
||||
cached: cachedEntry?.status === "cached",
|
||||
};
|
||||
return mochStreams;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Conf} conf
|
||||
* @param {number} [offset]
|
||||
* @param {string} ip
|
||||
*/
|
||||
export async function getCatalog(conf, offset = 0, ip) {
|
||||
if (offset > 0) {
|
||||
return [];
|
||||
}
|
||||
const options = getDefaultOptions(ip);
|
||||
const ST = new StremThru({ ...options, baseUrl: conf.url, auth: conf.auth });
|
||||
return ST.store.listMagnets().then((response) =>
|
||||
response.data.items
|
||||
.filter((torrent) => statusDownloaded(torrent.status))
|
||||
.map((torrent) => ({
|
||||
id: `${KEY}:${torrent.id}`,
|
||||
type: Type.OTHER,
|
||||
name: torrent.name,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} itemId
|
||||
* @param {Conf} conf
|
||||
* @param {string} ip
|
||||
*/
|
||||
export async function getItemMeta(itemId, conf, ip) {
|
||||
const options = getDefaultOptions(ip);
|
||||
const ST = new StremThru({ ...options, baseUrl: conf.url, auth: conf.auth });
|
||||
const apiKey = encodeConf(conf);
|
||||
return ST.store
|
||||
.getMagnet(itemId)
|
||||
.then((response) => response.data)
|
||||
.then((torrent) => ({
|
||||
id: `${KEY}:${torrent.id}`,
|
||||
type: Type.OTHER,
|
||||
name: torrent.name,
|
||||
infoHash: torrent.hash,
|
||||
videos: torrent.files
|
||||
.filter((file) => isVideo(file.name) && file.link)
|
||||
.map((file) => ({
|
||||
id: `${KEY}:${torrent.id}:${file.index}`,
|
||||
title: file.name,
|
||||
released: torrent.added_at,
|
||||
streams: [
|
||||
{
|
||||
url: `${apiKey}/${torrent.hash}/${encodeURIComponent(file.name)}/${file.index}`,
|
||||
},
|
||||
],
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function resolve({
|
||||
apiKey,
|
||||
infoHash,
|
||||
cachedEntryInfo,
|
||||
fileIndex,
|
||||
ip,
|
||||
}) {
|
||||
console.log(`Unrestricting StremThru ${infoHash} [${fileIndex}]`);
|
||||
const conf = decodeConf(apiKey);
|
||||
const fileName = decodeURIComponent(cachedEntryInfo);
|
||||
const options = getDefaultOptions(ip);
|
||||
const ST = new StremThru({ ...options, baseUrl: conf.url, auth: conf.auth });
|
||||
|
||||
return _resolve(ST, infoHash, fileName, fileIndex).catch((error) => {
|
||||
if (errorFailedAccessError(error)) {
|
||||
console.log(`Access denied to StremThru ${infoHash} [${fileIndex}]`);
|
||||
return StaticResponse.FAILED_ACCESS;
|
||||
} else if (error.code === "STORE_LIMIT_EXCEEDED") {
|
||||
console.log(
|
||||
`Deleting and retrying adding to StremThru ${infoHash} [${fileIndex}]...`,
|
||||
);
|
||||
return _deleteAndRetry(ST, infoHash, fileName, fileIndex);
|
||||
}
|
||||
return Promise.reject(
|
||||
`Failed StremThru adding torrent ${JSON.stringify(error)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function _resolve(ST, infoHash, fileName, fileIndex) {
|
||||
const torrent = await _createOrFindTorrent(ST, infoHash);
|
||||
if (torrent && statusDownloaded(torrent.status)) {
|
||||
return _unrestrictLink(ST, torrent, fileName, fileIndex);
|
||||
} else if (torrent && statusDownloading(torrent.status)) {
|
||||
console.log(`Downloading to StremThru ${infoHash} [${fileIndex}]...`);
|
||||
return StaticResponse.DOWNLOADING;
|
||||
} else if (torrent && torrent.status === "invalid") {
|
||||
console.log(
|
||||
`Failed StremThru opening torrent ${infoHash} [${fileIndex}] due to magnet error`,
|
||||
);
|
||||
return StaticResponse.FAILED_OPENING;
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
`Failed StremThru adding torrent ${JSON.stringify(torrent)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
*/
|
||||
async function _createOrFindTorrent(ST, infoHash) {
|
||||
return _findTorrent(ST, infoHash).catch(() => _createTorrent(ST, infoHash));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
* @param {string} infoHash
|
||||
* @param {string} fileName
|
||||
* @param {number} fileIndex
|
||||
*/
|
||||
async function _retryCreateTorrent(ST, infoHash, fileName, fileIndex) {
|
||||
const newTorrent = await _createTorrent(ST, infoHash);
|
||||
return newTorrent && statusDownloaded(newTorrent.status)
|
||||
? _unrestrictLink(ST, newTorrent, fileName, fileIndex)
|
||||
: StaticResponse.FAILED_DOWNLOAD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
* @param {string} infoHash
|
||||
* @param {string} fileName
|
||||
* @param {number} fileIndex
|
||||
*/
|
||||
async function _deleteAndRetry(ST, infoHash, fileName, fileIndex) {
|
||||
const torrents = await ST.store
|
||||
.listMagnets()
|
||||
.then((response) =>
|
||||
response.data.items.filter((item) => statusDownloading(item.status)),
|
||||
);
|
||||
const oldestActiveTorrent = torrents[torrents.length - 1];
|
||||
return ST.store
|
||||
.removeMagnet(oldestActiveTorrent.id)
|
||||
.then(() => _retryCreateTorrent(ST, infoHash, fileName, fileIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
* @param {string} infoHash
|
||||
*/
|
||||
async function _findTorrent(ST, infoHash) {
|
||||
const torrents = await ST.store
|
||||
.listMagnets()
|
||||
.then((response) => response.data.items);
|
||||
const foundTorrents = torrents.filter((torrent) => torrent.hash === infoHash);
|
||||
const nonFailedTorrent = foundTorrents.find(
|
||||
(torrent) => !statusError(torrent.status),
|
||||
);
|
||||
const foundTorrent = nonFailedTorrent || foundTorrents[0];
|
||||
if (foundTorrent) {
|
||||
return ST.store.getMagnet(foundTorrent.id).then((res) => res.data);
|
||||
}
|
||||
return Promise.reject("No recent torrent found");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
* @param {string} infoHash
|
||||
*/
|
||||
async function _createTorrent(ST, infoHash) {
|
||||
const magnetLink = await getMagnetLink(infoHash);
|
||||
const uploadResponse = await ST.store.addMagnet({ magnet: magnetLink });
|
||||
const torrentId = uploadResponse.data.id;
|
||||
return ST.store
|
||||
.getMagnet(torrentId)
|
||||
.then((statusResponse) => statusResponse.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThru} ST
|
||||
* @param {Awaited<ReturnType<import('stremthru').StremThru['store']['getMagnet']>>['data']} torrent
|
||||
* @param {string} fileName
|
||||
* @param {number} fileIndex
|
||||
*/
|
||||
async function _unrestrictLink(ST, torrent, fileName, fileIndex) {
|
||||
const targetFile = torrent.files.find((file) =>
|
||||
isVideo(file.name) && file.index === -1
|
||||
? file.name === fileName
|
||||
: file.index === fileIndex,
|
||||
);
|
||||
if (!targetFile && torrent.files.every((file) => isArchive(file.name))) {
|
||||
console.log(
|
||||
`Only StremThru archive is available for ${torrent.hash} [${fileIndex}]`,
|
||||
);
|
||||
return StaticResponse.FAILED_RAR;
|
||||
}
|
||||
if (!targetFile || !targetFile.link || !targetFile.link.length) {
|
||||
return Promise.reject(
|
||||
`No StremThru links found for ${torrent.hash} [${fileIndex}]`,
|
||||
);
|
||||
}
|
||||
const unrestrictedLink = await ST.store
|
||||
.generateLink({ link: targetFile.link })
|
||||
.then((response) => response.data.link);
|
||||
console.log(
|
||||
`Unrestricted StremThru ${torrent.hash} [${fileIndex}] to ${unrestrictedLink}`,
|
||||
);
|
||||
return unrestrictedLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} clientIp
|
||||
*/
|
||||
function getDefaultOptions(clientIp) {
|
||||
return { userAgent: AGENT, timeout: 10000, clientIp };
|
||||
}
|
||||
|
||||
export function toCommonError(error) {
|
||||
if (error instanceof StremThruError) {
|
||||
if (error.code === "UNAUTHORIZED") {
|
||||
return BadTokenError;
|
||||
}
|
||||
if (error.code === "FORBIDDEN") {
|
||||
return AccessDeniedError;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StoreMagnetStatus} status
|
||||
*/
|
||||
function statusError(status) {
|
||||
return status === "invalid" || status === "failed";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StoreMagnetStatus} status
|
||||
*/
|
||||
function statusDownloading(status) {
|
||||
return (
|
||||
status === "queued" || status === "downloading" || status === "processing"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StoreMagnetStatus} status
|
||||
*/
|
||||
function statusDownloaded(status) {
|
||||
return status === "downloaded";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('stremthru').StremThruError} error
|
||||
*/
|
||||
function errorFailedAccessError(error) {
|
||||
return (
|
||||
error instanceof StremThruError &&
|
||||
["FORBIDDEN", "UNAUTHORIZED", "PAYMENT_REQUIRED"].includes(error.code)
|
||||
);
|
||||
}
|
||||
35
addon/package-lock.json
generated
35
addon/package-lock.json
generated
|
|
@ -31,6 +31,7 @@
|
|||
"router": "^1.3.8",
|
||||
"sequelize": "^6.29.0",
|
||||
"stremio-addon-sdk": "^1.6.10",
|
||||
"stremthru": "^0.6.0",
|
||||
"swagger-stats": "^0.99.7",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"user-agents": "^1.0.1444"
|
||||
|
|
@ -161,6 +162,30 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz",
|
||||
"integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA=="
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch/node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
|
||||
|
|
@ -2176,6 +2201,16 @@
|
|||
"addon-bootstrap": "cli/bootstrap.js"
|
||||
}
|
||||
},
|
||||
"node_modules/stremthru": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/stremthru/-/stremthru-0.6.0.tgz",
|
||||
"integrity": "sha512-Bzqw6Hw6auVJUOtGjpJGkjZrXl2C03IFijGcAIux/FbCzRnaaOfpjBk+YAFh/kRse/dKFzHZKBcxau3XERxyTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"node-fetch": "release-2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"router": "^1.3.8",
|
||||
"sequelize": "^6.29.0",
|
||||
"stremio-addon-sdk": "^1.6.10",
|
||||
"stremthru": "^0.6.0",
|
||||
"swagger-stats": "^0.99.7",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"user-agents": "^1.0.1444"
|
||||
|
|
|
|||
Loading…
Reference in a new issue