/*
* 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.DLinkRouterBT;
import com.android.internalcopy.http.multipart.FilePart;
import com.android.internalcopy.http.multipart.MultipartEntity;
import com.android.internalcopy.http.multipart.Part;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
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.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.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.util.HttpHelper;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
/**
* The daemon adapter for the DLink Router Bittorrent client.
* @author AvengerMoJo <avengermojo at gmail.com>
*/
public class DLinkRouterBTAdapter implements IDaemonAdapter {
private static final String LOG_NAME = "DLinkRouterBT adapter";
private static final String PATH_TO_API = "/api/";
private static final String SESSION_HEADER = "X-Session-Id";
private static final String JSON_TORRENTS = "torrents";
private static final String API_GET = "torrents-get";
private static final String API_ADD = "torrent-add-url?start=yes&url=";
private static final String API_ADD_BY_FILE = "torrent-add?start=yes";
private static final String API_REMOVE = "torrent-remove?delete-torrent=yes&hash=";
private static final String API_DEL_DATA = "&delete-data=";
private static final String API_STOP = "torrent-stop?hash=";
private static final String API_START = "torrent-start?hash=";
private static final String BT_ADD_BY_FILE = "fileEl";
private static final String BT_CAPTION = "caption";
private static final String BT_COPYS = "distributed_copies";
private static final String BT_DOWNLOAD_RATE = "dl_rate";
private static final String BT_DONE = "done";
private static final String BT_HASH = "hash";
// private static final String BT_MAX_CONNECTED = "max_connections";
// private static final String BT_MAX_DOWNLOAD_RATE = "max_dl_rate";
// private static final String BT_MAX_UPLOAD_RATE = "max_ul_rate";
// private static final String BT_MAX_UPLOAD_CONNECTIONS = "max_uploads";
// private static final String BT_PAYLOAD_DOWNLOAD = "payload_download";
private static final String BT_PAYLOAD_UPLOAD = "payload_upload";
private static final String BT_PEERS_CONNECTED = "peers_connected";
private static final String BT_PEERS_TOTAL = "peers_total";
// private static final String BT_PRIVATE = "private";
private static final String BT_SEEDS_CONNECTED = "seeds_connected";
private static final String BT_SEEDS_TOTAL = "seeds_total";
private static final String BT_SIZE = "size";
private static final String BT_STATE = "state";
private static final String BT_STOPPED = "stopped";
private static final String BT_UPLOAD_RATE = "ul_rate";
private static final String API_GET_FILES = "torrent-get-files?hash=";
private static final String BT_FILE_DONE = "done";
private static final String BT_FILE_NAME = "name";
private static final String BT_FILE_SIZE = "size";
private static final String BT_FILE_PRIORITY = "pri";
private DaemonSettings settings;
private DefaultHttpClient httpclient;
private String sessionToken;
public DLinkRouterBTAdapter(DaemonSettings settings) {
this.settings = settings;
}
@Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try {
switch (task.getMethod()) {
case Retrieve:
// Request all torrents from server
JSONObject result = makeRequest(log, API_GET);
return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonRetrieveTorrents(result), null);
case GetFileList:
// Request all details for a specific torrent
JSONObject result2 = makeRequest(log, API_GET_FILES + task.getTargetTorrent().getUniqueID());
return new GetFileListTaskSuccessResult((GetFileListTask) task,
parseJsonFileList(result2, task.getTargetTorrent().getUniqueID()));
case AddByFile:
// Add a torrent to the server by sending the contents of a local .torrent file
String file = ((AddByFileTask) task).getFile();
// put .torrent file's data into the request
makeRequest(log, API_ADD_BY_FILE, new File(URI.create(file)));
return new DaemonTaskSuccessResult(task);
case AddByUrl:
// Request to add a torrent by URL
String url = ((AddByUrlTask) task).getUrl();
makeRequest(log, API_ADD + url);
return new DaemonTaskSuccessResult(task);
case Remove:
// Remove a torrent
RemoveTask removeTask = (RemoveTask) task;
makeRequest(log, API_REMOVE + removeTask.getTargetTorrent().getUniqueID() +
(removeTask.includingData() ? API_DEL_DATA + "yes" : ""), false);
return new DaemonTaskSuccessResult(task);
// case Stop:
case Pause:
// Pause a torrent
PauseTask pauseTask = (PauseTask) task;
makeRequest(log, API_STOP + pauseTask.getTargetTorrent().getUniqueID(), false);
return new DaemonTaskSuccessResult(task);
// case PauseAll:
// Resume all torrents
// makeRequest(buildRequestObject(RPC_METHOD_PAUSE, buildTorrentRequestObject(FOR_ALL, null,
// false)));
// return new DaemonTaskSuccessResult(task);
// case Start:
case Resume:
// Resume a torrent
ResumeTask resumeTask = (ResumeTask) task;
makeRequest(log, API_START + resumeTask.getTargetTorrent().getUniqueID(), false);
return new DaemonTaskSuccessResult(task);
// case ResumeAll:
// Resume all torrents
// makeRequest(buildRequestObject(RPC_METHOD_RESUME, buildTorrentRequestObject(FOR_ALL, null,
// false)));
// return new DaemonTaskSuccessResult(task);
// case SetTransferRates:
// Request to set the maximum transfer rates
// SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
// if (ratesTask.getUploadRate() == null) {
// request.put(RPC_SESSION_LIMITUPE, false);
// } else {
// request.put(RPC_SESSION_LIMITUPE, true);
// request.put(RPC_SESSION_LIMITUP, ratesTask.getUploadRate().intValue());
// }
// if (ratesTask.getDownloadRate() == null) {
// request.put(RPC_SESSION_LIMITDOWNE, false);
// } else {
// request.put(RPC_SESSION_LIMITDOWNE, true);
// request.put(RPC_SESSION_LIMITDOWN, ratesTask.getDownloadRate().intValue());
// }
// makeRequest( RPC_METHOD_SESSIONSET );
// 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);
}
}
private JSONObject makeRequest(Log log, String requestUrl, File upload) throws DaemonException {
return makeRequest(log, requestUrl, false, upload);
}
private JSONObject makeRequest(Log log, String requestUrl) throws DaemonException {
return makeRequest(log, requestUrl, true, null);
}
private JSONObject makeRequest(Log log, String requestUrl, boolean hasRespond) throws DaemonException {
return makeRequest(log, requestUrl, hasRespond, null);
}
private JSONObject makeRequest(Log log, String requestUrl, boolean hasRespond, File upload) throws DaemonException {
try {
// Initialise the HTTP client
if (httpclient == null) {
initialise();
}
// Setup request using POST stream with URL and data
HttpPost httppost = new HttpPost(buildWebUIUrl() + requestUrl);
if (upload != null) {
Part[] parts = {new FilePart(BT_ADD_BY_FILE, upload)};
httppost.setEntity(new MultipartEntity(parts, httppost.getParams()));
}
// Send the stored session token as a header
if (sessionToken != null) {
httppost.addHeader(SESSION_HEADER, sessionToken);
}
// Execute
HttpResponse response = httpclient.execute(httppost);
// 409 error because of a session id?
if (response.getStatusLine().getStatusCode() == 409) {
// Retry post, but this time with the new session token that was encapsulated in the 409
// response
sessionToken = response.getFirstHeader(SESSION_HEADER).getValue();
httppost.addHeader(SESSION_HEADER, sessionToken);
response = httpclient.execute(httppost);
}
if (!hasRespond) {
return null;
}
HttpEntity entity = response.getEntity();
if (entity != null) {
// Read JSON response
java.io.InputStream instream = entity.getContent();
String result = HttpHelper.convertStreamToString(instream);
JSONObject json = new JSONObject(result);
instream.close();
log.d(LOG_NAME, "Success: " +
(result.length() > 300 ? result.substring(0, 300) + "... (" + result.length() + " chars)" :
result));
// Return the JSON object
return json;
}
log.d(LOG_NAME, "Error: No entity in HTTP response");
throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response.");
} catch (DaemonException e) {
throw e;
} catch (JSONException e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
/**
* Instantiates an HTTP client with proper credentials that can be used for all Transmission requests.
* @throws DaemonException On conflicting or missing settings
*/
private void initialise() throws DaemonException {
httpclient = HttpHelper.createStandardHttpClient(settings, true);
}
/**
* Build the URL of the Transmission web UI from the user settings.
* @return The URL of the RPC API
*/
private String buildWebUIUrl() {
return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() +
PATH_TO_API;
}
private TorrentStatus convertStatus(String state) {
if ("allocating".equals(state)) {
return TorrentStatus.Checking;
}
if ("seeding".equals(state)) {
return TorrentStatus.Seeding;
}
if ("finished".equals(state)) {
return TorrentStatus.Downloading;
}
if ("connecting_to_tracker".equals(state)) {
return TorrentStatus.Checking;
}
if ("queued_for_checking".equals(state)) {
return TorrentStatus.Queued;
}
if ("downloading".equals(state)) {
return TorrentStatus.Downloading;
}
return TorrentStatus.Unknown;
}
private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONObject response) throws JSONException {
// Parse response
ArrayList<Torrent> torrents = new ArrayList<Torrent>();
JSONArray rarray = response.getJSONArray(JSON_TORRENTS);
for (int i = 0; i < rarray.length(); i++) {
JSONObject tor = rarray.getJSONObject(i);
// Add the parsed torrent to the list
TorrentStatus status;
if (tor.getInt(BT_STOPPED) == 1) {
status = TorrentStatus.Paused;
} else {
status = convertStatus(tor.getString(BT_STATE));
}
int eta = (int) ((tor.getLong(BT_SIZE) - tor.getLong(BT_DONE)) / (tor.getInt(BT_DOWNLOAD_RATE) + 1));
if (0 > eta) {
eta = -1;
}
// @formatter:off
Torrent new_t = new Torrent(
i,
tor.getString(BT_HASH),
tor.getString(BT_CAPTION),
status,
null, // Not supported?
tor.getInt(BT_DOWNLOAD_RATE),
tor.getInt(BT_UPLOAD_RATE),
tor.getInt(BT_PEERS_CONNECTED),
tor.getInt(BT_PEERS_TOTAL),
tor.getInt(BT_SEEDS_CONNECTED),
tor.getInt(BT_SEEDS_TOTAL),
eta,
tor.getLong(BT_DONE),
tor.getLong(BT_PAYLOAD_UPLOAD),
tor.getLong(BT_SIZE),
tor.getLong(BT_DONE) / (float) tor.getLong(BT_SIZE),
Float.parseFloat(tor.getString(BT_COPYS)),
null,
null,
null,
null,
settings.getType());
// @formatter:on
torrents.add(new_t);
}
// Return the list
return torrents;
}
private ArrayList<TorrentFile> parseJsonFileList(JSONObject response, String hash) throws JSONException {
// Parse response
ArrayList<TorrentFile> torrentfiles = new ArrayList<TorrentFile>();
JSONObject jobj = response.getJSONObject(JSON_TORRENTS);
if (jobj != null) {
JSONArray files = jobj.getJSONArray(hash); // "Hash id"
for (int i = 0; i < files.length(); i++) {
JSONObject file = files.getJSONObject(i);
// @formatter:off
torrentfiles.add(new TorrentFile(
String.valueOf(i),
file.getString(BT_FILE_NAME),
file.getString(BT_FILE_NAME),
null, // Not supported?
file.getLong(BT_FILE_SIZE),
file.getLong(BT_FILE_DONE),
convertTransmissionPriority(file.getInt(BT_FILE_PRIORITY))));
// @formatter:on
}
}
// Return the list
return torrentfiles;
}
private Priority convertTransmissionPriority(int priority) {
switch (priority) {
case 1:
return Priority.High;
case -1:
return Priority.Low;
default:
return Priority.Normal;
}
}
@Override
public Daemon getType() {
return settings.getType();
}
@Override
public DaemonSettings getSettings() {
return this.settings;
}
}