@egeria/rarbg-plugin
Version:
Egeria | rarbg plugin
268 lines (243 loc) • 8.44 kB
JavaScript
/*
.--. .-'. .--. .--. .--. .--. .`-. .--.
:::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\::::::::.\
' `--' `.-' `--' `--' `--' `-.' `--' `
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