├── .gitignore ├── README.md ├── autoLaunch.js ├── config.js ├── helpers.js ├── index.js ├── jackett.js ├── package.json └── tunnel.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | config.json 30 | 31 | build 32 | release 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stremio Jackett Add-on 2 | 3 | **THIS ADD-ON HAS NOW BEEN PORTED TO [PimpMyStremio](https://www.reddit.com/r/StremioAddons/comments/bsgvjt/news_pimpmystremio_release_a_local_addon_manager/) AND MOVED TO [A NEW REPOSITORY](https://github.com/BoredLama/stremio-jackett-local) FOR THIS PURPOSE, PLEASE USE PimpMyStremio AND INSTALL JACKETT FROM INSIDE THE APP** 4 | 5 | Search on all your favorite torrent sites directly in Stremio! 6 | 7 | **This Add-on requires Stremio v4.4.10+** 8 | 9 | Note: After running the Stremio Jackett Add-on for the first time, a `config.json` file will be created in the same folder as the add-on executable. You can edit this file to configure the add-on. 10 | 11 | Note 2: The Stremio Jackett Add-on executable needs to be running (along with Jackett) in order for this add-on to work in Stremio. 12 | 13 | Note 3: Run the add-on with `--remote` (or set `remote` to `true` in `config.json`) to also receive an add-on url that will work through LAN and the Internet (instead of just locally). 14 | 15 | Note 4: Setting `autoLaunch` to `true` in `config.json` will make the add-on auto launch on system start-up. 16 | 17 | 18 | ## Install and Usage 19 | 20 | 21 | ### Install Jackett 22 | 23 | - [Install Jackett on Windows](https://github.com/Jackett/Jackett#installation-on-windows) 24 | - [Install Jackett on OSX](https://github.com/Jackett/Jackett#installation-on-macos) 25 | - [Install Jackett on Linux](https://github.com/Jackett/Jackett#installation-on-linux) 26 | 27 | 28 | ### Setup Jackett 29 | 30 | Open your browser, go on [http://127.0.0.1:9117/](http://127.0.0.1:9117/). Press "+ Add Indexer", add as many indexers as you want. 31 | 32 | Copy the text from the input where it writes "API Key" from top right of the menu in Jackett. 33 | 34 | 35 | ### Run Jackett Add-on 36 | 37 | [Download Jackett Add-on](https://github.com/BoredLama/stremio-jackett-addon/releases) for your operating system, unpack it, run it. 38 | 39 | 40 | ### Add Jackett Add-on to Stremio 41 | 42 | Add `http://127.0.0.1:7000/[my-jackett-key]/manifest.json` (replace `[my-jackett-key]` with your Jackett API Key) as an Add-on URL in Stremio. 43 | 44 | ![addlink](https://user-images.githubusercontent.com/1777923/43146711-65a33ccc-8f6a-11e8-978e-4c69640e63e3.png) 45 | -------------------------------------------------------------------------------- /autoLaunch.js: -------------------------------------------------------------------------------- 1 | const AutoLaunch = require('auto-launch') 2 | 3 | module.exports = (appName, shouldRun) => { 4 | const addonAutoLauncher = new AutoLaunch({ 5 | name: appName, 6 | path: process.execPath 7 | }) 8 | 9 | addonAutoLauncher.isEnabled() 10 | .then(isEnabled => { 11 | if (isEnabled && !shouldRun) 12 | addonAutoLauncher.disable() 13 | else if (!isEnabled && shouldRun) 14 | addonAutoLauncher.enable() 15 | }) 16 | .catch(err => { 17 | console.log('Auto Launch Error:') 18 | console.log(err) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const commentJson = require('comment-json') 2 | 3 | const path = require('path') 4 | const rootDir = path.dirname(process.execPath) 5 | 6 | const fs = require('fs') 7 | 8 | const configFile = 'config.json' 9 | 10 | const defaultConfig = { 11 | 12 | "// autoLaunch": [["// if this is set to true, the add-on will run on system start-up"]], 13 | "autoLaunch": false, 14 | 15 | "// responseTimeout": [["// for stremio add-on, in milliseconds, if timeout is reached it will respond with whatever results it already has, 0 = no timeout"]], 16 | "responseTimeout": 11000, 17 | 18 | "// addonPort": [["// port to use for stremio add-on, default is 7000"]], 19 | "addonPort": 7000, 20 | 21 | "// minimumSeeds": [["// remove torrents with less then X seeds"]], 22 | "minimumSeeds": 3, 23 | 24 | "// maximumResults": [["// maximum number of torrents to respond with, 0 = no limit"]], 25 | "maximumResults": 15, 26 | 27 | "// remote": [["// make add-on available remotely too, through LAN and the Internet"]], 28 | "remote": false, 29 | 30 | "// subdomain": [["// set the preferred subdomain (if available), only applicable if remote is set to true"]], 31 | "subdomain": false, 32 | 33 | "jackett": { 34 | 35 | "// host": [["// self explanatory, the default port is presumed"]], 36 | "host": "http://127.0.0.1:9117/", 37 | 38 | "// readTimeout": [["// read timeout in milliseconds for http requests to jackett server, 0 = no timeout"]], 39 | "readTimeout": 10000, 40 | 41 | "// openTimeout": [["// open timeout in milliseconds for http requests to jackett server, 0 = no timeout"]], 42 | "openTimeout": 10000 43 | 44 | } 45 | } 46 | 47 | const readConfig = () => { 48 | 49 | const configFilePath = path.join(rootDir, configFile) 50 | 51 | if (fs.existsSync(configFilePath)) { 52 | var config 53 | 54 | try { 55 | config = fs.readFileSync(configFilePath) 56 | } catch(e) { 57 | // ignore read file issues 58 | return defaultConfig 59 | } 60 | 61 | return commentJson.parse(config.toString()) 62 | } else { 63 | const configString = commentJson.stringify(defaultConfig, null, 4) 64 | 65 | try { 66 | fs.writeFileSync(configFilePath, configString) 67 | } catch(e) { 68 | // ignore write file issues 69 | return defaultConfig 70 | } 71 | 72 | return readConfig() 73 | } 74 | 75 | } 76 | 77 | module.exports = readConfig() 78 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | const needle = require('needle') 2 | const videoNameParser = require('video-name-parser') 3 | 4 | const ticker = {} 5 | 6 | const helper = { 7 | 8 | isObject: (s) => { 9 | return (s !== null && typeof s === 'object') 10 | }, 11 | 12 | setTicker: (ticks, cb) => { 13 | 14 | // const tick = setTicket(3, callback); tick(); tick(); tick(); 15 | 16 | const tag = Date.now() 17 | 18 | ticker[tag] = ticks 19 | 20 | return () => { 21 | ticker[tag]-- 22 | if (!ticker[tag]) { 23 | delete ticker[tag] 24 | cb() 25 | } 26 | } 27 | 28 | }, 29 | 30 | followRedirect: (url, cb) => { 31 | if (!url.startsWith('magnet:')) { 32 | // follow redirect to see if the jackett url is a torrent link or a magnet link 33 | needle.get(url, (err, resp, body) => { 34 | if (resp && resp.headers && resp.headers.location) 35 | cb(resp.headers.location) 36 | else 37 | cb(url) 38 | }) 39 | return 40 | } 41 | cb(url) 42 | }, 43 | 44 | episodeTag: (season, episode) => { 45 | return 'S' + ('0' + season).slice(-2) + 'E' + ('0' + episode).slice(-2) 46 | }, 47 | 48 | simpleName: (name) => { 49 | 50 | // Warning: black magic ahead 51 | 52 | name = name.replace(/\.|_|\-|\–|\(|\)|\[|\]|\:|\,/g, ' ') // remove all unwanted characters 53 | name = name.replace(/\s+/g, ' ') // remove duplicate spaces 54 | name = name.replace(/\\\\/g, '\\').replace(new RegExp('\\\\\'|\\\'|\\\\\"|\\\"', 'g'), '') // remove escaped quotes 55 | 56 | return name 57 | }, 58 | 59 | extraTag: (name, searchQuery) => { 60 | 61 | // Warning: black magic ahead 62 | 63 | const parsedName = videoNameParser(name + '.mp4') 64 | 65 | let extraTag = helper.simpleName(name) 66 | 67 | searchQuery = helper.simpleName(searchQuery) 68 | 69 | // remove search query from torrent title 70 | extraTag = extraTag.replace(new RegExp(searchQuery, 'gi'), '') 71 | 72 | // remove parsed movie/show title from torrent title 73 | extraTag = extraTag.replace(new RegExp(parsedName.name, 'gi'), '') 74 | 75 | // remove year 76 | if (parsedName.year) 77 | extraTag = extraTag.replace(parsedName.year+'', '') 78 | 79 | // remove episode tag 80 | if (parsedName.season && parsedName.episode && parsedName.episode.length) 81 | extraTag = extraTag.replace(new RegExp(helper.episodeTag(parsedName.season, parsedName.episode[0]), 'gi'), '') 82 | 83 | // send to barber shop 84 | extraTag = extraTag.trim() 85 | 86 | let extraParts = extraTag.split(' ') 87 | 88 | // scenarios where extraTag starts with '06', and it refers to 'S03E01-06' 89 | // in this case we'll add the episode tag back in the title so it makes sense 90 | if (parsedName.season && parsedName.episode && parsedName.episode.length) { 91 | if (extraParts[0] && (extraParts[0] + '').length == 2 && !isNaN(extraParts[0])) { 92 | const possibleEpTag = helper.episodeTag(parsedName.season, parsedName.episode[0]) + '-' + extraParts[0] 93 | if (name.toLowerCase().includes(possibleEpTag.toLowerCase())) { 94 | extraParts[0] = possibleEpTag 95 | } 96 | } 97 | } 98 | 99 | const foundPart = name.toLowerCase().indexOf(extraParts[0].toLowerCase()) 100 | 101 | if (foundPart > -1) { 102 | 103 | // clean up extra tags, we'll allow more characters here 104 | extraTag = name.substr(foundPart).replace(/_|\(|\)|\[|\]|\,/g, ' ') 105 | 106 | // remove dots, only if there are more then 1. one still makes 107 | // sense for cases like '1.6gb', but in cases of more it is 108 | // probably used instead of space 109 | if ((extraTag.match(/\./g) || []).length > 1) 110 | extraTag = extraTag.replace(/\./g, ' ') 111 | 112 | // remove duplicate space 113 | extraTag = extraTag.replace(/\s+/g,' ') 114 | 115 | } 116 | 117 | return extraTag 118 | 119 | } 120 | } 121 | 122 | module.exports = helper 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const parseTorrent = require('parse-torrent') 2 | const needle = require('needle') 3 | const async = require('async') 4 | const getPort = require('get-port') 5 | 6 | const express = require('express') 7 | const addon = express() 8 | 9 | const jackettApi = require('./jackett') 10 | const tunnel = require('./tunnel') 11 | const helper = require('./helpers') 12 | 13 | const config = require('./config') 14 | 15 | const autoLaunch = require('./autoLaunch') 16 | 17 | const version = require('./package.json').version 18 | 19 | autoLaunch('Jackett Add-on', config.autoLaunch) 20 | 21 | const respond = (res, data) => { 22 | res.setHeader('Access-Control-Allow-Origin', '*') 23 | res.setHeader('Access-Control-Allow-Headers', '*') 24 | res.setHeader('Content-Type', 'application/json') 25 | res.send(data) 26 | } 27 | 28 | const manifest = { 29 | "id": "org.stremio.jackett", 30 | "version": version, 31 | 32 | "name": "Stremio Jackett Addon", 33 | "description": "Stremio Add-on to get torrent results from Jackett", 34 | 35 | "icon": "https://static1.squarespace.com/static/55c17e7ae4b08ccd27be814e/t/599b81c32994ca8ff6c1cd37/1508813048508/Jackett-logo-2.jpg", 36 | 37 | // set what type of resources we will return 38 | "resources": [ 39 | "stream" 40 | ], 41 | 42 | // works for both movies and series 43 | "types": ["movie", "series"], 44 | 45 | // prefix of item IDs (ie: "tt0032138") 46 | "idPrefixes": [ "tt" ], 47 | 48 | "catalogs": [] 49 | 50 | }; 51 | 52 | addon.get('/:jackettKey/manifest.json', (req, res) => { 53 | respond(res, manifest) 54 | }) 55 | 56 | // utility function to create stream object from magnet or remote torrent 57 | const streamFromMagnet = (tor, uri, type, cb) => { 58 | const toStream = (parsed) => { 59 | 60 | const infoHash = parsed.infoHash.toLowerCase() 61 | 62 | let title = tor.extraTag || parsed.name 63 | 64 | const subtitle = 'Seeds: ' + tor.seeders + ' / Peers: ' + tor.peers 65 | 66 | title += (title.indexOf('\n') > -1 ? '\r\n' : '\r\n\r\n') + subtitle 67 | 68 | cb({ 69 | name: tor.from, 70 | type: type, 71 | infoHash: infoHash, 72 | sources: (parsed.announce || []).map(x => { return "tracker:"+x }).concat(["dht:"+infoHash]), 73 | title: title 74 | }) 75 | } 76 | if (uri.startsWith("magnet:?")) { 77 | toStream(parseTorrent(uri)) 78 | } else { 79 | parseTorrent.remote(uri, (err, parsed) => { 80 | if (err) { 81 | cb(false) 82 | return 83 | } 84 | toStream(parsed) 85 | }) 86 | } 87 | } 88 | 89 | // stream response 90 | addon.get('/:jackettKey/stream/:type/:id.json', (req, res) => { 91 | 92 | if (!req.params.id || !req.params.jackettKey) 93 | return respond(res, { streams: [] }) 94 | 95 | let results = [] 96 | 97 | let sentResponse = false 98 | 99 | const respondStreams = () => { 100 | 101 | if (sentResponse) return 102 | sentResponse = true 103 | 104 | if (results && results.length) { 105 | 106 | tempResults = results 107 | 108 | // filter out torrents with less then 3 seeds 109 | 110 | if (config.minimumSeeds) 111 | tempResults = tempResults.filter(el => { return !!(el.seeders && el.seeders > config.minimumSeeds -1) }) 112 | 113 | // order by seeds desc 114 | 115 | tempResults = tempResults.sort((a, b) => { return a.seeders < b.seeders ? 1 : -1 }) 116 | 117 | // limit to 15 results 118 | 119 | if (config.maximumResults) 120 | tempResults = tempResults.slice(0, config.maximumResults) 121 | 122 | const streams = [] 123 | 124 | const q = async.queue((task, callback) => { 125 | if (task && (task.magneturl || task.link)) { 126 | const url = task.magneturl || task.link 127 | // jackett links can sometimes redirect to magnet links or torrent files 128 | // we follow the redirect if needed and bring back the direct link 129 | helper.followRedirect(url, url => { 130 | // convert torrents and magnet links to stream object 131 | streamFromMagnet(task, url, req.params.type, stream => { 132 | if (stream) 133 | streams.push(stream) 134 | callback() 135 | }) 136 | }) 137 | return 138 | } 139 | callback() 140 | }, 1) 141 | 142 | q.drain = () => { 143 | respond(res, { streams: streams }) 144 | } 145 | 146 | tempResults.forEach(elm => { q.push(elm) }) 147 | } else { 148 | respond(res, { streams: [] }) 149 | } 150 | } 151 | 152 | const idParts = req.params.id.split(':') 153 | 154 | const imdbId = idParts[0] 155 | 156 | needle.get('https://v3-cinemeta.strem.io/meta/' + req.params.type + '/' + imdbId + '.json', (err, resp, body) => { 157 | 158 | if (body && body.meta && body.meta.name && body.meta.year) { 159 | 160 | const searchQuery = { 161 | name: body.meta.name, 162 | year: body.meta.year, 163 | type: req.params.type 164 | } 165 | 166 | if (idParts.length == 3) { 167 | searchQuery.season = idParts[1] 168 | searchQuery.episode = idParts[2] 169 | } 170 | 171 | jackettApi.search(req.params.jackettKey, searchQuery, 172 | 173 | partialResponse = (tempResults) => { 174 | results = results.concat(tempResults) 175 | }, 176 | 177 | endResponse = (tempResults) => { 178 | results = tempResults 179 | respondStreams() 180 | }) 181 | 182 | 183 | if (config.responseTimeout) 184 | setTimeout(respondStreams, config.responseTimeout) 185 | 186 | } else { 187 | respond(res, { streams: [] }) 188 | } 189 | }) 190 | 191 | }) 192 | 193 | if (process && process.argv) 194 | process.argv.forEach((cmdLineArg) => { 195 | if (cmdLineArg == '--remote') 196 | config.remote = true 197 | else if (cmdLineArg == '-v') { 198 | // version check 199 | console.log('v' + version) 200 | process.exit() 201 | } 202 | }) 203 | 204 | const runAddon = async () => { 205 | 206 | config.addonPort = await getPort({ port: config.addonPort }) 207 | 208 | addon.listen(config.addonPort, () => { 209 | 210 | console.log('Add-on URL: http://127.0.0.1:'+config.addonPort+'/[my-jackett-key]/manifest.json') 211 | 212 | if (config.remote) { 213 | 214 | const remoteOpts = {} 215 | 216 | if (config.subdomain) 217 | remoteOpts.subdomain = config.subdomain 218 | 219 | tunnel(config.addonPort, remoteOpts) 220 | 221 | } else 222 | console.log('Replace "[my-jackett-key]" with your Jackett API Key') 223 | 224 | }) 225 | } 226 | 227 | runAddon() 228 | -------------------------------------------------------------------------------- /jackett.js: -------------------------------------------------------------------------------- 1 | const xmlJs = require('xml-js') 2 | const needle = require('needle') 3 | const helper = require('./helpers') 4 | 5 | const config = require('./config') 6 | 7 | const getIndexers = (apiKey, cb) => { 8 | needle.get(config.jackett.host + 'api/v2.0/indexers/all/results/torznab/api?apikey='+apiKey+'&t=indexers&configured=true', { 9 | open_timeout: config.jackett.openTimeout, 10 | read_timeout: config.jackett.readTimeout, 11 | parse_response: false 12 | }, (err, resp) => { 13 | if (!err && resp && resp.body) { 14 | let indexers = xmlJs.xml2js(resp.body) 15 | 16 | if (indexers && indexers.elements && indexers.elements[0] && indexers.elements[0].elements) { 17 | indexers = indexers.elements[0].elements 18 | cb(null, indexers) 19 | } else { 20 | cb(new Error('No Indexers')) 21 | } 22 | } else { 23 | cb(err || new Error('No Indexers')) 24 | } 25 | }) 26 | } 27 | 28 | module.exports = { 29 | 30 | search: (apiKey, query, cb, end) => { 31 | getIndexers(apiKey, (err, apiIndexers) => { 32 | if (!err && apiIndexers && apiIndexers.length) { 33 | // we don't handle anime cats yet 34 | const cat = query.type && query.type == 'movie' ? 2000 : 5000 35 | let results = [] 36 | if (apiIndexers && apiIndexers.length) { 37 | 38 | const tick = helper.setTicker(apiIndexers.length, () => { 39 | end(results) 40 | }) 41 | 42 | let searchQuery = query.name 43 | 44 | if (query.season && query.episode) { 45 | searchQuery += ' ' + helper.episodeTag(query.season, query.episode) 46 | } 47 | 48 | apiIndexers.forEach(indexer => { 49 | if (indexer && indexer.attributes && indexer.attributes.id) { 50 | needle.get(config.jackett.host + 'api/v2.0/indexers/'+indexer.attributes.id+'/results/torznab/api?apikey='+apiKey+'&t=search&cat='+cat+'&q='+encodeURI(searchQuery), { 51 | open_timeout: config.jackett.openTimeout, 52 | read_timeout: config.jackett.readTimeout, 53 | parse_response: false 54 | }, (err, resp) => { 55 | if (!err && resp && resp.body) { 56 | const tors = xmlJs.xml2js(resp.body) 57 | 58 | // this is crazy, i know 59 | if (tors.elements && tors.elements[0] && tors.elements[0].elements && tors.elements[0].elements[0] && tors.elements[0].elements[0].elements) { 60 | 61 | const elements = tors.elements[0].elements[0].elements 62 | 63 | const tempResults = [] 64 | 65 | elements.forEach(elem => { 66 | 67 | if (elem.type == 'element' && elem.name == 'item' && elem.elements) { 68 | 69 | const newObj = {} 70 | const tempObj = {} 71 | 72 | elem.elements.forEach(subElm => { 73 | if (subElm.name == 'torznab:attr' && subElm.attributes && subElm.attributes.name && subElm.attributes.value) 74 | tempObj[subElm.attributes.name] = subElm.attributes.value 75 | else if (subElm.elements && subElm.elements.length) 76 | tempObj[subElm.name] = subElm.elements[0].text 77 | }) 78 | 79 | const ofInterest = ['title', 'link', 'magneturl'] 80 | 81 | ofInterest.forEach(ofInterestElm => { 82 | if (tempObj[ofInterestElm]) 83 | newObj[ofInterestElm] = tempObj[ofInterestElm] 84 | }) 85 | 86 | const toInt = ['seeders', 'peers', 'size', 'files'] 87 | 88 | toInt.forEach(toIntElm => { 89 | if (tempObj[toIntElm]) 90 | newObj[toIntElm] = parseInt(tempObj[toIntElm]) 91 | }) 92 | 93 | if (tempObj.pubDate) 94 | newObj.jackettDate = new Date(tempObj.pubDate).getTime() 95 | 96 | newObj.from = indexer.attributes.id 97 | 98 | newObj.extraTag = helper.extraTag(newObj.title, query.name) 99 | tempResults.push(newObj) 100 | } 101 | }) 102 | cb(tempResults) 103 | results = results.concat(tempResults) 104 | } 105 | } 106 | tick() 107 | }) 108 | } 109 | }) 110 | } else { 111 | cb([]) 112 | end([]) 113 | } 114 | } else { 115 | cb([]) 116 | end([]) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-jackett-addon", 3 | "version": "1.0.6", 4 | "description": "Stremio Add-on to get torrent results from Jackett", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "build": "pkg index.js --out-path build/ --targets linux-x64,macos-x64,win-x64,linux-x86,macos-x86,win-x86" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "async": "^2.6.1", 13 | "comment-json": "^1.1.3", 14 | "express": "^4.16.3", 15 | "needle": "^2.2.3", 16 | "parse-torrent": "^6.1.2", 17 | "rusha": "^0.8.1", 18 | "video-name-parser": "^1.4.6", 19 | "xml-js": "^1.6.7", 20 | "localtunnel": "^1.9.1", 21 | "death": "^1.1.0", 22 | "get-port": "^4.0.0", 23 | "auto-launch": "^5.0.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tunnel.js: -------------------------------------------------------------------------------- 1 | 2 | const localtunnel = require('localtunnel') 3 | 4 | let allowClose = false 5 | 6 | let once 7 | 8 | let firstTime 9 | 10 | function runTunnel(addonPort, remoteOpts) { 11 | const tunnel = localtunnel(addonPort, remoteOpts, (err, tunnel) => { 12 | 13 | if (err) { 14 | console.error(err) 15 | return 16 | } 17 | 18 | if (!firstTime || !remoteOpts.subdomain) { 19 | firstTime = true 20 | console.log('Remote Add-on URL: '+tunnel.url+'/[my-jackett-key]/manifest.json') 21 | console.log('Replace "[my-jackett-key]" with your Jackett API Key') 22 | } else { 23 | console.log('Reconnected Tunnel as: '+tunnel.url) 24 | } 25 | 26 | if (remoteOpts.subdomain && !tunnel.url.startsWith('https://' + remoteOpts.subdomain + '.')) { 27 | console.log('Subdomain set but tunnel urls do not match, closing tunnel and trying again in 30 secs') 28 | cleanClose(30) 29 | } 30 | }) 31 | 32 | function cleanClose(secs) { 33 | tunnel.removeListener('close', onClose) 34 | tunnel.removeListener('error', onError) 35 | tunnel.close() 36 | setTimeout(() => { 37 | runTunnel(addonPort, remoteOpts) 38 | }, secs * 1000) 39 | } 40 | 41 | function onClose() { if (allowClose) process.exit() } 42 | 43 | function onError(err) { 44 | console.log('caught exception:') 45 | console.log(err) 46 | console.log('Tunnel error, closing tunnel and trying again in 30 secs') 47 | cleanClose(30) 48 | } 49 | 50 | tunnel.on('close', onClose) 51 | tunnel.on('error', onError) 52 | 53 | if (!once) { 54 | once = true 55 | 56 | const cleanUp = require('death')({ uncaughtException: true }) 57 | 58 | cleanUp((sig, err) => { 59 | console.error(err) 60 | allowClose = true 61 | tunnel.close() 62 | }) 63 | } 64 | } 65 | 66 | module.exports = runTunnel 67 | --------------------------------------------------------------------------------