/*
* 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.Vuze;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.openjpa.lib.util.Base16Encoder;
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.DaemonMethod;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.AddByFileTask;
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.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
/**
* An adapter that allows for easy access to Vuze torrent data. Communication
* is handled via the XML-RPC protocol.
*
* @author erickok
*
*/
public class VuzeAdapter implements IDaemonAdapter {
private static final String LOG_NAME = "Vuze daemon";
private static final String RPC_URL = "/process.cgi";
private VuzeXmlOverHttpClient rpcclient;
private DaemonSettings settings;
private Long savedConnectionID;
private Long savedPluginID;
private Long savedDownloadManagerID;
private Long savedTorrentManagerID;
private Long savedPluginConfigID;
public VuzeAdapter(DaemonSettings settings) {
this.settings = settings;
}
@Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try {
switch (task.getMethod()) {
case Retrieve:
Object result = makeVuzeCall(DaemonMethod.Retrieve, "getDownloads");
return new RetrieveTaskSuccessResult((RetrieveTask) task, onTorrentsRetrieved(log, result),null);
case GetFileList:
// Retrieve a listing of the files in some torrent
Object fresult = makeVuzeCall(DaemonMethod.GetFileList, "getDiskManagerFileInfo", task.getTargetTorrent(), new Object[] {} );
return new GetFileListTaskSuccessResult((GetFileListTask) task, onTorrentFilesRetrieved(fresult, task.getTargetTorrent()));
case AddByFile:
byte[] bytes;
FileInputStream in = null;
try {
// Request to add a torrent by local .torrent file
String file = ((AddByFileTask)task).getFile();
in = new FileInputStream(new File(URI.create(file)));
bytes = new byte[in.available()];
in.read(bytes, 0, in.available());
in.close();
} catch (FileNotFoundException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
} catch (IllegalArgumentException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, "Invalid local URI"));
} catch (Exception e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
} finally {
try {
if (in != null)
in.close();
} catch (IOException e) {
// Ignore; it was already closed or never opened
}
}
makeVuzeCall(DaemonMethod.AddByFile, "createFromBEncodedData[byte[]]", new String[] { Base16Encoder.encode(bytes) });
return new DaemonTaskSuccessResult(task);
case AddByUrl:
// Request to add a torrent by URL
String url = ((AddByUrlTask)task).getUrl();
makeVuzeCall(DaemonMethod.AddByUrl, "addDownload[URL]", new URL[] { new URL(url) });
return new DaemonTaskSuccessResult(task);
case Remove:
// Remove a torrent
RemoveTask removeTask = (RemoveTask) task;
if (removeTask.includingData()) {
makeVuzeCall(DaemonMethod.Remove, "remove[boolean,boolean]", task.getTargetTorrent(), new String[] { "true", "true"} );
} else {
makeVuzeCall(DaemonMethod.Remove, "remove", task.getTargetTorrent(), new Object[] {} );
}
return new DaemonTaskSuccessResult(task);
case Pause:
// Pause a torrent
makeVuzeCall(DaemonMethod.Pause, "stop", task.getTargetTorrent(), new Object[] {} );
return new DaemonTaskSuccessResult(task);
case PauseAll:
// Resume all torrents
makeVuzeCall(DaemonMethod.ResumeAll, "stopAllDownloads");
return new DaemonTaskSuccessResult(task);
case Resume:
// Resume a torrent
makeVuzeCall(DaemonMethod.Start, "restart", task.getTargetTorrent(), new Object[] {} );
return new DaemonTaskSuccessResult(task);
case ResumeAll:
// Resume all torrents
makeVuzeCall(DaemonMethod.ResumeAll, "startAllDownloads" );
return new DaemonTaskSuccessResult(task);
case SetFilePriorities:
// For each of the chosen files belonging to some torrent, set the priority
SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
// One at a time; Vuze doesn't seem to support setting the isPriority or isSkipped on (a subset of) all files at once
for (TorrentFile forFile : prioTask.getForFiles()) {
if (prioTask.getNewPriority() == Priority.Off) {
makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[] { "true" } );
} else if (prioTask.getNewPriority() == Priority.High) {
makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[] { "false" } );
makeVuzeCall(DaemonMethod.SetFilePriorities, "setPriority[boolean]", Long.parseLong(forFile.getKey()), new String[] { "true" } );
} else {
makeVuzeCall(DaemonMethod.SetFilePriorities, "setSkipped[boolean]", Long.parseLong(forFile.getKey()), new String[] { "false" } );
makeVuzeCall(DaemonMethod.SetFilePriorities, "setPriority[boolean]", Long.parseLong(forFile.getKey()), new String[] { "false" } );
}
}
return new DaemonTaskSuccessResult(task);
case SetTransferRates:
// Request to set the maximum transfer rates
SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
makeVuzeCall(DaemonMethod.SetTransferRates, "setBooleanParameter[String,boolean]", new Object[] { "Auto Upload Speed Enabled", false } );
makeVuzeCall(DaemonMethod.SetTransferRates, "setCoreIntParameter[String,int]", new Object[] { "Max Upload Speed KBs", (ratesTask.getUploadRate() == null? 0:
ratesTask.getUploadRate())} );
makeVuzeCall(DaemonMethod.SetTransferRates, "setCoreIntParameter[String,int]", new Object[] { "Max Download Speed KBs", (ratesTask.getDownloadRate() == null? 0:
ratesTask.getDownloadRate())} );
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 (IOException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError, e.toString()));
}
}
private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Torrent actOnTorrent, Object[] params) throws DaemonException {
return makeVuzeCall(method, serverMethod, Long.parseLong(actOnTorrent.getUniqueID()), params, actOnTorrent.getStatusCode());
}
private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Long actOnObject, Object[] params) throws DaemonException {
return makeVuzeCall(method, serverMethod, actOnObject, params, null);
}
private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Object[] params) throws DaemonException {
return makeVuzeCall(method, serverMethod, null, params, null);
}
private Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod) throws DaemonException {
return makeVuzeCall(method, serverMethod, null, new Object[] {}, null);
}
private synchronized Map<String, Object> makeVuzeCall(DaemonMethod method, String serverMethod, Long actOnObject, Object[] params, TorrentStatus torrentStatus) throws DaemonException {
// TODO: It would be nicer to now split each of these steps into separate makeVuzeCalls when there are multiple logical steps such as stopping a torrent before removing it
// Initialise the HTTP client
if (rpcclient == null) {
initialise();
}
if (settings.getAddress() == null || settings.getAddress().equals("")) {
throw new DaemonException(DaemonException.ExceptionType.AuthenticationFailure, "No host name specified.");
}
if (savedConnectionID == null || savedPluginID == null) {
// Get plug-in interface (for connection and plug-in object IDs)
Map<String, Object> plugin = rpcclient.callXMLRPC(null, "getSingleton", null, null, false);
if (!plugin.containsKey("_connection_id")) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No connection ID returned on getSingleton request.");
}
savedConnectionID = (Long) plugin.get("_connection_id");
savedPluginID = (Long) plugin.get("_object_id");
}
// If no specific torrent was provided, get the download manager or plugin config to execute the method against
long vuzeObjectID;
if (actOnObject == null) {
if (method == DaemonMethod.SetTransferRates) {
// Execute this method against the plugin config (setParameter)
if (savedPluginConfigID == null) {
// Plugin config needed, but we don't know it's ID yet
Map<String, Object> config = rpcclient.callXMLRPC(savedPluginID, "getPluginconfig", null, savedConnectionID, false);
if (!config.containsKey("_object_id")) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No plugin config ID returned on getPluginconfig");
}
savedPluginConfigID = (Long) config.get("_object_id");
vuzeObjectID = savedPluginConfigID;
} else {
// We stored the plugin config ID, so no need to ask for it again
vuzeObjectID = savedPluginConfigID;
}
} else if (serverMethod.equals("createFromBEncodedData[byte[]]")) {
// Execute this method against the torrent manager (createFromBEncodedData)
if (savedTorrentManagerID == null) {
// Download manager needed, but we don't know it's ID yet
Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getTorrentManager", null, savedConnectionID, false);
if (!manager.containsKey("_object_id")) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No torrent manager ID returned on getTorrentManager");
}
savedTorrentManagerID = (Long) manager.get("_object_id");
vuzeObjectID = savedTorrentManagerID;
} else {
// We stored the torrent manager ID, so no need to ask for it again
vuzeObjectID = savedTorrentManagerID;
}
// And we will need the download manager as well later on (for addDownload after createFromBEncodedData)
if (savedDownloadManagerID == null) {
// Download manager needed, but we don't know it's ID yet
Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getDownloadManager", null, savedConnectionID, false);
if (!manager.containsKey("_object_id")) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No download manager ID returned on getDownloadManager");
}
savedDownloadManagerID = (Long) manager.get("_object_id");
}
} else {
// Execute this method against download manager (addDownload, startAllDownloads, etc.)
if (savedDownloadManagerID == null) {
// Download manager needed, but we don't know it's ID yet
Map<String, Object> manager = rpcclient.callXMLRPC(savedPluginID, "getDownloadManager", null, savedConnectionID, false);
if (!manager.containsKey("_object_id")) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No download manager ID returned on getDownloadManager");
}
savedDownloadManagerID = (Long) manager.get("_object_id");
vuzeObjectID = savedDownloadManagerID;
} else {
// We stored the download manager ID, so no need to ask for it again
vuzeObjectID = savedDownloadManagerID;
}
}
} else {
vuzeObjectID = actOnObject;
}
if (method == DaemonMethod.Remove && torrentStatus != null && torrentStatus != TorrentStatus.Paused) {
// Vuze, for some strange reason, wants us to stop the torrent first before removing it
rpcclient.callXMLRPC(vuzeObjectID, "stop", new Object[] {}, savedConnectionID, false);
}
boolean paramsAreVuzeObjects = false;
if (serverMethod.equals("createFromBEncodedData[byte[]]")) {
// Vuze does not directly add the torrent that we are uploading the file contents of
// We first do the createFromBEncodedData call and next actually add it
Map<String, Object> torrentData = rpcclient.callXMLRPC(vuzeObjectID, serverMethod, params, savedConnectionID, false);
serverMethod = "addDownload[Torrent]";
vuzeObjectID = savedDownloadManagerID;
params = new String[] { torrentData.get("_object_id").toString() };
paramsAreVuzeObjects = true;
}
// Call the actual method we wanted
return rpcclient.callXMLRPC(vuzeObjectID, serverMethod, params, savedConnectionID, paramsAreVuzeObjects);
}
/**
* Instantiates a Vuze XML over HTTP client with proper credentials.
* @throws DaemonException On conflicting settings (i.e. user authentication but no password or username provided)
*/
private void initialise() throws DaemonException {
this.rpcclient = new VuzeXmlOverHttpClient(settings, buildWebUIUrl());
}
/**
* Build the URL of the Vuze XML over HTTP plugin listener from the user settings.
* @return The URL of the RPC API
*/
private String buildWebUIUrl() {
return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + RPC_URL;
}
@SuppressWarnings("unchecked")
private List<Torrent> onTorrentsRetrieved(Log log, Object result) throws DaemonException {
Map<String, Object> response = (Map<String, Object>) result;
// We might have an empty list if no torrents are on the server
if (response == null) {
return new ArrayList<Torrent>();
}
log.d(LOG_NAME, response.toString().length() > 300? response.toString().substring(0, 300) + "... (" + response.toString().length() + " chars)": response.toString());
List<Torrent> torrents = new ArrayList<Torrent>();
// Parse torrent list from Vuze response, which is a map list of ENTRYs
for (String key : response.keySet()) {
/**
* Every Vuze ENTRY is a map of key-value pairs with information, or a key-map pair with that map being a mapping of key-value pairs with information
* VuzeXmlTorrentListResponse.txt in the Transdroid wiki shows a full example response, but it looks something like:
* ENTRY0={
position=1,
torrent_file=/home/erickok/.azureus/torrents/ubuntu.torrent,
name=ubuntu-9.04-desktop-i386.iso,
torrent={
size=732909568,
creation_date=1240473087
}
}
*/
Map<String, Object> info = (Map<String, Object>) response.get(key);
if (info == null || !info.containsKey("_object_id") || info.get("_object_id") == null) {
// No valid XML data object returned
throw new DaemonException(DaemonException.ExceptionType.UnexpectedResponse, "Map of objects returned by Vuze, but these object do not have some <info> attached or no <_object_id> is available");
}
Map<String, Object> torrentinfo = (Map<String, Object>) info.get("torrent");
Map<String, Object> statsinfo = (Map<String, Object>) info.get("stats");
Map<String, Object> scrapeinfo = (Map<String, Object>) info.get("scrape_result");
Map<String, Object> announceinfo = (Map<String, Object>) info.get("announce_result");
int scrapeSeedCount = ((Long) scrapeinfo.get("seed_count")).intValue();
int scrapeNonSeedCount = ((Long) scrapeinfo.get("non_seed_count")).intValue();
String error = (String) info.get("error_state_details");
error = error != null && error.equals("")? null: error;
int announceSeedCount = ((Long) announceinfo.get("seed_count")).intValue();
int announceNonSeedCount = ((Long) announceinfo.get("non_seed_count")).intValue();
int rateDownload = ((Long) statsinfo.get("download_average")).intValue();
Double availability = (Double)statsinfo.get("availability");
Long size = torrentinfo != null? (Long)torrentinfo.get("size"): 0;
torrents.add(new Torrent(
(Long) info.get("_object_id"), // id
info.get("_object_id").toString(), // hash //(String) torrentinfo.get("hash"), // hash
info.get("name").toString().trim(), // name
convertTorrentStatus((Long) info.get("state")), // status
statsinfo.get("target_file_or_dir") + "/", // locationDir
rateDownload, // rateDownload
((Long)statsinfo.get("upload_average")).intValue(), // rateUpload
announceSeedCount, // seedersConnected
scrapeSeedCount, // seedersKnown
announceNonSeedCount, // leechersConnected
scrapeNonSeedCount, // leechersKnown
(rateDownload > 0? (int)((Long)statsinfo.get("remaining") / rateDownload): -1), // eta (bytes left / rate download, if rate > 0)
(Long)statsinfo.get("downloaded"), // downloadedEver
(Long)statsinfo.get("uploaded"), // uploadedEver
size, // totalSize
(float)((Long)statsinfo.get("downloaded")) / (float)(size), // partDone (downloadedEver / totalSize)
Math.min(availability.floatValue(), 1f),
null, // TODO: Implement Vuze label support
new Date((Long) statsinfo.get("time_started")), // dateAdded
null, // Unsupported?
error,
settings.getType()));
}
return torrents;
}
@SuppressWarnings("unchecked")
private List<TorrentFile> onTorrentFilesRetrieved(Object result, Torrent torrent) {
Map<String, Object> response = (Map<String, Object>) result;
// We might have an empty list
if (response == null) {
return new ArrayList<TorrentFile>();
}
//DLog.d(LOG_NAME, response.toString().length() > 300? response.toString().substring(0, 300) + "... (" + response.toString().length() + " chars)": response.toString());
List<TorrentFile> files = new ArrayList<TorrentFile>();
// Parse torrent file list from Vuze response, which is a map list of ENTRYs
for (String key : response.keySet()) {
/**
* Every Vuze ENTRY is a map of key-value pairs with information
* For file lists, it looks something like:
* ENTRY2={
is_deleted=false,
length=298,
downloaded=298,
is_priority=false,
first_piece_number=726,
is_skipped=false,
file=/var/data/Downloads/Some.Torrent/OneFile.txt,
_object_id=443243294889782236,
num_pieces=1,
access_mode=1
}
*/
Map<String, Object> info = (Map<String, Object>) response.get(key);
String file = (String)info.get("file");
files.add(new TorrentFile(
String.valueOf(info.get("_object_id")),
new File(file).getName(), // name
(file.length() > torrent.getLocationDir().length()? file.substring(torrent.getLocationDir().length()): file), // name
file, // fullPath
(Long)info.get("length"), // size
(Long)info.get("downloaded"), // downloaded
convertVuzePriority((String)info.get("is_skipped"), (String)info.get("is_priority")))); // priority
}
return files;
}
private Priority convertVuzePriority(String isSkipped, String isPriority) {
return isSkipped.equals("true")? Priority.Off: (isPriority.equals("true")? Priority.High: Priority.Normal);
}
private TorrentStatus convertTorrentStatus(Long state) {
switch (state.intValue()) {
case 2:
return TorrentStatus.Checking;
case 4:
return TorrentStatus.Downloading;
case 5:
return TorrentStatus.Seeding;
case 7:
return TorrentStatus.Paused;
case 8:
return TorrentStatus.Error;
}
return TorrentStatus.Unknown;
}
@Override
public Daemon getType() {
return settings.getType();
}
@Override
public DaemonSettings getSettings() {
return this.settings;
}
}