UNPKG

@egeria/rarbg-plugin

Version:

Egeria | rarbg plugin

268 lines (243 loc) 8.44 kB
/* .--. .-'. .--. .--. .--. .--. .`-. .--. :::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\ ' `--' `.-' `--' `--' `--' `-.' `--' ` Egeria - She bestows Knowledge and Wisdom Copyright (C) 2016-2019 MySidesTheyAreGone <mysidestheyaregone@protonmail.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. .--. .-'. .--. .--. .--. .--. .`-. .--. :::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\ ' `--' `.-' `--' `--' `--' `-.' `--' ` */ const R = require('ramda') const T = require('@egeria/tools') const W = require('@egeria/httplib') const temple = require('@egeria/temple').temple const divine = require('@egeria/divine').video const moment = require('moment') const filesize = require('file-size') const magnetlib = require('magnet-uri') const twelveMinutes = 60 * 12 const isMissing = R.either(R.isNil, R.isEmpty) const clean = R.pipe(R.defaultTo(''), R.replace(/\r?\n|\r/g, ' '), R.trim, R.replace(/\s+/g, ' ')) const categories = [ { code: 'movies', name: 'All movies' }, { code: 'tv', name: 'All TV shows' }, { code: '4', name: 'XXX' }, { code: '14', name: 'Movies/XVID' }, { code: '48', name: 'Movies/XVID/720' }, { code: '17', name: 'Movies/x264' }, { code: '44', name: 'Movies/x264/1080' }, { code: '45', name: 'Movies/x264/720' }, { code: '47', name: 'Movies/x264/3D' }, { code: '42', name: 'Movies/Full BD' }, { code: '46', name: 'Movies/BD remux' }, { code: '18', name: 'TV Episodes' }, { code: '41', name: 'TV HD Episodes' }, { code: '23', name: 'Music/MP3' }, { code: '25', name: 'Music/FLAC' }, { code: '27', name: 'Games/PC ISO' }, { code: '28', name: 'Games/PC RIP' }, { code: '40', name: 'Games/PS3' }, { code: '32', name: 'Games/XBOX-360' }, { code: '33', name: 'Software/PC ISO' }, { code: '35', name: 'e-Books' } ] const base = 'https://torrentapi.org/pubapi_v2.php?app_id=egeria' const commonParameters = '{{&token=||token}}{{&category=||category}}{{&sort=||sort}}' + '{{&min_seeders=||minSeeders}}{{&min_leechers=||minLeechers}}{{&ranked=||ranked}}' const searchParameters = '{{&search_string=||search}}{{&search_imdb=||imdbId}}{{&search_tvdb=||tvdbId}}{{&search_themoviedb=||tmdbId}}' const apiMap = { getToken: '&get_token=get_token&format=json', search: '&mode=search&format=json_extended&limit=100' + commonParameters + searchParameters, list: '&mode=list&format=json_extended&limit=100' + commonParameters } function apiCall (endpoint, parms) { let opts = { method: 'get', json: true, uri: base + temple(endpoint)(parms) } return W.httpreq(opts) } let rarbg = R.mapObjIndexed((endpoint) => R.curry(apiCall)(endpoint), apiMap) function configCallParms (cfg) { let parms = { sort: R.defaultTo('seeders', cfg.sort), minSeeders: cfg.minSeeders, minLeechers: cfg.minLeechers, ranked: (cfg.ranked ? null : 0) } let searchFields = [] R.forEach((s) => { switch (s.type) { case 'query': parms.search = s.term searchFields.push('search') break case 'imdb': parms.imdbId = s.term searchFields.push('imdbId') break case 'tvdb': parms.tvdbId = s.term searchFields.push('tvdbId') break case 'tmdb': parms.tmdbId = s.term searchFields.push('tmdbId') break } }, cfg.search) if (!R.isNil(cfg.categories)) { parms.category = R.pipe( R.map(T.findByProp('name', categories)), R.reject(R.isNil), R.map(R.prop('code')), R.join(';') )(cfg.categories) } let quality = R.defaultTo([], cfg.quality) let reject = R.defaultTo([], cfg.reject) return { parms, searchFields, quality, reject } } async function refreshToken () { let data = await rarbg.getToken(null) let token = data.body.token let expiration = moment().add(12, 'minutes') return { token, expiration } } async function execute (api, opts, returnTopOnly = false) { let { parms, quality, reject } = opts let titleIsRejected = R.anyPass(R.map(R.pipe(T.mkRegExp, R.test), reject)) parms.token = (await api.cache.run({ key: 'rarbg:token', fn: refreshToken, ttl: twelveMinutes })).token let data let c = 0 while (T.isMissing(R.path(['body', 'torrent_results'], data)) && c < 3) { data = await api.enqueue(() => rarbg.search(parms)) c++ } let torrents = R.defaultTo([], data.body.torrent_results) let topTorrent for (let torrent of torrents) { torrent.title = clean(torrent.title) if (titleIsRejected(torrent.title)) { continue } let torrentQuality = divine(torrent.title) let rejectedByQualityFilters = false for (let type in quality) { if (R.isNil(torrentQuality[type]) || !R.contains(torrentQuality[type], quality[type])) { rejectedByQualityFilters = true break } } if (rejectedByQualityFilters) { continue } torrent = R.merge(torrent, torrentQuality) torrent.key = R.prop('xt', magnetlib.decode(torrent.download)) torrent.pubdate = moment(torrent.pubdate, 'YYYY-MM-DD HH:mm:ss').toDate() torrent.hrsize = filesize(parseInt(torrent.size)).human() let metadata = T.collapse('rarbg:', torrent) metadata.key = torrent.key if (returnTopOnly) { topTorrent = metadata break } else { metadata.origin = 'rarbgSearch' api.announce(metadata) } } return topTorrent } async function searchAct (api, cfg) { await execute(api, configCallParms(cfg)) } async function lookupAct (api, cfg, fact) { let { parms, searchFields, quality, reject } = configCallParms(cfg) var searchParms = {} for (let field of searchFields) { searchParms[field] = fact.applyTemplate(parms[field]) } parms = R.merge(parms, searchParms) let topTorrent = await execute(api, { parms, quality, reject }, true) if (isMissing(topTorrent)) { return fact } else { return fact.map(R.merge(topTorrent)) } } let sanity = { type: 'object', required: ['categories', 'search'], properties: { categories: { type: 'array', items: { enum: R.map(R.prop('name'), categories) } }, search: { type: 'array', items: { type: 'object', required: ['type', 'term'], properties: { type: { type: 'string', enum: ['query', 'imdb', 'tvdb', 'tmdb'] }, term: { type: 'string' } } } }, sort: { type: 'string', enum: ['seeders', 'leechers', 'last'] }, minSeeders: { type: 'number', exclusiveMinimum: 0 }, minLeechers: { type: 'number', exclusiveMinimum: 0 }, ranked: { type: 'boolean' }, quality: { type: 'object', properties: { audioCodec: { type: 'array' }, videoCodec: { type: 'array' }, videoResolution: { type: 'array' }, videoSource: { type: 'array' } } }, reject: { type: 'array' } } } const plugins = { rarbgSearch: { type: 'input', requires: ['schedule', 'cache'], sanity: sanity, limits: { upstream: 'rarbg', concurrency: 1, delay: 2.1, timeout: 60 }, act: searchAct }, rarbgLookup: { type: 'mutator', requires: ['cache'], sanity: sanity, limits: { upstream: 'rarbg', concurrency: 1, delay: 2.1, timeout: 60 }, act: lookupAct } } module.exports = plugins