Eric Kok
10 years ago
10 changed files with 546 additions and 21 deletions
@ -0,0 +1,502 @@
@@ -0,0 +1,502 @@
|
||||
/* |
||||
* 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 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; |
||||
|
||||
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.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.DLog; |
||||
import org.transdroid.daemon.util.HttpHelper; |
||||
|
||||
import android.net.Uri; |
||||
import android.text.TextUtils; |
||||
|
||||
/** |
||||
* 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(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(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(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(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(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(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(buildRequest("aria2.addUri", params).toString()); |
||||
return new DaemonTaskSuccessResult(task); |
||||
|
||||
case Remove: |
||||
|
||||
// Remove a torrent
|
||||
RemoveTask removeTask = (RemoveTask) task; |
||||
makeRequest(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(buildRequest("aria2.pause", params.put(pauseTask.getTargetTorrent().getUniqueID())) |
||||
.toString()); |
||||
return new DaemonTaskSuccessResult(task); |
||||
|
||||
case PauseAll: |
||||
|
||||
// Resume all torrents
|
||||
makeRequest(buildRequest("aria2.pauseAll", null).toString()); |
||||
return new DaemonTaskSuccessResult(task); |
||||
|
||||
case Resume: |
||||
|
||||
// Resume a torrent
|
||||
ResumeTask resumeTask = (ResumeTask) task; |
||||
makeRequest(buildRequest("aria2.unpause", params.put(resumeTask.getTargetTorrent().getUniqueID())) |
||||
.toString()); |
||||
return new DaemonTaskSuccessResult(task); |
||||
|
||||
case ResumeAll: |
||||
|
||||
// Resume all torrents
|
||||
makeRequest(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().intValue())); |
||||
options.put("max-overall-upload-limit", (ratesTask.getUploadRate() == null ? -1 : ratesTask |
||||
.getUploadRate().intValue())); |
||||
|
||||
makeRequest(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()); |
||||
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(String data) throws DaemonException { |
||||
String raw = makeRawRequest(data); |
||||
try { |
||||
return new JSONObject(raw); |
||||
} catch (JSONException e) { |
||||
DLog.d(LOG_NAME, "Error: " + e.toString()); |
||||
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString()); |
||||
} |
||||
} |
||||
|
||||
private synchronized JSONArray makeRequestForArray(String data) throws DaemonException { |
||||
String raw = makeRawRequest(data); |
||||
try { |
||||
return new JSONArray(raw); |
||||
} catch (JSONException e) { |
||||
DLog.d(LOG_NAME, "Error: " + e.toString()); |
||||
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString()); |
||||
} |
||||
} |
||||
|
||||
private synchronized String makeRawRequest(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(); |
||||
|
||||
DLog.d(LOG_NAME, |
||||
"Success: " |
||||
+ (result.length() > 300 ? result.substring(0, 300) + "... (" + result.length() + " chars)" |
||||
: result)); |
||||
return result; |
||||
|
||||
} catch (Exception e) { |
||||
DLog.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 = null; |
||||
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 = 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
|
||||
// @formatter:off
|
||||
String rel = file.getString("path"); |
||||
if (rel.startsWith(torrent.getLocationDir())) { |
||||
rel = rel.substring(torrent.getLocationDir().length()); |
||||
} |
||||
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; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue