diff --git a/.github/workflows/deploy_addon.yml b/.github/workflows/deploy_addon.yml index 553ae41..15ae633 100644 --- a/.github/workflows/deploy_addon.yml +++ b/.github/workflows/deploy_addon.yml @@ -40,5 +40,5 @@ jobs: docker load -i /tmp/docker/torrentio_addon_latest.tar docker stop torrentio-addon docker rm torrentio-addon - docker run -p 80:7000 -d --name torrentio-addon --restart always -e MONGODB_URI=${{ secrets.MONGODB_URI }} -e DATABASE_URI=${{ secrets.DATABASE_URI }} -e RESOLVER_HOST=${{ secrets.RESOLVER_HOST }} -e PROXY_HOSTS=${{ secrets.PROXY_HOSTS }} -e PROXY_USERNAME=${{ secrets.PROXY_USERNAME }} -e PROXY_PASSWORD=${{ secrets.PROXY_PASSWORD }} torrentio-addon:latest + docker run -p 80:7000 -d --name torrentio-addon --restart always -e MONGODB_URI=${{ secrets.MONGODB_URI }} -e DATABASE_URI=${{ secrets.DATABASE_URI }} -e PROXY_HOSTS=${{ secrets.PROXY_HOSTS }} -e PROXY_USERNAME=${{ secrets.PROXY_USERNAME }} -e PROXY_PASSWORD=${{ secrets.PROXY_PASSWORD }} torrentio-addon:latest docker image prune -f diff --git a/addon/index.js b/addon/index.js index fb543e8..928bff8 100644 --- a/addon/index.js +++ b/addon/index.js @@ -5,6 +5,7 @@ const serverless = require('./serverless'); const { initBestTrackers } = require('./lib/magnetHelper'); const app = express(); +app.enable('trust proxy'); const limiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hours max: 300, // limit each IP to 300 requests per windowMs diff --git a/addon/lib/cache.js b/addon/lib/cache.js index 3da9c51..5d8871b 100644 --- a/addon/lib/cache.js +++ b/addon/lib/cache.js @@ -1,6 +1,6 @@ const cacheManager = require('cache-manager'); const mangodbStore = require('cache-manager-mongodb'); -const { RESOLVER_HOST } = require('../moch/static') +const { isStaticUrl } = require('../moch/static') const GLOBAL_KEY_PREFIX = 'torrentio-addon'; const STREAM_KEY_PREFIX = `${GLOBAL_KEY_PREFIX}|stream`; @@ -68,7 +68,7 @@ function cacheWrapStream(id, method) { function cacheWrapResolvedUrl(id, method) { return cacheWrap(memoryCache, `${RESOLVED_URL_KEY_PREFIX}:${id}`, method, { - ttl: (url) => url.startsWith(RESOLVER_HOST) ? MESSAGE_VIDEO_URL_TTL : STREAM_TTL + ttl: (url) => isStaticUrl(url) ? MESSAGE_VIDEO_URL_TTL : STREAM_TTL }); } diff --git a/addon/moch/moch.js b/addon/moch/moch.js index cba49fa..31f013a 100644 --- a/addon/moch/moch.js +++ b/addon/moch/moch.js @@ -10,11 +10,11 @@ const StaticResponse = require('./static'); const { cacheWrapResolvedUrl } = require('../lib/cache'); const { timeout } = require('../lib/promises'); const { BadTokenError, streamFilename, AccessDeniedError, enrichMeta } = require('./mochHelper'); +const { isStaticUrl } = require("./static"); const RESOLVE_TIMEOUT = 2 * 60 * 1000; // 2 minutes const MIN_API_KEY_SYMBOLS = 15; const TOKEN_BLACKLIST = []; -const RESOLVER_HOST = process.env.RESOLVER_HOST || 'http://localhost:7050'; const MOCHS = { realdebrid: { key: 'realdebrid', @@ -65,11 +65,11 @@ const unrestrictQueue = new namedQueue((task, callback) => task.method() .catch((error => callback(error))), 20); function hasMochConfigured(config) { - return Object.keys(MOCHS).find(moch => config && config[moch]) + return Object.keys(MOCHS).find(moch => config?.[moch]) } async function applyMochs(streams, config) { - if (!streams || !streams.length || !hasMochConfigured(config)) { + if (!streams?.length || !hasMochConfigured(config)) { return streams; } return Promise.all(Object.keys(config) @@ -94,18 +94,19 @@ async function applyMochs(streams, config) { async function resolve(parameters) { const moch = MOCHS[parameters.mochKey]; if (!moch) { - return Promise.reject(`Not a valid moch provider: ${parameters.mochKey}`); + return Promise.reject(new Error(`Not a valid moch provider: ${parameters.mochKey}`)); } if (!parameters.apiKey || !parameters.infoHash || !parameters.cachedEntryInfo) { - return Promise.reject("No valid parameters passed"); + return Promise.reject(new Error("No valid parameters passed")); } const id = `${parameters.ip}_${parameters.mochKey}_${parameters.apiKey}_${parameters.infoHash}_${parameters.fileIndex}`; const method = () => timeout(RESOLVE_TIMEOUT, cacheWrapResolvedUrl(id, () => moch.instance.resolve(parameters))) .catch(error => { console.warn(error); return StaticResponse.FAILED_UNEXPECTED; - }); + }) + .then(url => isStaticUrl(url) ? `${parameters.host}/${url}` : url); return new Promise(((resolve, reject) => { unrestrictQueue.push({ id, method }, (error, result) => result ? resolve(result) : reject(error)); })); @@ -114,10 +115,10 @@ async function resolve(parameters) { async function getMochCatalog(mochKey, config) { const moch = MOCHS[mochKey]; if (!moch) { - return Promise.reject(`Not a valid moch provider: ${mochKey}`); + return Promise.reject(new Error(`Not a valid moch provider: ${mochKey}`)); } if (isInvalidToken(config[mochKey], mochKey)) { - return Promise.reject(`Invalid API key for moch provider: ${mochKey}`); + return Promise.reject(new Error(`Invalid API key for moch provider: ${mochKey}`)); } return moch.instance.getCatalog(config[moch.key], config.skip, config.ip); } @@ -125,7 +126,7 @@ async function getMochCatalog(mochKey, config) { async function getMochItemMeta(mochKey, itemId, config) { const moch = MOCHS[mochKey]; if (!moch) { - return Promise.reject(`Not a valid moch provider: ${mochKey}`); + return Promise.reject(new Error(`Not a valid moch provider: ${mochKey}`)); } return moch.instance.getItemMeta(itemId, config[moch.key], config.ip) @@ -135,14 +136,14 @@ async function getMochItemMeta(mochKey, itemId, config) { .map(video => video.streams) .reduce((a, b) => a.concat(b), []) .filter(stream => !stream.url.startsWith('http')) - .forEach(stream => stream.url = `${RESOLVER_HOST}/${moch.key}/${stream.url}`) + .forEach(stream => stream.url = `${config.host}/${moch.key}/${stream.url}`) return meta; }); } function processMochResults(streams, config, results) { const errorResults = results - .map(result => errorStreamResponse(result.moch.key, result.error)) + .map(result => errorStreamResponse(result.moch.key, result.error, config)) .filter(errorResponse => errorResponse); if (errorResults.length) { return errorResults; @@ -150,22 +151,22 @@ function processMochResults(streams, config, results) { const includeTorrentLinks = options.includeTorrentLinks(config); const excludeDownloadLinks = options.excludeDownloadLinks(config); - const mochResults = results.filter(result => result && result.mochStreams); + const mochResults = results.filter(result => result?.mochStreams); const cachedStreams = mochResults - .reduce((resultStreams, mochResult) => populateCachedLinks(resultStreams, mochResult), streams); - const resultStreams = excludeDownloadLinks ? cachedStreams : populateDownloadLinks(cachedStreams, mochResults); + .reduce((resultStreams, mochResult) => populateCachedLinks(resultStreams, mochResult, config), streams); + const resultStreams = excludeDownloadLinks ? cachedStreams : populateDownloadLinks(cachedStreams, mochResults, config); return includeTorrentLinks ? resultStreams : resultStreams.filter(stream => stream.url); } -function populateCachedLinks(streams, mochResult) { +function populateCachedLinks(streams, mochResult, config) { return streams.map(stream => { const cachedEntry = stream.infoHash && mochResult.mochStreams[stream.infoHash]; - if (cachedEntry && cachedEntry.cached) { + if (cachedEntry?.cached) { return { name: `[${mochResult.moch.shortName}+] ${stream.name}`, title: stream.title, - url: `${RESOLVER_HOST}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`, + url: `${config.host}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`, behaviorHints: stream.behaviorHints }; } @@ -173,17 +174,17 @@ function populateCachedLinks(streams, mochResult) { }); } -function populateDownloadLinks(streams, mochResults) { +function populateDownloadLinks(streams, mochResults, config) { const torrentStreams = streams.filter(stream => stream.infoHash); const seededStreams = streams.filter(stream => !stream.title.includes('👤 0')); torrentStreams.forEach(stream => mochResults.forEach(mochResult => { const cachedEntry = mochResult.mochStreams[stream.infoHash]; - const isCached = cachedEntry && cachedEntry.cached; + const isCached = cachedEntry?.cached; if (!isCached && isHealthyStreamForDebrid(seededStreams, stream)) { streams.push({ name: `[${mochResult.moch.shortName} download] ${stream.name}`, title: stream.title, - url: `${RESOLVER_HOST}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`, + url: `${config.host}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`, behaviorHints: stream.behaviorHints }) } @@ -208,19 +209,19 @@ function blackListToken(token, mochKey) { TOKEN_BLACKLIST.push(tokenKey); } -function errorStreamResponse(mochKey, error) { +function errorStreamResponse(mochKey, error, config) { if (error === BadTokenError) { return { name: `Torrentio\n${MOCHS[mochKey].shortName} error`, title: `Invalid ${MOCHS[mochKey].name} ApiKey/Token!`, - url: StaticResponse.FAILED_ACCESS + url: `${config.host}/${StaticResponse.FAILED_ACCESS}` }; } if (error === AccessDeniedError) { return { name: `Torrentio\n${MOCHS[mochKey].shortName} error`, title: `Expired/invalid ${MOCHS[mochKey].name} subscription!`, - url: StaticResponse.FAILED_ACCESS + url: `${config.host}/${StaticResponse.FAILED_ACCESS}` }; } return undefined; diff --git a/addon/moch/static.js b/addon/moch/static.js index d5f6ee5..fc55088 100644 --- a/addon/moch/static.js +++ b/addon/moch/static.js @@ -1,12 +1,18 @@ -const RESOLVER_HOST = process.env.RESOLVER_HOST || 'http://localhost:7050'; +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` +} + +function isStaticUrl(url) { + return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl)); +} module.exports = { - RESOLVER_HOST: RESOLVER_HOST, - DOWNLOADING: `${RESOLVER_HOST}/videos/downloading_v2.mp4`, - FAILED_DOWNLOAD: `${RESOLVER_HOST}/videos/download_failed_v2.mp4`, - FAILED_ACCESS: `${RESOLVER_HOST}/videos/failed_access_v2.mp4`, - FAILED_RAR: `${RESOLVER_HOST}/videos/failed_rar_v2.mp4`, - FAILED_OPENING: `${RESOLVER_HOST}/videos/failed_opening_v2.mp4`, - FAILED_UNEXPECTED: `${RESOLVER_HOST}/videos/failed_unexpected_v2.mp4`, - FAILED_INFRINGEMENT: `${RESOLVER_HOST}/videos/failed_infringement_v2.mp4` + ...staticVideoUrls, + isStaticUrl } \ No newline at end of file diff --git a/addon/serverless.js b/addon/serverless.js index 2b9f4f1..f580a7f 100644 --- a/addon/serverless.js +++ b/addon/serverless.js @@ -37,7 +37,9 @@ router.get('/:configuration?/manifest.json', (req, res) => { router.get('/:configuration/:resource/:type/:id/:extra?.json', (req, res, next) => { const { configuration, resource, type, id } = req.params; const extra = req.params.extra ? qs.parse(req.url.split('/').pop().slice(0, -5)) : {} - const configValues = { ...extra, ...parseConfiguration(configuration), ip: requestIp.getClientIp(req) }; + const ip = requestIp.getClientIp(req); + const host = `${req.protocol}://${req.headers.host}`; + const configValues = { ...extra, ...parseConfiguration(configuration), ip, host }; addonInterface.get(resource, type, id, configValues) .then(resp => { const cacheHeaders = { @@ -78,6 +80,7 @@ router.get('/:moch/:apiKey/:infoHash/:cachedEntryInfo/:fileIndex/:filename?', (r fileIndex: isNaN(req.params.fileIndex) ? undefined : parseInt(req.params.fileIndex), cachedEntryInfo: req.params.cachedEntryInfo, ip: requestIp.getClientIp(req), + host: `${req.protocol}://${req.headers.host}`, isBrowser: !userAgent.includes('Stremio') && !!userAgentParser(userAgent).browser.name } moch.resolve(parameters)