/* * 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.Bitflu; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; 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.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.GetStatsTask; import org.transdroid.daemon.task.GetStatsTaskSuccessResult; import org.transdroid.daemon.task.RemoveTask; import org.transdroid.daemon.task.RetrieveTask; import org.transdroid.daemon.task.RetrieveTaskSuccessResult; import org.transdroid.daemon.util.HttpHelper; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; /** * An adapter that allows for easy access to uTorrent torrent data. Communication is handled via authenticated JSON-RPC * HTTP GET requests and responses. * @author adrianulrich */ // TODO: TransferRates support public class BitfluAdapter implements IDaemonAdapter { private static final String LOG_NAME = "Bitflu daemon"; private static final String JSON_ROOT = "Bitflu"; private static final String RPC_TORRENT_LIST = "torrentList"; private static final String RPC_PAUSE_TORRENT = "pause/"; private static final String RPC_RESUME_TORRENT = "resume/"; private static final String RPC_CANCEL_TORRENT = "cancel/"; private static final String RPC_REMOVE_TORRENT = "wipe/"; private static final String RPC_TORRENT_FILES = "showfiles-ext/"; private static final String RPC_START_DOWNLOAD = "startdownload/"; private DaemonSettings settings; private DefaultHttpClient httpclient; /** * Initialises an adapter that provides operations to the Bitflu web interface */ public BitfluAdapter(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 = makeBitfluRequest(log, RPC_TORRENT_LIST); return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonRetrieveTorrents(result.getJSONArray(JSON_ROOT)), null); case GetStats: return new GetStatsTaskSuccessResult((GetStatsTask) task, false, -1); case Pause: makeBitfluRequest(log, RPC_PAUSE_TORRENT + task.getTargetTorrent().getUniqueID()); return new DaemonTaskSuccessResult(task); case Resume: makeBitfluRequest(log, RPC_RESUME_TORRENT + task.getTargetTorrent().getUniqueID()); return new DaemonTaskSuccessResult(task); case Remove: // Remove a torrent RemoveTask removeTask = (RemoveTask) task; String removeUriBase = RPC_CANCEL_TORRENT; if (removeTask.includingData()) { removeUriBase = RPC_REMOVE_TORRENT; } makeBitfluRequest(log, removeUriBase + task.getTargetTorrent().getUniqueID()); return new DaemonTaskSuccessResult(task); case GetFileList: JSONObject jfiles = makeBitfluRequest(log, RPC_TORRENT_FILES + task.getTargetTorrent().getUniqueID()); return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonShowFilesTorrent(jfiles.getJSONArray(JSON_ROOT))); case AddByUrl: String url = URLEncoder.encode(((AddByUrlTask) task).getUrl(), "UTF-8"); makeBitfluRequest(log, RPC_START_DOWNLOAD + url); return new DaemonTaskSuccessResult(task); case AddByMagnetUrl: String magnet = URLEncoder.encode(((AddByMagnetUrlTask) task).getUrl(), "UTF-8"); makeBitfluRequest(log, RPC_START_DOWNLOAD + magnet); 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 (UnsupportedEncodingException e) { return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, e.toString())); } } private JSONObject makeBitfluRequest(Log log, String addToUrl) throws DaemonException { try { // Initialise the HTTP client if (httpclient == null) { initialise(); } // TLog.d(LOG_NAME, "Request to: "+ buildWebUIUrl() + addToUrl); // Make request HttpGet httpget = new HttpGet(buildWebUIUrl() + addToUrl); HttpResponse response = httpclient.execute(httpget); // Read JSON response InputStream instream = response.getEntity().getContent(); String result = HttpHelper.convertStreamToString(instream); int httpstatus = response.getStatusLine().getStatusCode(); if (httpstatus != 200) { throw new DaemonException(ExceptionType.UnexpectedResponse, "Invalid reply from server, http status code: " + httpstatus); } if (result.equals("")) { // Empty responses are ok: add fake json content result = "empty_response"; } JSONObject json = new JSONObject("{ \"" + JSON_ROOT + "\" : " + result + "}"); instream.close(); return json; } catch (DaemonException e) { throw e; } catch (JSONException e) { log.d(LOG_NAME, "Error: " + e.toString()); throw new DaemonException(ExceptionType.ParsingFailed, e.toString()); } catch (Exception e) { log.d(LOG_NAME, "Error: " + e.toString()); throw new DaemonException(ExceptionType.ConnectionError, e.toString()); } } private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONArray results) throws JSONException { ArrayList<Torrent> torrents = new ArrayList<Torrent>(); if (results != null) { for (int i = 0; i < results.length(); i++) { JSONObject tor = results.getJSONObject(i); long done_bytes = tor.getLong("done_bytes"); long total_bytes = tor.getLong("total_bytes"); float percent = ((float) done_bytes / ((float) total_bytes + 1)); // @formatter:off torrents.add(new Torrent(i, tor.getString("key"), tor.getString("name"), convertBitfluStatus(tor), "/" + settings.getOS().getPathSeperator(), tor.getInt("speed_download"), tor.getInt("speed_upload"), tor.getInt("active_clients"), tor.getInt("active_clients"), // Bitflu doesn't distinguish between seeders and leechers tor.getInt("clients"), tor.getInt("clients"), // Bitflu doesn't distinguish between seeders and leechers tor.getInt("eta"), done_bytes, tor.getLong("uploaded_bytes"), total_bytes, percent, // Percentage to [0..1] 0f, // Not available null, // label null, // Not available null, // Not available null, // Not available settings.getType())); // @formatter:on } } // Return the list return torrents; } private ArrayList<TorrentFile> parseJsonShowFilesTorrent(JSONArray response) throws JSONException { ArrayList<TorrentFile> files = new ArrayList<TorrentFile>(); if (response != null) { for (int i = 0; i < response.length(); i++) { JSONObject finfo = response.getJSONObject(i); long done_bytes = finfo.getLong("done") * finfo.getLong("chunksize"); long file_size = finfo.getLong("size"); if (done_bytes > file_size) { /* Shared chunk */ done_bytes = file_size; } // @formatter:off files.add(new TorrentFile( "" + i, finfo.getString("name"), finfo.getString("path"), null, // hmm.. can we have something without file:// ?! file_size, done_bytes, Priority.Normal )); // @formatter:on } } return files; } private TorrentStatus convertBitfluStatus(JSONObject obj) throws JSONException { if (obj.getInt("paused") != 0) { return TorrentStatus.Paused; } else if (obj.getLong("done_bytes") == obj.getLong("total_bytes")) { return TorrentStatus.Seeding; } return TorrentStatus.Downloading; } /** * Instantiates an HTTP client with proper credentials that can be used for all HTTP 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() { String webuiroot = ""; if (settings.getFolder() != null) { webuiroot = settings.getFolder(); } return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + webuiroot + "/"; } @Override public Daemon getType() { return settings.getType(); } @Override public DaemonSettings getSettings() { return this.settings; } }