/*
* 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.Aria2c;
import android.net.Uri;
import android.text.TextUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.base64.android.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
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.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.PauseTask;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.ResumeTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.HttpHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
/**
* The daemon adapter from the Aria2 torrent client. Documentation available at http://aria2.sourceforge.net/manual/en/html/aria2c.html
* @author erickok
*/
public class Aria2Adapter implements IDaemonAdapter {
private static final String LOG_NAME = "Aria2 daemon";
private DaemonSettings settings;
private DefaultHttpClient httpclient;
public Aria2Adapter(DaemonSettings settings) {
this.settings = settings;
}
@Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try {
JSONArray params = new JSONArray();
switch (task.getMethod()) {
case Retrieve:
// Request all torrents from server
// NOTE Since there is no aria2.tellAll (or something) we have to use batch requests
JSONArray fields =
new JSONArray().put("gid").put("status").put("totalLength").put("completedLength")
.put("uploadLength").put("downloadSpeed").put("uploadSpeed").put("numSeeders")
.put("dir").put("connections").put("errorCode").put("bittorrent").put("files");
JSONObject active = buildRequest("aria2.tellActive", new JSONArray().put(fields));
JSONObject waiting =
buildRequest("aria2.tellWaiting", new JSONArray().put(0).put(9999).put(fields));
JSONObject stopped =
buildRequest("aria2.tellStopped", new JSONArray().put(0).put(9999).put(fields));
params.put(active).put(waiting).put(stopped);
List<Torrent> torrents = new ArrayList<Torrent>();
JSONArray lists = makeRequestForArray(log, params.toString());
for (int i = 0; i < lists.length(); i++) {
torrents.addAll(parseJsonRetrieveTorrents(lists.getJSONObject(i).getJSONArray("result")));
}
return new RetrieveTaskSuccessResult((RetrieveTask) task, torrents, null);
case GetTorrentDetails:
// Request file listing of a torrent
params.put(task.getTargetTorrent().getUniqueID()); // gid
params.put(new JSONArray().put("bittorrent").put("errorCode"));
JSONObject dinfo = makeRequest(log, buildRequest("aria2.tellStatus", params).toString());
return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task,
parseJsonTorrentDetails(dinfo.getJSONObject("result")));
case GetFileList:
// Request file listing of a torrent
params.put(task.getTargetTorrent().getUniqueID()); // torrent_id
JSONObject finfo = makeRequest(log, buildRequest("aria2.getFiles", params).toString());
return new GetFileListTaskSuccessResult((GetFileListTask) task,
parseJsonFileListing(finfo.getJSONArray("result"), task.getTargetTorrent()));
case AddByFile:
// Encode the .torrent file's data
String file = ((AddByFileTask) task).getFile();
InputStream in =
new Base64.InputStream(new FileInputStream(new File(URI.create(file))), Base64.ENCODE);
StringWriter writer = new StringWriter();
int c;
while ((c = in.read()) != -1) {
writer.write(c);
}
in.close();
// Request to add a torrent by local .torrent file
params.put(writer.toString());
makeRequest(log, buildRequest("aria2.addTorrent", params).toString());
return new DaemonTaskSuccessResult(task);
case AddByUrl:
// Request to add a torrent by URL
String url = ((AddByUrlTask) task).getUrl();
params.put(new JSONArray().put(url));
makeRequest(log, buildRequest("aria2.addUri", params).toString());
return new DaemonTaskSuccessResult(task);
case AddByMagnetUrl:
// Request to add a magnet link by URL
String magnet = ((AddByMagnetUrlTask) task).getUrl();
params.put(new JSONArray().put(magnet));
makeRequest(log, buildRequest("aria2.addUri", params).toString());
return new DaemonTaskSuccessResult(task);
case Remove:
// Remove a torrent
RemoveTask removeTask = (RemoveTask) task;
makeRequest(log,
buildRequest(removeTask.includingData() ? "aria2.removeDownloadResult" : "aria2.remove",
params.put(removeTask.getTargetTorrent().getUniqueID())).toString());
return new DaemonTaskSuccessResult(task);
case Pause:
// Pause a torrent
PauseTask pauseTask = (PauseTask) task;
makeRequest(log, buildRequest("aria2.pause", params.put(pauseTask.getTargetTorrent().getUniqueID()))
.toString());
return new DaemonTaskSuccessResult(task);
case PauseAll:
// Resume all torrents
makeRequest(log, buildRequest("aria2.pauseAll", null).toString());
return new DaemonTaskSuccessResult(task);
case Resume:
// Resume a torrent
ResumeTask resumeTask = (ResumeTask) task;
makeRequest(log,
buildRequest("aria2.unpause", params.put(resumeTask.getTargetTorrent().getUniqueID()))
.toString());
return new DaemonTaskSuccessResult(task);
case ResumeAll:
// Resume all torrents
makeRequest(log, buildRequest("aria2.unpauseAll", null).toString());
return new DaemonTaskSuccessResult(task);
case SetTransferRates:
// Request to set the maximum transfer rates
SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
JSONObject options = new JSONObject();
options.put("max-overall-download-limit",
(ratesTask.getDownloadRate() == null ? -1 : ratesTask.getDownloadRate()));
options.put("max-overall-upload-limit",
(ratesTask.getUploadRate() == null ? -1 : ratesTask.getUploadRate()));
makeRequest(log, buildRequest("aria2.changeGlobalOption", params.put(options)).toString());
return new DaemonTaskSuccessResult(task);
default:
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
task.getMethod() + " is not supported by " + getType()));
}
} catch (JSONException e) {
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString()));
} 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.FileAccessError, e.toString()));
}
}
private JSONObject buildRequest(String sendMethod, JSONArray params) throws JSONException {
// Build request for method
if (!TextUtils.isEmpty(settings.getExtraPassword())) {
JSONArray signed = new JSONArray();
// Start with the secret token as parameter and then add the normal parameters
signed.put("token:" + settings.getExtraPassword());
if (params != null) {
for (int i = 0; i < params.length(); i++) {
signed.put(params.get(i));
}
}
params = signed;
}
JSONObject request = new JSONObject();
request.put("id", "transdroid");
request.put("jsonrpc", "2.0");
request.put("method", sendMethod);
request.put("params", params);
return request;
}
private synchronized JSONObject makeRequest(Log log, String data) throws DaemonException {
String raw = makeRawRequest(log, data);
try {
return new JSONObject(raw);
} catch (JSONException e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
}
}
private synchronized JSONArray makeRequestForArray(Log log, String data) throws DaemonException {
String raw = makeRawRequest(log, data);
try {
return new JSONArray(raw);
} catch (JSONException e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
}
}
private synchronized String makeRawRequest(Log log, String data) throws DaemonException {
try {
// Initialise the HTTP client
if (httpclient == null) {
httpclient = HttpHelper.createStandardHttpClient(settings, !TextUtils.isEmpty(settings.getUsername()));
httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor);
httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor);
}
// Set POST URL and data
String url =
(settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() +
(settings.getFolder() == null ? "" : settings.getFolder()) + "/jsonrpc";
HttpPost httppost = new HttpPost(url);
httppost.setEntity(new StringEntity(data));
httppost.setHeader("Content-Type", "application/json");
httppost.setHeader("Accept", "application/json");
// Execute
HttpResponse response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
if (entity == null) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity in response object.");
}
// Read JSON response
InputStream instream = entity.getContent();
String result = HttpHelper.convertStreamToString(instream);
instream.close();
log.d(LOG_NAME, "Success: " +
(result.length() > 300 ? result.substring(0, 300) + "... (" + result.length() + " chars)" :
result));
return result;
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONArray response) throws JSONException, DaemonException {
// Parse response
ArrayList<Torrent> torrents = new ArrayList<Torrent>();
for (int j = 0; j < response.length(); j++) {
// Add the parsed torrent to the list
JSONObject tor = response.getJSONObject(j);
int downloadSpeed = tor.getInt("downloadSpeed");
long totalLength = tor.getLong("totalLength");
long completedLength = tor.getLong("completedLength");
int numSeeders = tor.has("numSeeders") ? tor.getInt("numSeeders") : 0;
TorrentStatus status = convertAriaState(tor.getString("status"), completedLength == totalLength);
int errorCode = tor.optInt("errorCode", 0);
String error = errorCode > 0 ? convertAriaError(errorCode) : null;
String name = null;
JSONObject bittorrent;
if (tor.has("bittorrent")) {
// Get name form the bittorrent info object
bittorrent = tor.getJSONObject("bittorrent");
if (bittorrent.has("info")) {
name = bittorrent.getJSONObject("info").getString("name");
}
} else if (tor.has("files")) {
// Get name from the first included file we can find
JSONArray files = tor.getJSONArray("files");
if (files.length() > 0) {
name = Uri.parse(files.getJSONObject(0).getString("path")).getLastPathSegment();
if (name == null) {
name = files.getJSONObject(0).getString("path");
}
}
}
if (name == null) {
name = tor.getString("gid"); // Fallback name
}
// @formatter:off
torrents.add(new Torrent(
j,
tor.getString("gid"),
name,
status,
tor.getString("dir"),
downloadSpeed,
tor.getInt("uploadSpeed"),
tor.getInt("connections"),
numSeeders ,
tor.getInt("connections"),
numSeeders,
(downloadSpeed > 0? (int) (totalLength / downloadSpeed): -1),
completedLength,
tor.getLong("uploadLength"),
totalLength,
completedLength / (float) totalLength, // Percentage to [0..1]
0f, // Not available
null, // Not available
null, // Not available
null, // Not available
error,
settings.getType()));
// @formatter:on
}
return torrents;
}
private ArrayList<TorrentFile> parseJsonFileListing(JSONArray response, Torrent torrent) throws JSONException {
// Parse response
ArrayList<TorrentFile> files = new ArrayList<TorrentFile>();
for (int j = 0; j < response.length(); j++) {
JSONObject file = response.getJSONObject(j);
// Add the parsed torrent to the list
String rel = file.getString("path");
if (rel.startsWith(torrent.getLocationDir())) {
rel = rel.substring(torrent.getLocationDir().length());
}
// @formatter:off
files.add(new TorrentFile(
Integer.toString(file.getInt("index")),
rel,
rel,
file.getString("path"),
file.getLong("length"),
file.getLong("completedLength"),
file.getBoolean("selected") ? Priority.Normal : Priority.Off));
// @formatter:on
}
return files;
}
private TorrentDetails parseJsonTorrentDetails(JSONObject response) throws JSONException {
// Parse response
List<String> trackers = new ArrayList<String>();
List<String> errors = new ArrayList<String>();
int error = response.optInt("errorCode", 0);
if (error > 0) {
errors.add(convertAriaError(error));
}
if (response.has("bittorrent")) {
JSONObject bittorrent = response.getJSONObject("bittorrent");
JSONArray announceList = bittorrent.getJSONArray("announceList");
for (int i = 0; i < announceList.length(); i++) {
JSONArray announceUrlList = announceList.getJSONArray(i);
for (int j = 0; j < announceUrlList.length(); j++) {
trackers.add(announceUrlList.getString(j));
}
}
}
return new TorrentDetails(trackers, errors);
}
private TorrentStatus convertAriaState(String state, boolean isFinished) {
// Aria2 sends a string as status code
// (http://aria2.sourceforge.net/manual/en/html/aria2c.html#aria2.tellStatus)
if (state.equals("active")) {
return isFinished ? TorrentStatus.Seeding : TorrentStatus.Downloading;
} else if (state.equals("waiting")) {
return TorrentStatus.Queued;
} else if (state.equals("paused") || state.equals("complete")) {
return TorrentStatus.Paused;
} else if (state.equals("error")) {
return TorrentStatus.Error;
} else if (state.equals("removed")) {
return TorrentStatus.Checking;
}
return TorrentStatus.Unknown;
}
private String convertAriaError(int errorCode) {
// Aria2 sends an exit code as error (http://aria2.sourceforge.net/manual/en/html/aria2c.html#id1)
String error = "Aria error #" + Integer.toString(errorCode);
switch (errorCode) {
case 3:
case 4:
return error + ": Resource was not found";
case 5:
return error + ": Aborted because download speed was too slow";
case 6:
return error + ": Network problem occurred";
case 8:
return error + ": Remote server did not support resume when resume was required to complete download";
case 9:
return error + ": There was not enough disk space available";
case 11:
case 12:
return error + ": Duplicate file or info hash download";
case 15:
case 16:
return error + ": Aria2 could not create new or open or truncate existing file";
case 17:
case 18:
case 19:
return error + ": File I/O error occurred";
case 20:
case 27:
return error + ": Aria2 could not parse Magnet URI or Metalink document";
case 21:
return error + ": FTP command failed";
case 22:
return error + ": HTTP response header was bad or unexpected";
case 23:
return error + ": Too many redirects occurred";
case 24:
return error + ": HTTP authorization failed";
case 26:
return error + ": \".torrent\" file is corrupted or missing information that aria2 needs";
default:
return error;
}
}
@Override
public Daemon getType() {
return settings.getType();
}
@Override
public DaemonSettings getSettings() {
return this.settings;
}
}