-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 77c86c9
Showing
5 changed files
with
464 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"jackettKey": { | ||
"title": "Jackett API Key", | ||
"type": "string", | ||
"default": "", | ||
"required": true | ||
}, | ||
"jackettHost": { | ||
"title": "Jackett host", | ||
"type": "string", | ||
"default": "http://127.0.0.1:9117/" | ||
}, | ||
"respTimeout": { | ||
"title": "Response timeout", | ||
"type": "number", | ||
"default": 11000 | ||
}, | ||
"minimumSeeds": { | ||
"title": "Minimum torrent seeds", | ||
"type": "number", | ||
"default": 3 | ||
}, | ||
"maximumResults": { | ||
"title": "Maximum torrents for a response, 0 = no limit", | ||
"type": "number", | ||
"default": 15 | ||
}, | ||
"openTimeout": { | ||
"title": "Requests open timeout", | ||
"type": "number", | ||
"default": 10000 | ||
}, | ||
"readTimeout": { | ||
"title": "Requests read timeout", | ||
"type": "number", | ||
"default": 10000 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
|
||
const modules = require('./modules') | ||
|
||
const ticker = {} | ||
|
||
const helper = { | ||
|
||
isObject: (s) => { | ||
return (s !== null && typeof s === 'object') | ||
}, | ||
|
||
setTicker: (ticks, cb) => { | ||
|
||
// const tick = setTicket(3, callback); tick(); tick(); tick(); | ||
|
||
const tag = Date.now() | ||
|
||
ticker[tag] = ticks | ||
|
||
return () => { | ||
ticker[tag]-- | ||
if (!ticker[tag]) { | ||
delete ticker[tag] | ||
cb() | ||
} | ||
} | ||
|
||
}, | ||
|
||
followRedirect: (url, cb) => { | ||
if (!url.startsWith('magnet:')) { | ||
// follow redirect to see if the jackett url is a torrent link or a magnet link | ||
modules.get.needle.get(url, (err, resp, body) => { | ||
if (resp && resp.headers && resp.headers.location) | ||
cb(resp.headers.location) | ||
else | ||
cb(url) | ||
}) | ||
return | ||
} | ||
cb(url) | ||
}, | ||
|
||
episodeTag: (season, episode) => { | ||
return 'S' + ('0' + season).slice(-2) + 'E' + ('0' + episode).slice(-2) | ||
}, | ||
|
||
simpleName: (name) => { | ||
|
||
// Warning: black magic ahead | ||
|
||
name = name.replace(/\.|_|\-|\–|\(|\)|\[|\]|\:|\,/g, ' ') // remove all unwanted characters | ||
name = name.replace(/\s+/g, ' ') // remove duplicate spaces | ||
name = name.replace(/\\\\/g, '\\').replace(new RegExp('\\\\\'|\\\'|\\\\\"|\\\"', 'g'), '') // remove escaped quotes | ||
|
||
return name | ||
}, | ||
|
||
extraTag: (name, searchQuery) => { | ||
|
||
// Warning: black magic ahead | ||
|
||
const parsedName = modules.get['video-name-parser'](name + '.mp4') | ||
|
||
let extraTag = helper.simpleName(name) | ||
|
||
searchQuery = helper.simpleName(searchQuery) | ||
|
||
// remove search query from torrent title | ||
extraTag = extraTag.replace(new RegExp(searchQuery, 'gi'), '') | ||
|
||
// remove parsed movie/show title from torrent title | ||
extraTag = extraTag.replace(new RegExp(parsedName.name, 'gi'), '') | ||
|
||
// remove year | ||
if (parsedName.year) | ||
extraTag = extraTag.replace(parsedName.year+'', '') | ||
|
||
// remove episode tag | ||
if (parsedName.season && parsedName.episode && parsedName.episode.length) | ||
extraTag = extraTag.replace(new RegExp(helper.episodeTag(parsedName.season, parsedName.episode[0]), 'gi'), '') | ||
|
||
// send to barber shop | ||
extraTag = extraTag.trim() | ||
|
||
let extraParts = extraTag.split(' ') | ||
|
||
// scenarios where extraTag starts with '06', and it refers to 'S03E01-06' | ||
// in this case we'll add the episode tag back in the title so it makes sense | ||
if (parsedName.season && parsedName.episode && parsedName.episode.length) { | ||
if (extraParts[0] && (extraParts[0] + '').length == 2 && !isNaN(extraParts[0])) { | ||
const possibleEpTag = helper.episodeTag(parsedName.season, parsedName.episode[0]) + '-' + extraParts[0] | ||
if (name.toLowerCase().includes(possibleEpTag.toLowerCase())) { | ||
extraParts[0] = possibleEpTag | ||
} | ||
} | ||
} | ||
|
||
const foundPart = name.toLowerCase().indexOf(extraParts[0].toLowerCase()) | ||
|
||
if (foundPart > -1) { | ||
|
||
// clean up extra tags, we'll allow more characters here | ||
extraTag = name.substr(foundPart).replace(/_|\(|\)|\[|\]|\,/g, ' ') | ||
|
||
// remove dots, only if there are more then 1. one still makes | ||
// sense for cases like '1.6gb', but in cases of more it is | ||
// probably used instead of space | ||
if ((extraTag.match(/\./g) || []).length > 1) | ||
extraTag = extraTag.replace(/\./g, ' ') | ||
|
||
// remove duplicate space | ||
extraTag = extraTag.replace(/\s+/g,' ') | ||
|
||
} | ||
|
||
return extraTag | ||
|
||
} | ||
} | ||
|
||
module.exports = helper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
const modules = require('./modules') | ||
const jackettApi = require('./jackett') | ||
const helper = require('./helpers') | ||
|
||
const streamFromMagnet = (tor, uri, type, cb) => { | ||
const parseTorrent = modules.get['parse-torrent'] | ||
const toStream = (parsed) => { | ||
|
||
const infoHash = parsed.infoHash.toLowerCase() | ||
|
||
let title = tor.extraTag || parsed.name | ||
|
||
const subtitle = tor.seeders + ' S / ' + tor.peers + ' L' | ||
|
||
title += '\r\n' + subtitle | ||
|
||
cb({ | ||
name: tor.from, | ||
type: type, | ||
infoHash: infoHash, | ||
sources: (parsed.announce || []).map(x => { return "tracker:"+x }).concat(["dht:"+infoHash]), | ||
title: title | ||
}) | ||
} | ||
if (uri.startsWith("magnet:?")) { | ||
toStream(parseTorrent(uri)) | ||
} else { | ||
parseTorrent.remote(uri, (err, parsed) => { | ||
if (err) { | ||
cb(false) | ||
return | ||
} | ||
toStream(parsed) | ||
}) | ||
} | ||
} | ||
|
||
module.exports = { | ||
manifest: () => { | ||
return { | ||
"id": "org.stremio.jackett", | ||
"version": "1.0.0", | ||
|
||
"name": "Jackett", | ||
"description": "Stremio Add-on to get torrent results from Jackett", | ||
|
||
"icon": "https://static1.squarespace.com/static/55c17e7ae4b08ccd27be814e/t/599b81c32994ca8ff6c1cd37/1508813048508/Jackett-logo-2.jpg", | ||
|
||
"resources": [ | ||
"stream" | ||
], | ||
|
||
"types": ["movie", "series"], | ||
|
||
"idPrefixes": [ "tt" ], | ||
|
||
"catalogs": [] | ||
|
||
} | ||
}, | ||
handler: (args, local) => { | ||
modules.set(local.modules) | ||
const config = local.config | ||
const cinemeta = modules.get.internal.cinemeta | ||
const async = modules.get.async | ||
return new Promise((resolve, reject) => { | ||
|
||
if (args.resource != 'stream'){ | ||
reject(new Error('Resource Unsupported')) | ||
return | ||
} | ||
|
||
if (!args.id) { | ||
reject(new Error('No ID Specified')) | ||
return | ||
} | ||
|
||
let results = [] | ||
|
||
let sentResponse = false | ||
|
||
const respondStreams = () => { | ||
|
||
if (sentResponse) return | ||
sentResponse = true | ||
|
||
if (results && results.length) { | ||
|
||
tempResults = results | ||
|
||
// filter out torrents with less then 3 seeds | ||
|
||
if (config.minimumSeeds) | ||
tempResults = tempResults.filter(el => { return !!(el.seeders && el.seeders > config.minimumSeeds -1) }) | ||
|
||
// order by seeds desc | ||
|
||
tempResults = tempResults.sort((a, b) => { return a.seeders < b.seeders ? 1 : -1 }) | ||
|
||
// limit to 15 results | ||
|
||
if (config.maximumResults) | ||
tempResults = tempResults.slice(0, config.maximumResults) | ||
|
||
const streams = [] | ||
|
||
const q = async.queue((task, callback) => { | ||
if (task && (task.magneturl || task.link)) { | ||
const url = task.magneturl || task.link | ||
// jackett links can sometimes redirect to magnet links or torrent files | ||
// we follow the redirect if needed and bring back the direct link | ||
helper.followRedirect(url, url => { | ||
// convert torrents and magnet links to stream object | ||
streamFromMagnet(task, url, args.type, stream => { | ||
if (stream) | ||
streams.push(stream) | ||
callback() | ||
}) | ||
}) | ||
return | ||
} | ||
callback() | ||
}, 1) | ||
|
||
q.drain = () => { | ||
resolve({ streams: streams }) | ||
} | ||
|
||
tempResults.forEach(elm => { q.push(elm) }) | ||
} else { | ||
resolve({ streams: [] }) | ||
} | ||
} | ||
|
||
const idParts = args.id.split(':') | ||
|
||
const imdb = idParts[0] | ||
|
||
cinemeta.get({ type: args.type, imdb }).then(meta => { | ||
if (meta) { | ||
|
||
const searchQuery = { | ||
name: meta.name, | ||
year: meta.year, | ||
type: args.type | ||
} | ||
|
||
if (idParts.length == 3) { | ||
searchQuery.season = idParts[1] | ||
searchQuery.episode = idParts[2] | ||
} | ||
|
||
jackettApi.search(config, searchQuery, | ||
|
||
partialResponse = (tempResults) => { | ||
results = results.concat(tempResults) | ||
}, | ||
|
||
endResponse = (tempResults) => { | ||
results = tempResults | ||
respondStreams() | ||
}) | ||
|
||
|
||
if (config.responseTimeout) | ||
setTimeout(respondStreams, config.responseTimeout) | ||
|
||
} else { | ||
resolve({ streams: [] }) | ||
} | ||
}) | ||
|
||
}) | ||
} | ||
} |
Oops, something went wrong.