Manage your torrents from your Android device
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

677 lines
30 KiB

/*
* This file is part of Transdroid <http://www.transdroid.org>
*
* Transdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Transdroid 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Transdroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.transdroid.daemon.adapters.rTorrent;
import android.text.TextUtils;
import de.timroes.axmlrpc.XMLRPCClient;
import de.timroes.axmlrpc.XMLRPCClient.UnauthorizdException;
import de.timroes.axmlrpc.XMLRPCException;
import org.transdroid.core.gui.log.Log;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Label;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.HttpHelper;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* An adapter that allows for easy access to rTorrent torrent data. Communication is handled via the XML-RPC protocol as
* implemented by the aXMLRPC library.
*
* @author erickok
*/
public class RTorrentAdapter implements IDaemonAdapter {
private static final String LOG_NAME = "rTorrent daemon";
private static final String DEFAULT_RPC_URL = "/RPC2";
private static final int XMLRPC_MINIMUM_SIZE = 2 * 1024 * 1024;
private static final int XMLRPC_EXTRA_PADDING = 1280;
private DaemonSettings settings;
private XMLRPCClient rpcclient;
private List<Label> lastKnownLabels = null;
private Integer version = null;
public RTorrentAdapter(DaemonSettings settings) {
this.settings = settings;
}
@Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try {
// Ensure a version number is know to switch to the right methods
if (version == null) {
try {
Object versionObject = makeRtorrentCall(log, "system.client_version", new String[0]);
String[] versionRaw = versionObject.toString().split("\\.");
version = (Integer.parseInt(versionRaw[0]) * 10000) + (Integer.parseInt(versionRaw[1]) * 100) + Integer.parseInt(versionRaw[2]);
} catch (Exception e) {
version = 10000;
}
}
switch (task.getMethod()) {
case Retrieve:
// @formatter:off
Object result = makeRtorrentCall(log, "d.multicall2",
new String[]{"", "main",
"d.hash=",
"d.name=",
"d.state=",
"d.down.rate=",
"d.up.rate=",
"d.peers_connected=",
"d.peers_not_connected=",
"d.peers_accounted=",
"d.bytes_done=",
"d.up.total=",
"d.size_bytes=",
"d.creation_date=",
"d.left_bytes=",
"d.complete=",
"d.is_active=",
"d.is_hash_checking=",
"d.is_multi_file=",
"d.base_filename=",
"d.message=",
"d.custom=addtime",
"d.custom=seedingtime",
"d.custom1=",
"d.peers_complete=",
"d.peers_accounted=",
"d.is_open="});
// @formatter:on
return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(result),
lastKnownLabels);
case GetTorrentDetails:
// @formatter:off
Object dresult = makeRtorrentCall(log, "t.multicall", new String[]{
task.getTargetTorrent().getUniqueID(),
"",
"t.url="});
// @formatter:on
return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task,
onTorrentDetailsRetrieved(log, dresult));
case GetFileList:
// @formatter:off
Object fresult = makeRtorrentCall(log, "f.multicall", new String[]{
task.getTargetTorrent().getUniqueID(),
"",
"f.path=",
"f.size_bytes=",
"f.priority=",
"f.completed_chunks=",
"f.size_chunks=",
"f.priority=",
"f.frozen_path="});
// @formatter:on
return new GetFileListTaskSuccessResult((GetFileListTask) task,
onTorrentFilesRetrieved(fresult, task.getTargetTorrent()));
case AddByFile:
// Request to add a torrent by local .torrent file
File file = new File(URI.create(((AddByFileTask) task).getFile()));
FileInputStream in = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[(int) file.length()];
int read;
while ((read = in.read(buffer, 0, buffer.length)) > 0) {
baos.write(buffer, 0, read);
}
byte[] bytes = baos.toByteArray();
int size = Math.max(((int) file.length() * 2) + XMLRPC_EXTRA_PADDING, XMLRPC_MINIMUM_SIZE);
if (version >= 904) {
makeRtorrentCall(log, "network.xmlrpc.size_limit.set", new Object[]{"", size + XMLRPC_EXTRA_PADDING});
makeRtorrentCall(log, "load.raw_start", new Object[]{"", bytes});
} else {
makeRtorrentCall(log, "set_xmlrpc_size_limit", new Object[]{size + XMLRPC_EXTRA_PADDING});
makeRtorrentCall(log, "load_raw_start", new Object[]{bytes});
}
return new DaemonTaskSuccessResult(task);
case AddByUrl:
// Request to add a torrent by URL
String url = ((AddByUrlTask) task).getUrl();
if (version >= 904) {
makeRtorrentCall(log, "load.start", new String[]{"", url});
} else {
makeRtorrentCall(log, "load_start", new String[]{url});
}
return new DaemonTaskSuccessResult(task);
case AddByMagnetUrl:
// Request to add a magnet link by URL
String magnet = ((AddByMagnetUrlTask) task).getUrl();
if (version >= 904) {
makeRtorrentCall(log, "load.start", new String[]{"", magnet});
} else {
makeRtorrentCall(log, "load_start", new String[]{magnet});
}
return new DaemonTaskSuccessResult(task);
case Remove:
// Remove a torrent
RemoveTask removeTask = (RemoveTask) task;
if (removeTask.includingData()) {
makeRtorrentCall(log, "d.custom5.set",
new String[]{task.getTargetTorrent().getUniqueID(), "1"});
makeRtorrentCall(log, "d.delete_tied",
new String[]{task.getTargetTorrent().getUniqueID()});
}
makeRtorrentCall(log, "d.erase", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
case Pause:
// Pause a torrent
makeRtorrentCall(log, "d.stop", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
case PauseAll:
// Resume all torrents
makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.stop="});
return new DaemonTaskSuccessResult(task);
case Resume:
// Resume a torrent
makeRtorrentCall(log, "d.start", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
case ResumeAll:
// Resume all torrents
makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.start="});
return new DaemonTaskSuccessResult(task);
case Stop:
// Stop a torrent
makeRtorrentCall(log, "d.stop", new String[]{task.getTargetTorrent().getUniqueID()});
makeRtorrentCall(log, "d.close", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
case StopAll:
// Stop all torrents
makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.stop=", "d.close="});
return new DaemonTaskSuccessResult(task);
case Start:
// Start a torrent
makeRtorrentCall(log, "d.open", new String[]{task.getTargetTorrent().getUniqueID()});
makeRtorrentCall(log, "d.start", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
case StartAll:
// Start all torrents
makeRtorrentCall(log, "d.multicall2", new String[]{"", "main", "d.open=", "d.start="});
return new DaemonTaskSuccessResult(task);
case SetFilePriorities:
// For each of the chosen files belonging to some torrent, set the priority
SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
String newPriority = "" + convertPriority(prioTask.getNewPriority());
// One at a time; rTorrent doesn't seem to support a multicall on a selective number of files
for (TorrentFile forFile : prioTask.getForFiles()) {
makeRtorrentCall(log, "f.priority.set",
new String[]{task.getTargetTorrent().getUniqueID() + ":f" + forFile.getKey(),
newPriority});
}
return new DaemonTaskSuccessResult(task);
case SetTransferRates:
// Request to set the maximum transfer rates
SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
makeRtorrentCall(log, "throttle.global_down.max_rate.set", new String[]{"", (ratesTask.getDownloadRate() == null ? "0" :
ratesTask.getDownloadRate().toString() + "k")});
makeRtorrentCall(log, "throttle.global_up.max_rate.set", new String[]{"",
(ratesTask.getUploadRate() == null ? "0" : ratesTask.getUploadRate().toString() + "k")});
return new DaemonTaskSuccessResult(task);
case SetLabel:
SetLabelTask labelTask = (SetLabelTask) task;
makeRtorrentCall(log, "d.custom1.set",
new String[]{task.getTargetTorrent().getUniqueID(), labelTask.getNewLabel()});
return new DaemonTaskSuccessResult(task);
case ForceRecheck:
// Force re-check of data of a torrent
makeRtorrentCall(log, "d.check_hash", new String[]{task.getTargetTorrent().getUniqueID()});
return new DaemonTaskSuccessResult(task);
default:
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
task.getMethod() + " is not supported by " + getType()));
}
} catch (DaemonException e) {
return new DaemonTaskFailureResult(task, e);
} catch (FileNotFoundException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
} catch (IOException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError, e.toString()));
}
}
private Object makeRtorrentCall(Log log, String serverMethod, Object[] arguments)
throws DaemonException, MalformedURLException {
// Initialise the HTTP client
initialise();
StringBuilder paramsBuilder = new StringBuilder();
for (Object arg : arguments) {
paramsBuilder.append(" ").append(arg.toString());
}
String params = paramsBuilder.toString();
String s = params.length() > 100 ? params.substring(0, 100) + "..." : params;
try {
log.d(LOG_NAME, "Calling " + serverMethod + " with params [" +
s + " ]");
return rpcclient.call(serverMethod, arguments);
} catch (IllegalArgumentException e) {
log.d(LOG_NAME, "Using " + buildWebUIUrl() + ": " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, "Error making call to " + serverMethod);
} catch (XMLRPCException e) {
log.d(LOG_NAME, e.toString());
if (e.getCause() instanceof UnauthorizdException) {
throw new DaemonException(ExceptionType.AuthenticationFailure, e.toString());
}
if (e.getCause() instanceof DaemonException) {
throw (DaemonException) e.getCause();
}
throw new DaemonException(ExceptionType.ConnectionError,
"Error making call to " + serverMethod + " with params [" +
s + " ]: " +
e.toString());
}
}
/**
* Instantiates a XML-RPC client with proper credentials.
*
* @throws DaemonException On conflicting settings (i.e. user authentication but no password or username provided)
*/
private synchronized void initialise() throws DaemonException {
if(rpcclient == null) {
int flags = XMLRPCClient.FLAGS_8BYTE_INT;
this.rpcclient = new XMLRPCClient(HttpHelper.createStandardHttpClient(settings, true),
settings.getAddress(), buildWebUIUrl(), flags);
}
}
/**
* Build the URL of rTorrent's XML-RPC location from the user settings.
*
* @return The URL of the RPC API
*/
private String buildWebUIUrl() {
String address = settings.getAddress() == null ? "" : settings.getAddress().trim();
String folder = settings.getFolder() == null ? "" : settings.getFolder().trim();
return (settings.getSsl() ? "https://" : "http://") + address + ":" + settings.getPort() +
(TextUtils.isEmpty(folder) ? DEFAULT_RPC_URL : folder);
}
private List<Torrent> onTorrentsRetrieved(Object response) throws DaemonException {
if (!(response instanceof Object[])) {
throw new DaemonException(ExceptionType.ParsingFailed,
"Response on retrieveing torrents did not return a list of objects");
} else {
// Parse torrent list from response
// Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
List<Torrent> torrents = new ArrayList<>();
Map<String, Integer> labels = new HashMap<>();
Object[] responseList = (Object[]) response;
for (int i = 0; i < responseList.length; i++) {
Object[] info = (Object[]) responseList[i];
String error = (String) info[18];
error = error.equals("") ? null : error;
// Determine the time added
Date added;
Long addtime = null;
try {
addtime = Long.valueOf(((String) info[19]).trim());
} catch (NumberFormatException e) {
// Not a number (timestamp); ignore and fall back to using creationtime
}
if (addtime != null)
// Successfully received the addtime from rTorrent (which is a String like '1337089336\n')
{
added = new Date(addtime * 1000L);
} else {
// rTorrent didn't have the addtime (missing plugin?): base it on creationtime instead
if (info[11] instanceof Long) {
added = new Date((Long) info[11] * 1000L);
} else {
added = new Date((Integer) info[11] * 1000L);
}
}
// Determine the seeding time
Date finished = null;
Long seedingtime = null;
try {
seedingtime = Long.valueOf(((String) info[20]).trim());
} catch (NumberFormatException e) {
// Not a number (timestamp); ignore and fall back to using creationtime
}
if (seedingtime != null)
// Successfully received the seedingtime from rTorrent (which is a String like '1337089336\n')
{
finished = new Date(seedingtime * 1000L);
}
// Determine the label
String label = null;
try {
label = URLDecoder.decode((String) info[21], "UTF-8");
if (labels.containsKey(label)) {
labels.put(label, labels.get(label) + 1);
} else {
labels.put(label, 0);
}
} catch (UnsupportedEncodingException e) {
// Can't decode label name; ignore it
}
String baseFilename = info[17] + "/";
if (info[3] instanceof Long) {
// rTorrent uses the i8 dialect which returns 64-bit integers
long rateDownload = (Long) info[3];
// @formatter:off
torrents.add(new Torrent(
i,
(String) info[0], // hash
(String) info[1], // name
convertTorrentStatus((Long) info[2], (Long) info[24], (Long) info[13], (Long) info[14], (Long) info[15]), // status
(((Long) info[16]) == 1) ? baseFilename : "", // multi file? base_filename else ""
((Long) info[3]).intValue(), // rateDownload
((Long) info[4]).intValue(), // rateUpload
((Long) info[22]).intValue(), // seedersConnected
((Long) info[5]).intValue() + ((Long) info[6]).intValue(), // seedersKnown
((Long) info[23]).intValue(), // leechersConnected
((Long) info[5]).intValue() + ((Long) info[6]).intValue(), // leechersKnown
(rateDownload > 0 ? (int) (((Long) info[12]) / rateDownload) : -1), // eta (bytes left / rate download, if rate > 0)
(Long) info[8], // downloadedEver
(Long) info[9], // uploadedEver
(Long) info[10], // totalSize
((Long) info[8]).floatValue() / ((Long) info[10]).floatValue(), // partDone
0f, // TODO: Add availability data
label,
added,
finished,
error,
settings.getType()));
// @formatter:on
} else {
// rTorrent uses the default dialect with 32-bit integers
int rateDownload = (Integer) info[3];
// @formatter:off
torrents.add(new Torrent(
i,
(String) info[0], // hash
(String) info[1], // name
convertTorrentStatus(((Integer) info[2]).longValue(), ((Integer) info[24]).longValue(), ((Integer) info[13]).longValue(), ((Integer) info[14]).longValue(), ((Integer) info[15]).longValue()), // status
(((Integer) info[16]) == 1) ? baseFilename : "", // multi file? base_filename else ""
rateDownload, // rateDownload
(Integer) info[4], // rateUpload
(Integer) info[22], // seedersConnected
(Integer) info[5] + (Integer) info[6], // seedersKnown
(Integer) info[23], // leechersConnected
(Integer) info[5] + (Integer) info[6], // leechersKnown
(rateDownload > 0 ? (Integer) info[12] / rateDownload : -1), // eta (bytes left / rate download, if rate > 0)
(Integer) info[8], // downloadedEver
(Integer) info[9], // uploadedEver
(Integer) info[10], // totalSize
((Integer) info[8]).floatValue() / ((Integer) info[10]).floatValue(), // partDone
0f, // TODO: Add availability data
label,
added,
finished,
error,
settings.getType()));
// @formatter:on
}
}
lastKnownLabels = new ArrayList<>();
for (Entry<String, Integer> pair : labels.entrySet()) {
if (pair.getKey() != null) {
lastKnownLabels.add(new Label(pair.getKey(), pair.getValue()));
}
}
return torrents;
}
}
private List<TorrentFile> onTorrentFilesRetrieved(Object response, Torrent torrent) throws DaemonException {
if (!(response instanceof Object[])) {
throw new DaemonException(ExceptionType.ParsingFailed,
"Response on retrieveing torrent files did not return a list of objects");
} else {
// Parse torrent files from response
// Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
List<TorrentFile> files = new ArrayList<>();
Object[] responseList = (Object[]) response;
for (int i = 0; i < responseList.length; i++) {
Object[] info = (Object[]) responseList[i];
if (info[1] instanceof Long) {
// rTorrent uses the i8 dialect which returns 64-bit integers
Long size = (Long) info[1];
Long chunksDone = (Long) info[3];
Long chunksTotal = (Long) info[4];
Long priority = (Long) info[5];
// @formatter:off
files.add(new TorrentFile(
"" + i,
(String) info[0], // name
torrent.getLocationDir() + info[0], // torrent locationDir + file name
(String) info[6], // fullPath
size, // size
(long) (size * ((float) chunksDone / (float) chunksTotal)), // done
convertRtorrentPriority(priority.intValue()))); // priority
// @formatter:on
} else {
// rTorrent uses the default dialect with 32-bit integers
Integer size = (Integer) info[1];
Integer chunksDone = (Integer) info[3];
Integer chunksTotal = (Integer) info[4];
Integer priority = (Integer) info[5];
// @formatter:off
files.add(new TorrentFile(
"" + i,
(String) info[0], // name
torrent.getLocationDir() + "/" + info[0], // torrent locationDir + file name
(String) info[0], // fullPath
size, // size
(int) (size * ((float) chunksDone / (float) chunksTotal)), // done
convertRtorrentPriority(priority))); // priority
// @formatter:on
}
}
return files;
}
}
private Priority convertRtorrentPriority(int code) {
// Note that Rtorrent has no low priority value
switch (code) {
case 0:
return Priority.Off;
case 2:
return Priority.High;
default:
return Priority.Normal;
}
}
private int convertPriority(Priority priority) {
// Note that Rtorrent has no low priority value
switch (priority) {
case Off:
return 0;
case High:
return 2;
default:
return 1;
}
}
private TorrentStatus convertTorrentStatus(Long state, Long open, Long complete, Long active, Long checking) {
if (checking == 1) {
return TorrentStatus.Checking;
}
if (open == 1) {
// links with tracker/peers are open: not stopped
if (state == 1 && active == 1) {
if (complete == 1) {
return TorrentStatus.Seeding;
} else {
return TorrentStatus.Downloading;
}
}
return TorrentStatus.Paused;
} else {
// maybe could be Stopped or Waiting
return TorrentStatus.Queued;
}
}
private TorrentDetails onTorrentDetailsRetrieved(Log log, Object response) throws DaemonException {
if (!(response instanceof Object[])) {
throw new DaemonException(ExceptionType.ParsingFailed,
"Response on retrieveing trackers did not return a list of objects");
} else {
// Parse a torrent's trackers from response
// Formatted as Object[][], see http://libtorrent.rakshasa.no/wiki/RTorrentCommands#Download
List<String> trackers = new ArrayList<>();
Object[] responseList = (Object[]) response;
try {
for (Object aResponseList : responseList) {
Object[] info = (Object[]) aResponseList;
trackers.add((String) info[0]);
}
} catch (Exception e) {
log.e(LOG_NAME, e.toString());
}
return new TorrentDetails(trackers, null);
}
}
@Override
public Daemon getType() {
return settings.getType();
}
@Override
public DaemonSettings getSettings() {
return this.settings;
}
}