/* * 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.Transmission; 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.base64.android.Base64.InputStream; 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.ForceRecheckTask; 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.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.SetAlternativeModeTask; import org.transdroid.daemon.task.SetDownloadLocationTask; import org.transdroid.daemon.task.SetFilePriorityTask; 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.StringWriter; import java.net.URI; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * The daemon adapter from the Transmission torrent client. * @author erickok */ public class TransmissionAdapter implements IDaemonAdapter { private static final String LOG_NAME = "Transdroid daemon"; private static final int FOR_ALL = -1; private static final String RPC_ID = "id"; private static final String RPC_NAME = "name"; private static final String RPC_STATUS = "status"; private static final String RPC_ERROR = "error"; private static final String RPC_ERRORSTRING = "errorString"; private static final String RPC_DOWNLOADDIR = "downloadDir"; private static final String RPC_RATEDOWNLOAD = "rateDownload"; private static final String RPC_RATEUPLOAD = "rateUpload"; private static final String RPC_PEERSGETTING = "peersGettingFromUs"; private static final String RPC_PEERSSENDING = "peersSendingToUs"; private static final String RPC_PEERSCONNECTED = "peersConnected"; private static final String RPC_ETA = "eta"; private static final String RPC_DOWNLOADSIZE1 = "haveUnchecked"; private static final String RPC_DOWNLOADSIZE2 = "haveValid"; private static final String RPC_UPLOADEDEVER = "uploadedEver"; private static final String RPC_TOTALSIZE = "sizeWhenDone"; private static final String RPC_DATEADDED = "addedDate"; private static final String RPC_DATEDONE = "doneDate"; private static final String RPC_AVAILABLE = "desiredAvailable"; private static final String RPC_COMMENT = "comment"; private static final String RPC_FILE_NAME = "name"; private static final String RPC_FILE_LENGTH = "length"; private static final String RPC_FILE_COMPLETED = "bytesCompleted"; private static final String RPC_FILESTAT_WANTED = "wanted"; private static final String RPC_FILESTAT_PRIORITY = "priority"; private static String sessionToken; private DaemonSettings settings; private DefaultHttpClient httpclient; private long rpcVersion = -1; public TransmissionAdapter(DaemonSettings settings) { this.settings = settings; } @Override public DaemonTaskResult executeTask(Log log, DaemonTask task) { try { // Get the server version if (rpcVersion <= -1) { // Get server session statistics JSONObject response = makeRequest(log, buildRequestObject("session-get", new JSONObject())); rpcVersion = response.getJSONObject("arguments").getInt("rpc-version"); } JSONObject request = new JSONObject(); switch (task.getMethod()) { case Retrieve: // Request all torrents from server JSONArray fields = new JSONArray(); final String[] fieldsArray = new String[]{RPC_ID, RPC_NAME, RPC_ERROR, RPC_ERRORSTRING, RPC_STATUS, RPC_DOWNLOADDIR, RPC_RATEDOWNLOAD, RPC_RATEUPLOAD, RPC_PEERSGETTING, RPC_PEERSSENDING, RPC_PEERSCONNECTED, RPC_ETA, RPC_DOWNLOADSIZE1, RPC_DOWNLOADSIZE2, RPC_UPLOADEDEVER, RPC_TOTALSIZE, RPC_DATEADDED, RPC_DATEDONE, RPC_AVAILABLE, RPC_COMMENT}; for (String field : fieldsArray) { fields.put(field); } request.put("fields", fields); JSONObject result = makeRequest(log, buildRequestObject("torrent-get", request)); return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonRetrieveTorrents(result.getJSONObject("arguments")), null); case GetStats: // Request the current server statistics JSONObject stats = makeRequest(log, buildRequestObject("session-get", new JSONObject())) .getJSONObject("arguments"); return new GetStatsTaskSuccessResult((GetStatsTask) task, stats.getBoolean("alt-speed-enabled"), rpcVersion >= 12 ? stats.getLong("download-dir-free-space") : -1); case GetTorrentDetails: // Request fine details of a specific torrent JSONArray dfields = new JSONArray(); dfields.put("trackers"); dfields.put("trackerStats"); JSONObject buildDGet = buildTorrentRequestObject(task.getTargetTorrent().getUniqueID(), null, false); buildDGet.put("fields", dfields); JSONObject getDResult = makeRequest(log, buildRequestObject("torrent-get", buildDGet)); return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(getDResult.getJSONObject("arguments"))); case GetFileList: // Request all details for a specific torrent JSONArray ffields = new JSONArray(); ffields.put("files"); ffields.put("fileStats"); JSONObject buildGet = buildTorrentRequestObject(task.getTargetTorrent().getUniqueID(), null, false); buildGet.put("fields", ffields); JSONObject getResult = makeRequest(log, buildRequestObject("torrent-get", buildGet)); return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFileList(getResult.getJSONObject("arguments"), task.getTargetTorrent())); case AddByFile: // Add a torrent to the server by sending the contents of a local .torrent file String file = ((AddByFileTask) task).getFile(); // Encode the .torrent file's data 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 Base64-encoded meta data request.put("metainfo", writer.toString()); makeRequest(log, buildRequestObject("torrent-add", request)); return new DaemonTaskSuccessResult(task); case AddByUrl: // Request to add a torrent by URL String url = ((AddByUrlTask) task).getUrl(); request.put("filename", url); makeRequest(log, buildRequestObject("torrent-add", request)); return new DaemonTaskSuccessResult(task); case AddByMagnetUrl: // Request to add a magnet link by URL String magnet = ((AddByMagnetUrlTask) task).getUrl(); request.put("filename", magnet); makeRequest(log, buildRequestObject("torrent-add", request)); return new DaemonTaskSuccessResult(task); case Remove: // Remove a torrent RemoveTask removeTask = (RemoveTask) task; makeRequest(log, buildRequestObject("torrent-remove", buildTorrentRequestObject(removeTask.getTargetTorrent().getUniqueID(), "delete-local-data", removeTask.includingData()))); return new DaemonTaskSuccessResult(task); case Pause: // Pause a torrent PauseTask pauseTask = (PauseTask) task; makeRequest(log, buildRequestObject("torrent-stop", buildTorrentRequestObject(pauseTask.getTargetTorrent().getUniqueID(), null, false))); return new DaemonTaskSuccessResult(task); case PauseAll: // Resume all torrents makeRequest(log, buildRequestObject("torrent-stop", buildTorrentRequestObject(FOR_ALL, null, false))); return new DaemonTaskSuccessResult(task); case Resume: // Resume a torrent ResumeTask resumeTask = (ResumeTask) task; makeRequest(log, buildRequestObject("torrent-start", buildTorrentRequestObject(resumeTask.getTargetTorrent().getUniqueID(), null, false))); return new DaemonTaskSuccessResult(task); case ResumeAll: // Resume all torrents makeRequest(log, buildRequestObject("torrent-start", buildTorrentRequestObject(FOR_ALL, null, false))); return new DaemonTaskSuccessResult(task); case SetDownloadLocation: // Change the download location SetDownloadLocationTask sdlTask = (SetDownloadLocationTask) task; // Build request JSONObject sdlrequest = new JSONObject(); JSONArray sdlids = new JSONArray(); sdlids.put(Long.parseLong(task.getTargetTorrent().getUniqueID())); sdlrequest.put("ids", sdlids); sdlrequest.put("location", sdlTask.getNewLocation()); sdlrequest.put("move", true); makeRequest(log, buildRequestObject("torrent-set-location", sdlrequest)); return new DaemonTaskSuccessResult(task); case SetFilePriorities: // Set priorities of the files of some torrent SetFilePriorityTask prioTask = (SetFilePriorityTask) task; // Build request JSONObject prequest = new JSONObject(); JSONArray ids = new JSONArray(); ids.put(Long.parseLong(task.getTargetTorrent().getUniqueID())); prequest.put("ids", ids); JSONArray fileids = new JSONArray(); for (TorrentFile forfile : prioTask.getForFiles()) { fileids.put(Integer.parseInt( forfile.getKey())); // The keys are the indices of the files, so always numeric } switch (prioTask.getNewPriority()) { case Off: prequest.put("files-unwanted", fileids); break; case Low: prequest.put("files-wanted", fileids); prequest.put("priority-low", fileids); break; case Normal: prequest.put("files-wanted", fileids); prequest.put("priority-normal", fileids); break; case High: prequest.put("files-wanted", fileids); prequest.put("priority-high", fileids); break; } makeRequest(log, buildRequestObject("torrent-set", prequest)); return new DaemonTaskSuccessResult(task); case SetTransferRates: // Request to set the maximum transfer rates SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; if (ratesTask.getUploadRate() == null) { request.put("speed-limit-up-enabled", false); } else { request.put("speed-limit-up-enabled", true); request.put("speed-limit-up", ratesTask.getUploadRate().intValue()); } if (ratesTask.getDownloadRate() == null) { request.put("speed-limit-down-enabled", false); } else { request.put("speed-limit-down-enabled", true); request.put("speed-limit-down", ratesTask.getDownloadRate().intValue()); } makeRequest(log, buildRequestObject("session-set", request)); return new DaemonTaskSuccessResult(task); case SetAlternativeMode: // Request to set the alternative speed mode (Tutle Mode) SetAlternativeModeTask altModeTask = (SetAlternativeModeTask) task; request.put("alt-speed-enabled", altModeTask.isAlternativeModeEnabled()); makeRequest(log, buildRequestObject("session-set", request)); return new DaemonTaskSuccessResult(task); case ForceRecheck: // Verify torrent data integrity ForceRecheckTask verifyTask = (ForceRecheckTask) task; makeRequest(log, buildRequestObject("torrent-verify", buildTorrentRequestObject(verifyTask.getTargetTorrent().getUniqueID(), null, false))); 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 buildTorrentRequestObject(String torrentID, String extraKey, boolean extraValue) throws JSONException { return buildTorrentRequestObject(Long.parseLong(torrentID), extraKey, extraValue); } private JSONObject buildTorrentRequestObject(long torrentID, String extraKey, boolean extraValue) throws JSONException { // Build request for one specific torrent JSONObject request = new JSONObject(); if (torrentID != FOR_ALL) { JSONArray ids = new JSONArray(); ids.put(torrentID); // The only id to add request.put("ids", ids); } if (extraKey != null) { request.put(extraKey, extraValue); } return request; } private JSONObject buildRequestObject(String sendMethod, JSONObject arguments) throws JSONException { // Build request for method JSONObject request = new JSONObject(); request.put("method", sendMethod); request.put("arguments", arguments); request.put("tag", 0); return request; } private synchronized JSONObject makeRequest(Log log, JSONObject data) throws DaemonException { try { // Initialise the HTTP client if (httpclient == null) { initialise(); } final String sessionHeader = "X-Transmission-Session-Id"; // Setup request using POST stream with URL and data HttpPost httppost = new HttpPost(buildWebUIUrl()); StringEntity se = new StringEntity(data.toString(), "UTF-8"); httppost.setEntity(se); // Send the stored session token as a header if (sessionToken != null) { httppost.addHeader(sessionHeader, sessionToken); } // Execute log.d(LOG_NAME, "Execute " + data.getString("method") + " request to " + httppost.getURI().toString()); HttpResponse response = httpclient.execute(httppost); // Authentication error? if (response.getStatusLine().getStatusCode() == 401) { throw new DaemonException(ExceptionType.AuthenticationFailure, "401 HTTP response (username or password incorrect)"); } // 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 log.d(LOG_NAME, "Receive HTTP 409 with new session code; now try again for the actual request"); sessionToken = response.getFirstHeader(sessionHeader).getValue(); httppost.addHeader(sessionHeader, sessionToken); log.d(LOG_NAME, "Retry to execute " + data.getString("method") + " request, now with " + sessionHeader + ": " + sessionToken); response = httpclient.execute(httppost); } HttpEntity entity = response.getEntity(); if (entity != null) { // Read JSON response java.io.InputStream instream = entity.getContent(); String result = HttpHelper.convertStreamToString(instream); log.d(LOG_NAME, "Received content response starting with " + (result.length() > 100 ? result.substring(0, 100) + "..." : result)); JSONObject json = new JSONObject(result); instream.close(); // 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.ParsingFailed, 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() { String folder = "/transmission"; if (settings.getFolder() != null && !settings.getFolder().trim().equals("")) { // Allow the user's folder setting to override /transmission (as per Transmission's rpc-url option) folder = settings.getFolder().trim(); // Strip any trailing slashes if (folder.endsWith("/")) { folder = folder.substring(0, folder.length() - 1); } } return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + folder + "/rpc"; } private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONObject response) throws JSONException { // Parse response ArrayList<Torrent> torrents = new ArrayList<Torrent>(); JSONArray rarray = response.getJSONArray("torrents"); for (int i = 0; i < rarray.length(); i++) { JSONObject tor = rarray.getJSONObject(i); // Add the parsed torrent to the list float have = (float) (tor.getLong(RPC_DOWNLOADSIZE1) + tor.getLong(RPC_DOWNLOADSIZE2)); long total = tor.getLong(RPC_TOTALSIZE); // Error is a number, see https://trac.transmissionbt.com/browser/trunk/libtransmission/transmission.h#L1747 // We only consider it a real error if it is local (blocking), which is error code 3 boolean hasError = tor.getInt(RPC_ERROR) == 3; String errorString = tor.getString(RPC_ERRORSTRING).trim(); String commentString = tor.getString(RPC_COMMENT).trim(); if (!commentString.equals("")) { errorString = errorString.equals("") ? commentString : errorString + "\n" + commentString; } String locationDir = tor.getString(RPC_DOWNLOADDIR); if (!locationDir.endsWith(settings.getOS().getPathSeperator())) { locationDir += settings.getOS().getPathSeperator(); } // @formatter:off torrents.add(new Torrent( tor.getInt(RPC_ID), null, tor.getString(RPC_NAME), hasError ? TorrentStatus.Error : getStatus(tor.getInt(RPC_STATUS)), locationDir, tor.getInt(RPC_RATEDOWNLOAD), tor.getInt(RPC_RATEUPLOAD), tor.getInt(RPC_PEERSSENDING), tor.getInt(RPC_PEERSCONNECTED), tor.getInt(RPC_PEERSGETTING), tor.getInt(RPC_PEERSCONNECTED), tor.getInt(RPC_ETA), tor.getLong(RPC_DOWNLOADSIZE1) + tor.getLong(RPC_DOWNLOADSIZE2), tor.getLong(RPC_UPLOADEDEVER), tor.getLong(RPC_TOTALSIZE), //(float) tor.getDouble(RPC_PERCENTDONE), (total == 0 ? 0 : have / (float) total), (total == 0 ? 0 : (have + (float) tor.getLong(RPC_AVAILABLE)) / (float) total), // No label/category/group support in the RPC API for now null, new Date(tor.getLong(RPC_DATEADDED) * 1000L), new Date(tor.getLong(RPC_DATEDONE) * 1000L), errorString, settings.getType())); // @formatter:on } // Return the list return torrents; } private TorrentStatus getStatus(int status) { if (rpcVersion <= -1) { return TorrentStatus.Unknown; } else if (rpcVersion >= 14) { switch (status) { case 0: return TorrentStatus.Paused; case 1: return TorrentStatus.Waiting; case 2: return TorrentStatus.Checking; case 3: return TorrentStatus.Queued; case 4: return TorrentStatus.Downloading; case 5: return TorrentStatus.Queued; case 6: return TorrentStatus.Seeding; } return TorrentStatus.Unknown; } else { return TorrentStatus.getStatus(status); } } private ArrayList<TorrentFile> parseJsonFileList(JSONObject response, Torrent torrent) throws JSONException { // Parse response ArrayList<TorrentFile> torrentfiles = new ArrayList<TorrentFile>(); JSONArray rarray = response.getJSONArray("torrents"); if (rarray.length() > 0) { JSONArray files = rarray.getJSONObject(0).getJSONArray("files"); JSONArray fileStats = rarray.getJSONObject(0).getJSONArray("fileStats"); for (int i = 0; i < files.length(); i++) { JSONObject file = files.getJSONObject(i); JSONObject stat = fileStats.getJSONObject(i); // @formatter:off torrentfiles.add(new TorrentFile( String.valueOf(i), file.getString(RPC_FILE_NAME), file.getString(RPC_FILE_NAME), torrent.getLocationDir() + file.getString(RPC_FILE_NAME), file.getLong(RPC_FILE_LENGTH), file.getLong(RPC_FILE_COMPLETED), convertTransmissionPriority(stat.getBoolean(RPC_FILESTAT_WANTED), stat.getInt(RPC_FILESTAT_PRIORITY)))); // @formatter:on } } // Return the list return torrentfiles; } private Priority convertTransmissionPriority(boolean isWanted, int priority) { if (!isWanted) { return Priority.Off; } else { switch (priority) { case 1: return Priority.High; case -1: return Priority.Low; default: return Priority.Normal; } } } private TorrentDetails parseJsonTorrentDetails(JSONObject response) throws JSONException { // Parse response // NOTE: Assumes only details for one torrent are requested at a time JSONArray rarray = response.getJSONArray("torrents"); if (rarray.length() > 0) { JSONArray trackersList = rarray.getJSONObject(0).getJSONArray("trackers"); List<String> trackers = new ArrayList<String>(); for (int i = 0; i < trackersList.length(); i++) { trackers.add(trackersList.getJSONObject(i).getString("announce")); } JSONArray trackerStatsList = rarray.getJSONObject(0).getJSONArray("trackerStats"); List<String> errors = new ArrayList<String>(); for (int i = 0; i < trackerStatsList.length(); i++) { // Get the tracker response and if it was an error then add it String lar = trackerStatsList.getJSONObject(i).getString("lastAnnounceResult"); if (lar != null && !lar.equals("") && !lar.equals("Success")) { errors.add(lar); } } return new TorrentDetails(trackers, errors); } return null; } @Override public Daemon getType() { return settings.getType(); } @Override public DaemonSettings getSettings() { return this.settings; } }