a66982a00d
Transmission addUrl sometimes returns a response without a hashString (e.g. duplicate or malformed response). Extract the info hash from the magnet link as a fallback, and log the response if neither works. Signed-off-by: William Mantly <wmantly@gmail.com>
222 lines
5.8 KiB
JavaScript
222 lines
5.8 KiB
JavaScript
'use strict';
|
|
|
|
const Transmission = require('transmission-promise');
|
|
const conf = require('>/conf');
|
|
const { withRetry } = require('>/controller/retry');
|
|
|
|
const tr_client = new Transmission(conf.transmission);
|
|
|
|
// Wrap the raw Transmission client so every RPC call retries on transient network errors.
|
|
const trClient = new Proxy(tr_client, {
|
|
get(target, prop){
|
|
let value = target[prop];
|
|
if(typeof value !== 'function') return value;
|
|
return async function(...args){
|
|
return await withRetry(
|
|
() => value.apply(target, args),
|
|
{ label: `transmission ${String(prop)}` }
|
|
);
|
|
};
|
|
}
|
|
});
|
|
|
|
const statusMap = [
|
|
'STOPPED', // 0
|
|
'CHECK_WAIT', // 1
|
|
'CHECK', // 2
|
|
'DOWNLOAD_WAIT', // 3
|
|
'DOWNLOAD', // 4
|
|
'SEED_WAIT', // 5
|
|
'SEED', // 6
|
|
'ISOLATED', // 7
|
|
];
|
|
|
|
module.exports = (sequelize, DataTypes, Model) => {
|
|
class Torrent extends Model {
|
|
/**
|
|
* Helper method for defining associations.
|
|
* This method is not a part of Sequelize lifecycle.
|
|
* The `models/index` file will call this method automatically.
|
|
*/
|
|
static associate(models) {
|
|
// define association here
|
|
}
|
|
|
|
static trClient = trClient;
|
|
|
|
// Map a raw TPB category id( e.g. 207) to one of our buckets. TPB groups by the
|
|
// hundreds digit; TV shows are the exception pulled out of the Video group.
|
|
static categoryFromTPB(id){
|
|
id = parseInt(id, 10);
|
|
if([205, 208, 212].includes(id)) return 'TV';
|
|
return {
|
|
1: 'Music',
|
|
2: 'Movie',
|
|
3: 'App',
|
|
4: 'Game',
|
|
5: 'Adult',
|
|
6: 'Other',
|
|
}[Math.floor(id / 100)] || 'Other';
|
|
}
|
|
|
|
static async create(data, ...args){
|
|
try{
|
|
|
|
// let instance = this.build(data);
|
|
// console.log('instance', instance)
|
|
await this.build(data).validate();
|
|
// console.log('validate', val);
|
|
data.isPrivate = data.isPrivate === 'true' ? true : false;
|
|
|
|
let options = {
|
|
'download-dir': data.isPrivate ? `${conf.privateDownloadLocation}/${data.added_by}` : undefined,
|
|
};
|
|
|
|
let res = await trClient.addUrl(data.magnetLink, options);
|
|
|
|
// Transmission usually returns hashString, but if it doesn't (duplicate, malformed
|
|
// response, etc.), fall back to parsing the info hash from the magnet link.
|
|
let hashString = res && (res.hashString || res.id);
|
|
if(!hashString){
|
|
let match = data.magnetLink.match(/urn:btih:([A-Fa-f0-9]{40})/i);
|
|
if(match) hashString = match[1].toLowerCase();
|
|
}
|
|
|
|
if(!hashString){
|
|
console.error('Transmission addUrl response missing hashString:', res);
|
|
let error = new Error('TorrentCreateError');
|
|
error.message = 'Transmission did not return a hash for this torrent';
|
|
throw error;
|
|
}
|
|
|
|
return await super.create({
|
|
magnetLink: data.magnetLink,
|
|
hashString,
|
|
isPrivate: data.isPrivate,
|
|
name: res && res.name,
|
|
added_by: data.added_by,
|
|
category: this.categoryFromTPB(data.category),
|
|
status: 0,
|
|
percentDone: 0,
|
|
}, args);
|
|
}catch (error){
|
|
console.log('Torrent create error', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getTorrentData(){
|
|
try{
|
|
|
|
if(this.percentDone === 1) return this.dataValues
|
|
|
|
let res = ( await trClient.get(this.hashString, [
|
|
"eta", "percentDone", "status", "rateDownload",
|
|
"errorString", "hashString", 'name',
|
|
'downloadDir',
|
|
'dateCreated',
|
|
'files', //array of files
|
|
'filesStats', // array of files with status
|
|
'isFinished',
|
|
'isStalled',
|
|
'peers',
|
|
'peersConnected', // array of peers,
|
|
'sizeWhenDone',
|
|
]) ).torrents[0];
|
|
|
|
await this.update(res);
|
|
|
|
return {...res, ...this.dataValues};
|
|
}catch(error){
|
|
if(error.code === 'ECONNREFUSED'){
|
|
let e = new Error('TorrentGatewayDown')
|
|
e.status = 555
|
|
throw e
|
|
}
|
|
// console.error(`Torrent ${this.hashString} getTorrentData error`, error);
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async stop(){
|
|
return await this.constructor.trClient.stop(this.hashString);
|
|
}
|
|
|
|
async start(force){
|
|
if(force) return await this.constructor.trClient.startNow(this.hashString);
|
|
let res = await this.constructor.trClient.start(this.hashString);
|
|
console.log('start', res);
|
|
return res;
|
|
}
|
|
|
|
async destroy(){
|
|
await this.constructor.trClient.remove(this.hashString, true);
|
|
return await super.destroy()
|
|
}
|
|
}
|
|
Torrent.init({
|
|
hashString: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
primaryKey: true
|
|
},
|
|
magnetLink: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
validate:{
|
|
notNull: true,
|
|
notEmpty: true,
|
|
},
|
|
},
|
|
isPrivate: {
|
|
type: DataTypes.BOOLEAN,
|
|
defaultValue: false,
|
|
},
|
|
name: DataTypes.STRING,
|
|
category: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
defaultValue: 'Other',
|
|
},
|
|
added_by: {
|
|
type: DataTypes.STRING,
|
|
ldapModel: 'User',
|
|
allowNull: false,
|
|
validate:{
|
|
notNull: true,
|
|
notEmpty: true,
|
|
},
|
|
},
|
|
status: DataTypes.NUMBER,
|
|
percentDone: DataTypes.FLOAT,
|
|
downloadDir: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
},
|
|
errorString: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
},
|
|
sizeWhenDone: {
|
|
type: DataTypes.NUMBER,
|
|
allowNull: true,
|
|
},
|
|
createdAt: {
|
|
type: DataTypes.DATE
|
|
},
|
|
organizedAt: {
|
|
type: DataTypes.DATE,
|
|
allowNull: true,
|
|
},
|
|
metadata: {
|
|
type: DataTypes.JSON,
|
|
allowNull: true,
|
|
},
|
|
}, {
|
|
sequelize,
|
|
modelName: 'Torrent',
|
|
logging: false,
|
|
});
|
|
return Torrent;
|
|
};
|