diff --git a/.hgignore b/.hgignore
index 9f9b6982..9bb46c43 100644
--- a/.hgignore
+++ b/.hgignore
@@ -7,3 +7,5 @@ bin/
gen/
lint.xml
.apt_generated/
+out/
+
diff --git a/android/res/values/arrays.xml b/android/res/values/arrays.xml
index 92949b84..71a34760 100644
--- a/android/res/values/arrays.xml
+++ b/android/res/values/arrays.xml
@@ -14,6 +14,7 @@
- rTorrent
- Torrentflux-b4rt
- Transmission
+ - Synology
- µTorrent
- Vuze
@@ -29,6 +30,7 @@
- daemon_rtorrent
- daemon_tfb4rt
- daemon_transmission
+ - daemon_synology
- daemon_utorrent
- daemon_vuze
diff --git a/lib/src/org/transdroid/daemon/Daemon.java b/lib/src/org/transdroid/daemon/Daemon.java
index 3b890006..7579ea07 100644
--- a/lib/src/org/transdroid/daemon/Daemon.java
+++ b/lib/src/org/transdroid/daemon/Daemon.java
@@ -22,6 +22,7 @@ import org.transdroid.daemon.DLinkRouterBT.DLinkRouterBTAdapter;
import org.transdroid.daemon.Ktorrent.KtorrentAdapter;
import org.transdroid.daemon.Qbittorrent.QbittorrentAdapter;
import org.transdroid.daemon.Rtorrent.RtorrentAdapter;
+import org.transdroid.daemon.Synology.SynologyAdapter;
import org.transdroid.daemon.Tfb4rt.Tfb4rtAdapter;
import org.transdroid.daemon.Transmission.TransmissionAdapter;
import org.transdroid.daemon.Utorrent.UtorrentAdapter;
@@ -83,8 +84,13 @@ public enum Daemon {
return new Tfb4rtAdapter(settings);
}
},
+ Synology {
+ public IDaemonAdapter createAdapter(DaemonSettings settings) {
+ return new SynologyAdapter(settings);
+ }
+ },
Transmission {
- public IDaemonAdapter createAdapter(DaemonSettings settings) {
+ public IDaemonAdapter createAdapter(DaemonSettings settings) {
return new TransmissionAdapter(settings);
}
},
@@ -142,8 +148,11 @@ public enum Daemon {
if (daemonCode.equals("daemon_tfb4rt")) {
return Tfb4rt;
}
- if (daemonCode.equals("daemon_transmission")) {
- return Transmission;
+ if (daemonCode.equals("daemon_transmission")) {
+ return Transmission;
+ }
+ if (daemonCode.equals("daemon_synology")) {
+ return Synology;
}
if (daemonCode.equals("daemon_utorrent")) {
return uTorrent;
@@ -179,6 +188,8 @@ public enum Daemon {
}
case Deluge:
return 8112;
+ case Synology:
+ return 5000;
case Transmission:
return 9091;
case Bitflu:
@@ -198,7 +209,7 @@ public enum Daemon {
}
public static boolean supportsFileListing(Daemon type) {
- return type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet;
+ return type == Synology || type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet;
}
public static boolean supportsFineDetails(Daemon type) {
@@ -235,7 +246,7 @@ public enum Daemon {
}
public static boolean supportsAddByMagnetUrl(Daemon type) {
- return type == uTorrent || type == BitTorrent || type == Transmission || type == Deluge || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet;
+ return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet;
}
public static boolean supportsRemoveWithData(Daemon type) {
diff --git a/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java b/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java
new file mode 100644
index 00000000..bf0708ea
--- /dev/null
+++ b/lib/src/org/transdroid/daemon/Synology/SynologyAdapter.java
@@ -0,0 +1,454 @@
+/*
+ * This file is part of Transdroid
+ *
+ * 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 .
+ *
+ */
+package org.transdroid.daemon.Synology;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.http.HttpEntity;
+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.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.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.RetrieveTask;
+import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
+import org.transdroid.daemon.task.SetTransferRatesTask;
+import org.transdroid.daemon.util.Collections2;
+import org.transdroid.daemon.util.DLog;
+import org.transdroid.daemon.util.HttpHelper;
+
+/**
+ * The daemon adapter from the Synology Download Station torrent client.
+ *
+ */
+public class SynologyAdapter implements IDaemonAdapter {
+
+ private static final String LOG_NAME = "Synology daemon";
+
+ private DaemonSettings settings;
+ private DefaultHttpClient httpClient;
+
+ private String sid;
+
+ public SynologyAdapter(DaemonSettings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public DaemonTaskResult executeTask(DaemonTask task) {
+ String tid;
+ try {
+ switch (task.getMethod()) {
+ case Retrieve:
+ return new RetrieveTaskSuccessResult((RetrieveTask) task, tasksList(), null);
+ case GetStats:
+ return null;
+ case GetTorrentDetails:
+ tid = task.getTargetTorrent().getUniqueID();
+ return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, torrentDetails(tid));
+ case GetFileList:
+ tid = task.getTargetTorrent().getUniqueID();
+ return new GetFileListTaskSuccessResult((GetFileListTask) task, fileList(tid));
+ case AddByFile:
+ return null;
+ case AddByUrl:
+ String url = ((AddByUrlTask)task).getUrl();
+ createTask(url);
+ return new DaemonTaskSuccessResult(task);
+ case AddByMagnetUrl:
+ String magnet = ((AddByMagnetUrlTask)task).getUrl();
+ createTask(magnet);
+ return new DaemonTaskSuccessResult(task);
+ case Remove:
+ tid = task.getTargetTorrent().getUniqueID();
+ removeTask(tid);
+ return new DaemonTaskSuccessResult(task);
+ case Pause:
+ tid = task.getTargetTorrent().getUniqueID();
+ pauseTask(tid);
+ return new DaemonTaskSuccessResult(task);
+ case PauseAll:
+ pauseAllTasks();
+ return new DaemonTaskSuccessResult(task);
+ case Resume:
+ tid = task.getTargetTorrent().getUniqueID();
+ resumeTask(tid);
+ return new DaemonTaskSuccessResult(task);
+ case ResumeAll:
+ resumeAllTasks();
+ return new DaemonTaskSuccessResult(task);
+ case SetDownloadLocation:
+ return null;
+ case SetFilePriorities:
+ return null;
+ case SetTransferRates:
+ SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
+ int uploadRate = ratesTask.getUploadRate() == null ? 0 : ratesTask.getUploadRate().intValue();
+ int downloadRate = ratesTask.getDownloadRate() == null ? 0 : ratesTask.getDownloadRate().intValue();
+ setTransferRates(uploadRate, downloadRate);
+ return new DaemonTaskSuccessResult(task);
+ case SetAlternativeMode:
+ default:
+ return null;
+ }
+ } catch (DaemonException e) {
+ return new DaemonTaskFailureResult(task, e);
+ }
+ }
+
+ @Override
+ public Daemon getType() {
+ return settings.getType();
+ }
+
+ @Override
+ public DaemonSettings getSettings() {
+ return this.settings;
+ }
+
+ // Synology API
+
+ private String login() throws DaemonException {
+ DLog.d(LOG_NAME, "login()");
+ try {
+ return new SynoRequest(
+ "auth.cgi",
+ "SYNO.API.Auth",
+ "2"
+ ).get("&method=login&account=" + settings.getUsername() + "&passwd=" + settings.getPassword() + "&session=DownloadStation&format=sid"
+ ).getData().getString("sid");
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ private void setTransferRates(int uploadRate, int downloadRate) throws DaemonException {
+ authGet("SYNO.DownloadStation.Info", "1", "DownloadStation/info.cgi",
+ "&method=setserverconfig&bt_max_upload=" + uploadRate + "&bt_max_download=" + downloadRate).ensureSuccess();
+ }
+
+ private void createTask(String uri) throws DaemonException {
+ try {
+ authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=create&uri=" + URLEncoder.encode(uri, "UTF-8")).ensureSuccess();
+ } catch (UnsupportedEncodingException e) {
+ // Never happens
+ throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString());
+ }
+ }
+
+ private void removeTask(String tid) throws DaemonException {
+ List tids = new ArrayList();
+ tids.add(tid);
+ removeTasks(tids);
+ }
+
+ private void pauseTask(String tid) throws DaemonException {
+ List tids = new ArrayList();
+ tids.add(tid);
+ pauseTasks(tids);
+ }
+
+ private void resumeTask(String tid) throws DaemonException {
+ List tids = new ArrayList();
+ tids.add(tid);
+ resumeTasks(tids);
+ }
+
+ private void pauseAllTasks() throws DaemonException {
+ List tids = new ArrayList();
+ for (Torrent torrent: tasksList()) {
+ tids.add(torrent.getUniqueID());
+ }
+ pauseTasks(tids);
+ }
+
+ private void resumeAllTasks() throws DaemonException {
+ List tids = new ArrayList();
+ for (Torrent torrent: tasksList()) {
+ tids.add(torrent.getUniqueID());
+ }
+ resumeTasks(tids);
+ }
+
+ private void removeTasks(List tids) throws DaemonException {
+ authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=delete&id=" + Collections2.joinString(tids, ",") + "").ensureSuccess();
+ }
+
+ private void pauseTasks(List tids) throws DaemonException {
+ authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=pause&id=" + Collections2.joinString(tids, ",")).ensureSuccess();
+ }
+
+ private void resumeTasks(List tids) throws DaemonException {
+ authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=resume&id=" + Collections2.joinString(tids, ",")).ensureSuccess();
+ }
+
+ private List tasksList() throws DaemonException {
+ try {
+ JSONArray jsonTasks = authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=list&additional=detail,transfer,tracker").getData().getJSONArray("tasks");
+ DLog.d(LOG_NAME, "Tasks = " + jsonTasks.toString());
+ List result = new ArrayList();
+ for (int i = 0; i < jsonTasks.length(); i++) {
+ result.add(parseTorrent(i, jsonTasks.getJSONObject(i)));
+ }
+ return result;
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ private List fileList(String torrentId) throws DaemonException {
+ try {
+ List result = new ArrayList();
+ JSONObject jsonTask = authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=getinfo&id=" + torrentId + "&additional=detail,transfer,tracker,file").getData().getJSONArray("tasks").getJSONObject(0);
+ DLog.d(LOG_NAME, "File list = " + jsonTask.toString());
+ JSONObject additional = jsonTask.getJSONObject("additional");
+ if (!additional.has("file")) return result;
+ JSONArray files = additional.getJSONArray("file");
+ for (int i = 0; i < files.length(); i++) {
+ JSONObject task = files.getJSONObject(i);
+ result.add(new TorrentFile(
+ task.getString("filename"),
+ task.getString("filename"),
+ null,
+ null,
+ task.getLong("size"),
+ task.getLong("size_downloaded"),
+ priority(task.getString("priority"))
+ ));
+ }
+ return result;
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ private TorrentDetails torrentDetails(String torrentId) throws DaemonException {
+ List trackers = new ArrayList();
+ List errors = new ArrayList();
+ try {
+ JSONObject jsonTorrent = authGet("SYNO.DownloadStation.Task", "1", "DownloadStation/task.cgi", "&method=getinfo&id=" + torrentId + "&additional=tracker").getData().getJSONArray("tasks").getJSONObject(0);
+ JSONObject additional = jsonTorrent.getJSONObject("additional");
+ if (additional.has("tracker")) {
+ JSONArray tracker = additional.getJSONArray("tracker");
+ for (int i = 0; i < tracker.length(); i++) {
+ JSONObject t = tracker.getJSONObject(i);
+ if ("Success".equals(t.getString("status"))) {
+ trackers.add(t.getString("url"));
+ } else {
+ errors.add(t.getString("status"));
+ }
+ }
+ }
+ return new TorrentDetails(trackers, errors);
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ private Torrent parseTorrent(long id, JSONObject jsonTorrent) throws JSONException, DaemonException {
+ JSONObject additional = jsonTorrent.getJSONObject("additional");
+ JSONObject detail = additional.getJSONObject("detail");
+ JSONObject transfer = additional.getJSONObject("transfer");
+ long downloaded = transfer.getLong("size_downloaded");
+ int speed = transfer.getInt("speed_download");
+ long size = jsonTorrent.getLong("size");
+ Float eta = new Float(size - downloaded) / speed;
+ int totalPeers = 0;
+ if (additional.has("tracker")) {
+ JSONArray tracker = additional.getJSONArray("tracker");
+ for (int i = 0; i < tracker.length(); i++) {
+ JSONObject t = tracker.getJSONObject(i);
+ if ("Success".equals(t.getString("status"))) {
+ totalPeers += t.getInt("peers");
+ totalPeers += t.getInt("seeds");
+ }
+ }
+ }
+ return new Torrent(
+ id,
+ jsonTorrent.getString("id"),
+ jsonTorrent.getString("title"),
+ torrentStatus(jsonTorrent.getString("status")),
+ detail.getString("destination"),
+ speed,
+ transfer.getInt("speed_upload"),
+ detail.getInt("connected_leechers"),
+ detail.getInt("connected_seeders"),
+ totalPeers,
+ totalPeers,
+ eta.intValue(),
+ downloaded,
+ Integer.parseInt(transfer.getString("size_uploaded")),
+ size,
+ (size == 0) ? 0 : (new Float(downloaded) / size),
+ 0,
+ jsonTorrent.getString("title"),
+ new Date(detail.getLong("create_time") * 1000),
+ null,
+ ""
+ );
+ }
+
+ private TorrentStatus torrentStatus(String status) {
+ if ("downloading".equals(status)) return TorrentStatus.Downloading;
+ if ("seeding".equals(status)) return TorrentStatus.Seeding;
+ if ("finished".equals(status)) return TorrentStatus.Paused;
+ if ("finishing".equals(status)) return TorrentStatus.Paused;
+ if ("waiting".equals(status)) return TorrentStatus.Waiting;
+ if ("paused".equals(status)) return TorrentStatus.Paused;
+ if ("error".equals(status)) return TorrentStatus.Error;
+ return TorrentStatus.Unknown;
+ }
+
+ private Priority priority(String priority) {
+ if ("low".equals(priority)) return Priority.Low;
+ if ("normal".equals(priority)) return Priority.Normal;
+ if ("high".equals(priority)) return Priority.High;
+ return Priority.Off;
+ }
+
+ /**
+ * Authenticated GET. If no session open, a login authGet will be done before-hand.
+ */
+ private SynoResponse authGet(String api, String version, String path, String params) throws DaemonException {
+ if (sid == null) {
+ sid = login();
+ }
+ return new SynoRequest(path, api, version).get(params + "&_sid=" + sid);
+ }
+
+ private DefaultHttpClient getHttpClient() throws DaemonException {
+ if (httpClient == null)
+ httpClient = HttpHelper.createStandardHttpClient(settings, true);
+ return httpClient;
+ }
+
+ private class SynoRequest {
+ private final String path;
+ private final String api;
+ private final String version;
+
+ public SynoRequest(String path, String api, String version) {
+ this.path = path;
+ this.api = api;
+ this.version = version;
+ }
+
+ public SynoResponse get(String params) throws DaemonException {
+ try {
+ return new SynoResponse(getHttpClient().execute(new HttpGet(buildURL(params))));
+ } catch (IOException e) {
+ throw new DaemonException(ExceptionType.ConnectionError, e.toString());
+ }
+ }
+
+ private String buildURL(String params) {
+ return (settings.getSsl() ? "https://" : "http://")
+ + settings.getAddress()
+ + ":" + settings.getPort()
+ + "/webapi/" + path
+ + "?api=" + api
+ + "&version=" + version
+ + params;
+ }
+
+ }
+
+ private static class SynoResponse {
+
+ private final HttpResponse response;
+
+ public SynoResponse(HttpResponse response) {
+ this.response = response;
+ }
+
+ public JSONObject getData() throws DaemonException {
+ JSONObject json = getJson();
+ try {
+ if (json.getBoolean("success")) {
+ return json.getJSONObject("data");
+ } else {
+ DLog.e(LOG_NAME, "not a success: " + json.toString());
+ throw new DaemonException(ExceptionType.AuthenticationFailure, json.getString("error"));
+ }
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ public JSONObject getJson() throws DaemonException {
+ try {
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ DLog.e(LOG_NAME, "Error: No entity in HTTP response");
+ throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response.");
+ }
+ // Read JSON response
+ java.io.InputStream instream = entity.getContent();
+ String result = HttpHelper.ConvertStreamToString(instream);
+ JSONObject json;
+ json = new JSONObject(result);
+ instream.close();
+ return json;
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.UnexpectedResponse, "Bad JSON");
+ } catch (IOException e) {
+ DLog.e(LOG_NAME, "getJson error: " + e.toString());
+ throw new DaemonException(ExceptionType.AuthenticationFailure, e.toString());
+ }
+ }
+
+ public void ensureSuccess() throws DaemonException {
+ JSONObject json = getJson();
+ try {
+ if (!json.getBoolean("success"))
+ throw new DaemonException(ExceptionType.UnexpectedResponse, json.getString("error"));
+ } catch (JSONException e) {
+ throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
+ }
+ }
+
+ }
+
+}
diff --git a/lib/src/org/transdroid/daemon/util/Collections2.java b/lib/src/org/transdroid/daemon/util/Collections2.java
new file mode 100644
index 00000000..97286575
--- /dev/null
+++ b/lib/src/org/transdroid/daemon/util/Collections2.java
@@ -0,0 +1,24 @@
+package org.transdroid.daemon.util;
+
+import java.util.Iterator;
+
+/**
+ * Helpers on Collections
+ */
+public class Collections2 {
+
+ /**
+ * Create a String from an iterable with a separator. Exemple: mkString({1,2,3,4}, ":" => "1:2:3:4"
+ */
+ public static String joinString(Iterable iterable, String separator) {
+ boolean first = true;
+ String result = "";
+ Iterator it = iterable.iterator();
+ while (it.hasNext()) {
+ result = (first ? "" : separator) + it.next().toString();
+ first = false;
+ }
+ return result;
+ }
+
+}