From 485ed115bb947a27ab33209734c12cf92f5811a3 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Wed, 11 Sep 2019 10:34:17 +0200 Subject: [PATCH 01/26] Point users to 2.5.16 release --- latest-app.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest-app.html b/latest-app.html index cad7fc87..92252cd3 100644 --- a/latest-app.html +++ b/latest-app.html @@ -1 +1 @@ -235|2.5.15 +236|2.5.16 From e454a07f21b9377b49fdf3d9aeb52911d551b8d5 Mon Sep 17 00:00:00 2001 From: Brian Witt Date: Mon, 23 Sep 2019 22:13:20 -0700 Subject: [PATCH 02/26] add two missing support checks for Deluge2Rpc --- app/src/main/java/org/transdroid/daemon/Daemon.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Daemon.java b/app/src/main/java/org/transdroid/daemon/Daemon.java index 69e92121..d4d88c5e 100644 --- a/app/src/main/java/org/transdroid/daemon/Daemon.java +++ b/app/src/main/java/org/transdroid/daemon/Daemon.java @@ -363,12 +363,12 @@ public enum Daemon { public static boolean supportsAddByMagnetUrl(Daemon type) { return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == DelugeRpc - || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet || type == Aria2 - || type == tTorrent || type == Dummy; + || type == Deluge2Rpc || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet + || type == Aria2 || type == tTorrent || type == Dummy; } public static boolean supportsRemoveWithData(Daemon type) { - return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == DelugeRpc + return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == DelugeRpc || type == Deluge2Rpc || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == rTorrent || type == Aria2 || type == tTorrent || type == Dummy; } @@ -396,7 +396,7 @@ public enum Daemon { } public static boolean supportsSetDownloadLocation(Daemon type) { - return type == Transmission || type == Deluge || type == DelugeRpc || type == Deluge2Rpc|| type == qBittorrent || type == Dummy; + return type == Transmission || type == Deluge || type == DelugeRpc || type == Deluge2Rpc || type == qBittorrent || type == Dummy; } public static boolean supportsSetAlternativeMode(Daemon type) { @@ -404,7 +404,7 @@ public enum Daemon { } public static boolean supportsSetTrackers(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeRpc || type == Deluge2Rpc|| type == Dummy; + return type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeRpc || type == Deluge2Rpc || type == Dummy; } public static boolean supportsForceRecheck(Daemon type) { From cd99cbb1597ce57fffa2af497af2e271ab0b2e8b Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Wed, 27 Nov 2019 22:32:06 -0800 Subject: [PATCH 03/26] Add sequential download and first+last piece prioratization download modes, implemented for qbittorrent --- .../transdroid/core/gui/DetailsActivity.java | 26 ++++++++++++++++ .../transdroid/core/gui/DetailsFragment.java | 27 ++++++++++++++++ .../core/gui/TorrentTasksExecutor.java | 4 +++ .../transdroid/core/gui/TorrentsActivity.java | 26 ++++++++++++++++ .../java/org/transdroid/daemon/Daemon.java | 8 +++++ .../org/transdroid/daemon/DaemonMethod.java | 4 ++- .../Qbittorrent/QbittorrentAdapter.java | 31 +++++++++++++++---- .../java/org/transdroid/daemon/Torrent.java | 23 ++++++++++++++ .../ToggleFirstLastPieceDownloadTask.java | 31 +++++++++++++++++++ .../task/ToggleSequentialDownloadTask.java | 31 +++++++++++++++++++ app/src/main/res/menu/fragment_details.xml | 31 ++++++++++++++----- app/src/main/res/values/strings.xml | 4 +++ 12 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java create mode 100644 app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java index 8a0da758..205731a1 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java @@ -56,6 +56,8 @@ import org.transdroid.daemon.TorrentFile; import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult; +import org.transdroid.daemon.task.ToggleSequentialDownloadTask; +import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask; import org.transdroid.daemon.task.ForceRecheckTask; import org.transdroid.daemon.task.GetFileListTask; import org.transdroid.daemon.task.GetFileListTaskSuccessResult; @@ -278,6 +280,30 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx } } + @Background + @Override + public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) { + torrent.mimicSequentialDownload(sequentialState); + DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log); + if (result instanceof DaemonTaskSuccessResult) { + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential)); + } else { + onCommunicationError((DaemonTaskFailureResult) result, false); + } + } + + @Background + @Override + public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) { + torrent.mimicFirstLastPieceDownload(firstLastPieceState); + DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log); + if (result instanceof DaemonTaskSuccessResult) { + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.action_toggle_firstlastpiece)); + } else { + onCommunicationError((DaemonTaskFailureResult) result, false); + } + } + @Background @Override public void forceRecheckTorrent(Torrent torrent) { diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java index 79843c00..5648bee0 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java @@ -301,6 +301,12 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen case R.id.action_stop: stopTorrent(); return true; + case R.id.action_toggle_sequential: + toggleSequentialDownload(menuItem); + return true; + case R.id.action_toggle_firstlastpiece: + toggleFirstLastPieceDownload(menuItem); + return true; case R.id.action_forcerecheck: setForceRecheck(); return true; @@ -359,6 +365,15 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen detailsMenu.getMenu().findItem(R.id.action_setlabel).setVisible(setLabel); boolean forceRecheck = Daemon.supportsForceRecheck(torrent.getDaemon()); detailsMenu.getMenu().findItem(R.id.action_forcerecheck).setVisible(forceRecheck); + boolean sequentialdl = Daemon.supportsSequentialDownload(torrent.getDaemon()); + MenuItem seqMenuItem = detailsMenu.getMenu().findItem(R.id.action_toggle_sequential); + seqMenuItem.setVisible(sequentialdl); + seqMenuItem.setChecked(torrent.isSequentiallyDownloading()); + boolean firstlastpiecedl = Daemon.supportsFirstLastPiece(torrent.getDaemon()); + MenuItem flpMenuItem = detailsMenu.getMenu().findItem(R.id.action_toggle_firstlastpiece); + flpMenuItem.setVisible(firstlastpiecedl); + flpMenuItem.setChecked(torrent.isDownloadingFirstLastPieceFirst()); + detailsMenu.getMenu().findItem(R.id.action_download_mode).setVisible(firstlastpiecedl || sequentialdl); boolean setTrackers = Daemon.supportsSetTrackers(torrent.getDaemon()); detailsMenu.getMenu().findItem(R.id.action_updatetrackers).setVisible(setTrackers); boolean setLocation = Daemon.supportsSetDownloadLocation(torrent.getDaemon()); @@ -421,6 +436,18 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen } } + @OptionsItem(R.id.action_toggle_sequential) + protected void toggleSequentialDownload(MenuItem menuItem) { + if (getTasksExecutor() != null) + getTasksExecutor().toggleSequentialDownload(torrent, !menuItem.isChecked()); + } + + @OptionsItem(R.id.action_toggle_firstlastpiece) + protected void toggleFirstLastPieceDownload(MenuItem menuItem) { + if (getTasksExecutor() != null) + getTasksExecutor().toggleFirstLastPieceDownload(torrent, !menuItem.isChecked()); + } + @OptionsItem(R.id.action_forcerecheck) protected void setForceRecheck() { if (getTasksExecutor() != null) diff --git a/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java b/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java index b9418bb9..cb405ce2 100644 --- a/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java +++ b/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java @@ -40,6 +40,10 @@ public interface TorrentTasksExecutor { void removeTorrent(Torrent torrent, boolean withData); + void toggleSequentialDownload(Torrent torrent, boolean sequentialState); + + void toggleFirstLastPieceDownload(Torrent torrent, boolean sequentialState); + void forceRecheckTorrent(Torrent torrent); void updateLabel(Torrent torrent, String newLabel); diff --git a/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java b/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java index 2956cf2b..a0d259e2 100644 --- a/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/TorrentsActivity.java @@ -103,6 +103,8 @@ import org.transdroid.daemon.task.AddByUrlTask; import org.transdroid.daemon.task.DaemonTaskFailureResult; import org.transdroid.daemon.task.DaemonTaskResult; import org.transdroid.daemon.task.DaemonTaskSuccessResult; +import org.transdroid.daemon.task.ToggleSequentialDownloadTask; +import org.transdroid.daemon.task.ToggleFirstLastPieceDownloadTask; import org.transdroid.daemon.task.ForceRecheckTask; import org.transdroid.daemon.task.GetFileListTask; import org.transdroid.daemon.task.GetFileListTaskSuccessResult; @@ -1225,6 +1227,30 @@ public class TorrentsActivity extends AppCompatActivity implements TorrentTasksE } } + @Background + @Override + public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) { + torrent.mimicSequentialDownload(sequentialState); + DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log); + if (result instanceof DaemonTaskSuccessResult) { + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential)); + } else { + onCommunicationError((DaemonTaskFailureResult) result, false); + } + } + + @Background + @Override + public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) { + torrent.mimicFirstLastPieceDownload(firstLastPieceState); + DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log); + if (result instanceof DaemonTaskSuccessResult) { + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.action_toggle_firstlastpiece)); + } else { + onCommunicationError((DaemonTaskFailureResult) result, false); + } + } + @Background @Override public void forceRecheckTorrent(Torrent torrent) { diff --git a/app/src/main/java/org/transdroid/daemon/Daemon.java b/app/src/main/java/org/transdroid/daemon/Daemon.java index d4d88c5e..121f6674 100644 --- a/app/src/main/java/org/transdroid/daemon/Daemon.java +++ b/app/src/main/java/org/transdroid/daemon/Daemon.java @@ -412,6 +412,14 @@ public enum Daemon { || type == Transmission || type == Dummy || type == qBittorrent; } + public static boolean supportsSequentialDownload(Daemon type) { + return type == qBittorrent; + } + + public static boolean supportsFirstLastPiece(Daemon type) { + return type == qBittorrent; + } + public static boolean supportsExtraPassword(Daemon type) { return type == Deluge || type == Aria2; } diff --git a/app/src/main/java/org/transdroid/daemon/DaemonMethod.java b/app/src/main/java/org/transdroid/daemon/DaemonMethod.java index f9ec9b7b..e0e242ef 100644 --- a/app/src/main/java/org/transdroid/daemon/DaemonMethod.java +++ b/app/src/main/java/org/transdroid/daemon/DaemonMethod.java @@ -44,7 +44,9 @@ public enum DaemonMethod { SetTrackers (19), SetAlternativeMode (20), GetStats (21), - ForceRecheck (22); + ForceRecheck (22), + ToggleSequentialDownload(23), + ToggleFirstLastPieceDownload(24); private int code; private static final Map lookup = new HashMap<>(); diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 0982053d..7f693b8f 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -286,11 +286,23 @@ public class QbittorrentAdapter implements IDaemonAdapter { } return new DaemonTaskSuccessResult(task); - case ForceRecheck: + case ForceRecheck: - // Force recheck a torrent - makeRequest(log, "/command/recheck", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); + // Force recheck a torrent + makeRequest(log, "/command/recheck", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); + + case ToggleSequentialDownload: + + // Toggle sequential download mode on a torrent + makeRequest(log, "/command/toggleSequentialDownload", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); + + case ToggleFirstLastPieceDownload: + + // Set policy for downloading first and last piece first on a torrent + makeRequest(log, "/command/toggleFirstLastPiecePrio", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); case SetLabel: @@ -492,6 +504,8 @@ public class QbittorrentAdapter implements IDaemonAdapter { long uploaded; int dlspeed; int upspeed; + boolean dlseq = false; + boolean dlflp = false; Date addedOn = null; Date completionOn = null; String label = null; @@ -507,6 +521,8 @@ public class QbittorrentAdapter implements IDaemonAdapter { ratio = tor.getDouble("ratio"); dlspeed = tor.getInt("dlspeed"); upspeed = tor.getInt("upspeed"); + dlseq = tor.getBoolean("seq_dl"); + dlflp = tor.getBoolean("f_l_piece_prio"); if (tor.has("uploaded")) { uploaded = tor.getLong("uploaded"); } else { @@ -535,7 +551,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { eta = (long) (size - (size * progress)) / dlspeed; // Add the parsed torrent to the list // @formatter:off - torrents.add(new Torrent( + Torrent torrent = new Torrent( (long) i, tor.getString("hash"), tor.getString("name"), @@ -557,7 +573,10 @@ public class QbittorrentAdapter implements IDaemonAdapter { addedOn, completionOn, null, - settings.getType())); + settings.getType()); + torrent.mimicSequentialDownload(dlseq); + torrent.mimicFirstLastPieceDownload(dlflp); + torrents.add(torrent); // @formatter:on } diff --git a/app/src/main/java/org/transdroid/daemon/Torrent.java b/app/src/main/java/org/transdroid/daemon/Torrent.java index 446b9022..b7dd249e 100644 --- a/app/src/main/java/org/transdroid/daemon/Torrent.java +++ b/app/src/main/java/org/transdroid/daemon/Torrent.java @@ -49,6 +49,8 @@ public final class Torrent implements Parcelable, Comparable, Finishabl final private float partDone; final private float available; private String label; + private boolean sequentialDownload; + private boolean firstLastPieceDownload; final private Date dateAdded; final private Date dateDone; @@ -76,6 +78,8 @@ public final class Torrent implements Parcelable, Comparable, Finishabl this.partDone = in.readFloat(); this.available = in.readFloat(); this.label = in.readString(); + this.sequentialDownload = in.readByte() != 0; + this.firstLastPieceDownload = in.readByte() != 0; long lDateAdded = in.readLong(); this.dateAdded = (lDateAdded == -1) ? null : new Date(lDateAdded); @@ -109,6 +113,8 @@ public final class Torrent implements Parcelable, Comparable, Finishabl this.partDone = partDone; this.available = available; this.label = label; + this.sequentialDownload = false; + this.firstLastPieceDownload = false; this.dateAdded = dateAdded; if (realDateDone != null) { @@ -197,6 +203,13 @@ public final class Torrent implements Parcelable, Comparable, Finishabl return label; } + public boolean isSequentiallyDownloading() { + return sequentialDownload; + } + public boolean isDownloadingFirstLastPieceFirst() { + return firstLastPieceDownload; + } + public Date getDateAdded() { return dateAdded; } @@ -342,6 +355,14 @@ public final class Torrent implements Parcelable, Comparable, Finishabl label = newLabel; } + public void mimicSequentialDownload(boolean sequentialDownload) { + this.sequentialDownload = sequentialDownload; + } + + public void mimicFirstLastPieceDownload(boolean firstLastPieceDownload) { + this.firstLastPieceDownload = firstLastPieceDownload; + } + public void mimicCheckingStatus() { statusCode = TorrentStatus.Checking; } @@ -399,6 +420,8 @@ public final class Torrent implements Parcelable, Comparable, Finishabl dest.writeFloat(partDone); dest.writeFloat(available); dest.writeString(label); + dest.writeByte((byte) (sequentialDownload ? 1 : 0)); + dest.writeByte((byte) (firstLastPieceDownload ? 1 : 0)); dest.writeLong((dateAdded == null) ? -1 : dateAdded.getTime()); dest.writeLong((dateDone == null) ? -1 : dateDone.getTime()); diff --git a/app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java b/app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java new file mode 100644 index 00000000..3030c325 --- /dev/null +++ b/app/src/main/java/org/transdroid/daemon/task/ToggleFirstLastPieceDownloadTask.java @@ -0,0 +1,31 @@ +/* + * 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.task; + +import org.transdroid.daemon.DaemonMethod; +import org.transdroid.daemon.IDaemonAdapter; +import org.transdroid.daemon.Torrent; + +public class ToggleFirstLastPieceDownloadTask extends DaemonTask { + protected ToggleFirstLastPieceDownloadTask(IDaemonAdapter adapter, Torrent targetTorrent) { + super(adapter, DaemonMethod.ToggleFirstLastPieceDownload, targetTorrent, null); + } + public static ToggleFirstLastPieceDownloadTask create(IDaemonAdapter adapter, Torrent targetTorrent) { + return new ToggleFirstLastPieceDownloadTask(adapter, targetTorrent); + } +} diff --git a/app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java b/app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java new file mode 100644 index 00000000..74575e14 --- /dev/null +++ b/app/src/main/java/org/transdroid/daemon/task/ToggleSequentialDownloadTask.java @@ -0,0 +1,31 @@ +/* + * 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.task; + +import org.transdroid.daemon.DaemonMethod; +import org.transdroid.daemon.IDaemonAdapter; +import org.transdroid.daemon.Torrent; + +public class ToggleSequentialDownloadTask extends DaemonTask { + protected ToggleSequentialDownloadTask(IDaemonAdapter adapter, Torrent targetTorrent) { + super(adapter, DaemonMethod.ToggleSequentialDownload, targetTorrent, null); + } + public static ToggleSequentialDownloadTask create(IDaemonAdapter adapter, Torrent targetTorrent) { + return new ToggleSequentialDownloadTask(adapter, targetTorrent); + } +} diff --git a/app/src/main/res/menu/fragment_details.xml b/app/src/main/res/menu/fragment_details.xml index 93a73257..7abfd206 100644 --- a/app/src/main/res/menu/fragment_details.xml +++ b/app/src/main/res/menu/fragment_details.xml @@ -47,19 +47,36 @@ + + + + + + @@ -74,25 +91,25 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7eb5dc56..c96932b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,9 @@ Remove torrent Remove and delete data Set label + Set download mode + Download sequentially + Download first and last pieces first Update trackers Change storage location Force data recheck @@ -175,6 +178,7 @@ Trackers updated Label set to \'%1$s\' Label removed + Toggled sequential download mode Checking %1$s data Torrent moved to \'%1$s\' File priorities updated From 99c419950e3984bc3680351a3c7e86321b986ecb Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Wed, 27 Nov 2019 22:35:55 -0800 Subject: [PATCH 04/26] Only show download mode options for active torrents --- app/src/main/java/org/transdroid/core/gui/DetailsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java index 5648bee0..08dad7eb 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java @@ -373,7 +373,7 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen MenuItem flpMenuItem = detailsMenu.getMenu().findItem(R.id.action_toggle_firstlastpiece); flpMenuItem.setVisible(firstlastpiecedl); flpMenuItem.setChecked(torrent.isDownloadingFirstLastPieceFirst()); - detailsMenu.getMenu().findItem(R.id.action_download_mode).setVisible(firstlastpiecedl || sequentialdl); + detailsMenu.getMenu().findItem(R.id.action_download_mode).setVisible(!torrent.isFinished() && (firstlastpiecedl || sequentialdl)); boolean setTrackers = Daemon.supportsSetTrackers(torrent.getDaemon()); detailsMenu.getMenu().findItem(R.id.action_updatetrackers).setVisible(setTrackers); boolean setLocation = Daemon.supportsSetDownloadLocation(torrent.getDaemon()); From 80e388a5600b8b69d70249f434b41e6d967ec855 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Wed, 27 Nov 2019 22:51:06 -0800 Subject: [PATCH 05/26] Make the successfull task snackbar duration a bit shorter (about 2s shorter). Helpful for two consecutive setting inputs, as you dont have to wait. --- app/src/main/java/org/transdroid/core/gui/DetailsActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java index 205731a1..59b48fc8 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java @@ -356,7 +356,7 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx // Refresh the screen as well refreshTorrent(); refreshTorrentDetails(torrent); - SnackbarManager.show(Snackbar.with(this).text(successMessage)); + SnackbarManager.show(Snackbar.with(this).text(successMessage).duration(Snackbar.SnackbarDuration.LENGTH_SHORT)); } @UiThread From 1fbb3dcea9efe3dd749e1ac092b7f6a926c7a466 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Wed, 27 Nov 2019 22:56:37 -0800 Subject: [PATCH 06/26] Adjust strings --- .../main/java/org/transdroid/core/gui/DetailsActivity.java | 2 +- app/src/main/res/values/strings.xml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java index 59b48fc8..448a9c25 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java @@ -298,7 +298,7 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx torrent.mimicFirstLastPieceDownload(firstLastPieceState); DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) { - onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.action_toggle_firstlastpiece)); + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece)); } else { onCommunicationError((DaemonTaskFailureResult) result, false); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c96932b3..e11907ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,9 +56,9 @@ Remove torrent Remove and delete data Set label - Set download mode + Download mode Download sequentially - Download first and last pieces first + Prioritize first and last piece Update trackers Change storage location Force data recheck @@ -179,6 +179,7 @@ Label set to \'%1$s\' Label removed Toggled sequential download mode + Toggled first and last piece priority Checking %1$s data Torrent moved to \'%1$s\' File priorities updated From 38943bf7f07f696af266c6e55c6ec885037d848b Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Thu, 28 Nov 2019 20:37:19 -0800 Subject: [PATCH 07/26] Russian localization --- app/src/main/res/values-ru/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fa16ea36..5840d258 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -54,6 +54,9 @@ Удалить торрент-файл Удалить вместе с данными Установить метку + Режим скачивания + Скачать последовательно + Скачать начало и конец первыми Обновить трекеры Изменить место расположения Пересчитать хэш @@ -167,6 +170,8 @@ Трекеры обновлены Метка установлена ​​в \'%1$s\' Метка удалена + Изменено последовательное скачивание торрента + Изменен приоритет скачивания начала и конца торрента Проверка данных %1$s Торрент перемещен в \'%1$s\' Приоритеты файлов обновлены From f9de3b095ced49ad5ae483ec42c19c40a8320ce0 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Thu, 28 Nov 2019 20:53:54 -0800 Subject: [PATCH 08/26] Make result strings be result dynamic --- .../java/org/transdroid/core/gui/DetailsActivity.java | 10 ++++++++-- app/src/main/res/values-ru/strings.xml | 6 ++++++ app/src/main/res/values/strings.xml | 8 ++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java index 448a9c25..c67d1f13 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsActivity.java @@ -284,9 +284,12 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx @Override public void toggleSequentialDownload(Torrent torrent, boolean sequentialState) { torrent.mimicSequentialDownload(sequentialState); + String onState = getString(R.string.result_togglesequential_onstate); + String offState = getString(R.string.result_togglesequential_offstate); + String stateString = sequentialState ? onState : offState; DaemonTaskResult result = ToggleSequentialDownloadTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) { - onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential)); + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglesequential, torrent.getName(), stateString)); } else { onCommunicationError((DaemonTaskFailureResult) result, false); } @@ -296,9 +299,12 @@ public class DetailsActivity extends AppCompatActivity implements TorrentTasksEx @Override public void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState) { torrent.mimicFirstLastPieceDownload(firstLastPieceState); + String onState = getString(R.string.result_togglefirstlastpiece_onstate); + String offState = getString(R.string.result_togglefirstlastpiece_offstate); + String stateString = firstLastPieceState ? onState : offState; DaemonTaskResult result = ToggleFirstLastPieceDownloadTask.create(currentConnection, torrent).execute(log); if (result instanceof DaemonTaskSuccessResult) { - onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece)); + onTaskSucceeded((DaemonTaskSuccessResult) result, getString(R.string.result_togglefirstlastpiece, torrent.getName(), stateString)); } else { onCommunicationError((DaemonTaskFailureResult) result, false); } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5840d258..cb9edf92 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -170,6 +170,12 @@ Трекеры обновлены Метка установлена ​​в \'%1$s\' Метка удалена + %1$s скачивается %2$s + последовательно + обычным образом + %1$s имеет %2$s + приоритет первого и последнего куска + обычый приоритет кусков Изменено последовательное скачивание торрента Изменен приоритет скачивания начала и конца торрента Проверка данных %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e11907ce..949b2e56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,8 +178,12 @@ Trackers updated Label set to \'%1$s\' Label removed - Toggled sequential download mode - Toggled first and last piece priority + %1$s is downloading %2$s + normally + sequentially + %1$s has %2$s + first and last piece priority + normal piece priority Checking %1$s data Torrent moved to \'%1$s\' File priorities updated From dd6b427777733b7a829afba2dbab9d285a34b0fb Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Fri, 29 Nov 2019 18:46:49 -0800 Subject: [PATCH 09/26] Fix minor typo --- .../main/java/org/transdroid/core/gui/TorrentTasksExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java b/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java index cb405ce2..dd7c5036 100644 --- a/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java +++ b/app/src/main/java/org/transdroid/core/gui/TorrentTasksExecutor.java @@ -42,7 +42,7 @@ public interface TorrentTasksExecutor { void toggleSequentialDownload(Torrent torrent, boolean sequentialState); - void toggleFirstLastPieceDownload(Torrent torrent, boolean sequentialState); + void toggleFirstLastPieceDownload(Torrent torrent, boolean firstLastPieceState); void forceRecheckTorrent(Torrent torrent); From 02ab41cffca0fa770ec852e32645562f1835ad06 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Wed, 4 Dec 2019 20:54:55 -0800 Subject: [PATCH 10/26] Simple text mockup of pieces map --- .../transdroid/core/gui/DetailsFragment.java | 2 ++ .../core/gui/lists/DetailsAdapter.java | 24 +++++++++++++++++ .../Qbittorrent/QbittorrentAdapter.java | 14 +++++++--- .../org/transdroid/daemon/TorrentDetails.java | 27 +++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java index 79843c00..75a53e8a 100644 --- a/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java +++ b/app/src/main/java/org/transdroid/core/gui/DetailsFragment.java @@ -203,6 +203,8 @@ public class DetailsFragment extends Fragment implements OnTrackersUpdatedListen .updateTrackers(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getTrackers())); ((DetailsAdapter) detailsList.getAdapter()) .updateErrors(SimpleListItemAdapter.SimpleStringItem.wrapStringsList(newTorrentDetails.getErrors())); + ((DetailsAdapter) detailsList.getAdapter()) + .updatePieces(newTorrentDetails.getPieces()); } /** diff --git a/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java b/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java index d1d374fe..dcfb7a1f 100644 --- a/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java +++ b/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java @@ -29,6 +29,7 @@ import android.text.util.Linkify; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.TextView; /** * List adapter that holds a header view showing torrent details and show the list list contained by the torrent. @@ -38,6 +39,8 @@ public class DetailsAdapter extends MergeAdapter { private ViewHolderAdapter torrentDetailsViewAdapter = null; private TorrentDetailsView torrentDetailsView = null; + private ViewHolderAdapter piecesMapViewAdapter = null; + private TextView piecesMapView = null; private ViewHolderAdapter trackersSeparatorAdapter = null; private SimpleListItemAdapter trackersAdapter = null; private ViewHolderAdapter errorsSeparatorAdapter = null; @@ -56,6 +59,13 @@ public class DetailsAdapter extends MergeAdapter { torrentDetailsViewAdapter.setViewVisibility(View.GONE); addAdapter(torrentDetailsViewAdapter); + // Pieces map + piecesMapView = new TextView(context); + piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView); + piecesMapViewAdapter.setViewEnabled(true); + piecesMapViewAdapter.setViewVisibility(View.VISIBLE); + addAdapter(piecesMapViewAdapter); + // Tracker errors errorsSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( context.getString(R.string.status_errors))); @@ -137,6 +147,20 @@ public class DetailsAdapter extends MergeAdapter { } } + public void updatePieces(List pieces) { + if (pieces == null || pieces.isEmpty()) { + //errorsAdapter.update(new ArrayList()); + //errorsSeparatorAdapter.setViewVisibility(View.GONE); + } else { + String piecesText = ""; + for (int piece : pieces) { + piecesText += piece; + } + piecesMapView.setText(piecesText.substring(0,40)); + //errorsSeparatorAdapter.setViewVisibility(View.VISIBLE); + } + } + /** * Clear currently visible torrent, including header and shown lists */ diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 0982053d..713c347c 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -204,7 +204,8 @@ public class QbittorrentAdapter implements IDaemonAdapter { String mhash = task.getTargetTorrent().getUniqueID(); JSONArray messages = new JSONArray(makeRequest(log, (version >= 30200 ? "/query/propertiesTrackers/" : "/json/propertiesTrackers/") + mhash)); - return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages)); + JSONArray pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash)); + return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages, pieces)); case GetFileList: @@ -439,7 +440,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + proxyFolder + path; } - private TorrentDetails parseJsonTorrentDetails(JSONArray messages) throws JSONException { + private TorrentDetails parseJsonTorrentDetails(JSONArray messages, JSONArray pieceStates) throws JSONException { ArrayList trackers = new ArrayList<>(); ArrayList errors = new ArrayList<>(); @@ -455,8 +456,15 @@ public class QbittorrentAdapter implements IDaemonAdapter { } } + ArrayList pieces = new ArrayList<>(); + if (pieceStates.length() > 0) { + for (int i = 0; i < pieceStates.length(); i++) { + pieces.add(pieceStates.getInt(i)); + } + } + // Return the list - return new TorrentDetails(trackers, errors); + return new TorrentDetails(trackers, errors, pieces); } diff --git a/app/src/main/java/org/transdroid/daemon/TorrentDetails.java b/app/src/main/java/org/transdroid/daemon/TorrentDetails.java index 6f3f2be4..45a02899 100644 --- a/app/src/main/java/org/transdroid/daemon/TorrentDetails.java +++ b/app/src/main/java/org/transdroid/daemon/TorrentDetails.java @@ -18,6 +18,7 @@ package org.transdroid.daemon; import java.util.List; +import java.util.ArrayList; import android.os.Parcel; import android.os.Parcelable; @@ -32,15 +33,29 @@ public final class TorrentDetails implements Parcelable { private final List trackers; private final List errors; + private final List pieces; public TorrentDetails(List trackers, List errors) { this.trackers = trackers; this.errors = errors; + this.pieces = null; + } + + public TorrentDetails(List trackers, List errors, List pieces) { + this.trackers = trackers; + this.errors = errors; + this.pieces = pieces; } private TorrentDetails(Parcel in) { this.trackers = in.createStringArrayList(); this.errors = in.createStringArrayList(); + + int[] piecesarray = in.createIntArray(); + this.pieces = new ArrayList(piecesarray.length); + for (int i : piecesarray) { + this.pieces.add(i); + } } public List getTrackers() { @@ -77,6 +92,10 @@ public final class TorrentDetails implements Parcelable { return errorsText; } + public List getPieces() { + return this.pieces; + } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public TorrentDetails createFromParcel(Parcel in) { return new TorrentDetails(in); @@ -96,6 +115,14 @@ public final class TorrentDetails implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeStringList(trackers); dest.writeStringList(errors); + + int[] piecesarray = new int[this.pieces.size()]; + for(int i = 0; i < this.pieces.size(); i++) { + if (this.pieces.get(i) != null) { + piecesarray[i] = this.pieces.get(i); + } + } + dest.writeIntArray(piecesarray); } } From db2892981b434094c86bba50ac6e9eeb61f7c8d5 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Thu, 5 Dec 2019 23:52:56 -0800 Subject: [PATCH 11/26] Full pieces view graphic --- .../core/gui/lists/DetailsAdapter.java | 34 ++-- .../core/gui/lists/PiecesMapView.java | 150 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java diff --git a/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java b/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java index dcfb7a1f..d04927b7 100644 --- a/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java +++ b/app/src/main/java/org/transdroid/core/gui/lists/DetailsAdapter.java @@ -21,6 +21,7 @@ import java.util.List; import org.transdroid.R; import org.transdroid.core.gui.navigation.*; +import org.transdroid.core.gui.lists.PiecesMapView; import org.transdroid.daemon.Torrent; import org.transdroid.daemon.TorrentFile; @@ -29,7 +30,6 @@ import android.text.util.Linkify; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.TextView; /** * List adapter that holds a header view showing torrent details and show the list list contained by the torrent. @@ -39,8 +39,9 @@ public class DetailsAdapter extends MergeAdapter { private ViewHolderAdapter torrentDetailsViewAdapter = null; private TorrentDetailsView torrentDetailsView = null; + private ViewHolderAdapter piecesSeparatorAdapter = null; private ViewHolderAdapter piecesMapViewAdapter = null; - private TextView piecesMapView = null; + private PiecesMapView piecesMapView = null; private ViewHolderAdapter trackersSeparatorAdapter = null; private SimpleListItemAdapter trackersAdapter = null; private ViewHolderAdapter errorsSeparatorAdapter = null; @@ -60,10 +61,15 @@ public class DetailsAdapter extends MergeAdapter { addAdapter(torrentDetailsViewAdapter); // Pieces map - piecesMapView = new TextView(context); + piecesSeparatorAdapter = new ViewHolderAdapter(FilterSeparatorView_.build(context).setText( + context.getString(R.string.status_pieces))); + piecesSeparatorAdapter.setViewEnabled(false); + piecesSeparatorAdapter.setViewVisibility(View.GONE); + addAdapter(piecesSeparatorAdapter); + piecesMapView = new PiecesMapView(context); piecesMapViewAdapter = new ViewHolderAdapter(piecesMapView); - piecesMapViewAdapter.setViewEnabled(true); - piecesMapViewAdapter.setViewVisibility(View.VISIBLE); + piecesMapViewAdapter.setViewEnabled(false); + piecesMapViewAdapter.setViewVisibility(View.GONE); addAdapter(piecesMapViewAdapter); // Tracker errors @@ -149,15 +155,17 @@ public class DetailsAdapter extends MergeAdapter { public void updatePieces(List pieces) { if (pieces == null || pieces.isEmpty()) { - //errorsAdapter.update(new ArrayList()); - //errorsSeparatorAdapter.setViewVisibility(View.GONE); + piecesSeparatorAdapter.setViewEnabled(false); + piecesSeparatorAdapter.setViewVisibility(View.GONE); + piecesMapViewAdapter.setViewEnabled(false); + piecesMapViewAdapter.setViewVisibility(View.GONE); } else { - String piecesText = ""; - for (int piece : pieces) { - piecesText += piece; - } - piecesMapView.setText(piecesText.substring(0,40)); - //errorsSeparatorAdapter.setViewVisibility(View.VISIBLE); + piecesMapView.setPieces(pieces); + + piecesMapViewAdapter.setViewEnabled(true); + piecesMapViewAdapter.setViewVisibility(View.VISIBLE); + piecesSeparatorAdapter.setViewEnabled(true); + piecesSeparatorAdapter.setViewVisibility(View.VISIBLE); } } diff --git a/app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java b/app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java new file mode 100644 index 00000000..805a3cbd --- /dev/null +++ b/app/src/main/java/org/transdroid/core/gui/lists/PiecesMapView.java @@ -0,0 +1,150 @@ +package org.transdroid.core.gui.lists; + +import org.transdroid.R; + +import android.content.Context; +import android.view.View; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; +import java.lang.Math; + +class PiecesMapView extends View { + + private final float scale = getContext().getResources().getDisplayMetrics().density; + private final int MINIMUM_HEIGHT = (int) (25 * scale); + private final int MINIMUM_PIECE_WIDTH = (int) (2 * scale); + + private ArrayList pieces = null; + + private final Paint downloadingPaint = new Paint(); + private final Paint donePaint = new Paint(); + private final Paint partialDonePaint = new Paint(); + + public PiecesMapView(Context context) { + super(context); + initPaints(); + } + + private void initPaints() { + downloadingPaint.setColor(getResources().getColor(R.color.torrent_downloading)); + donePaint.setColor(getResources().getColor(R.color.torrent_seeding)); + partialDonePaint.setColor(getResources().getColor(R.color.file_low)); + } + + public void setPieces(List pieces) { + this.pieces = new ArrayList(pieces); + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int ws = MeasureSpec.getSize(widthMeasureSpec); + int hs = Math.max(getHeight(), MINIMUM_HEIGHT); + setMeasuredDimension(ws, hs); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (this.pieces == null) { + return; + } + + int height = getHeight(); + int width = getWidth(); + + // downscale + ArrayList piecesScaled; + int pieceWidth; + + pieceWidth = MINIMUM_PIECE_WIDTH; + piecesScaled = new ArrayList(); + + int bucketCount = (int) Math.ceil((double) width / (double) pieceWidth); + int bucketSize = (int) Math.floor((double)this.pieces.size() / (double) bucketCount); + + // loop buckets + for (int i = 0; i < bucketCount; i++) { + + // Get segment of pieces that fall into bucket + int start = i * bucketSize; + + // If this is the last bucket, throw the remainder of the pieces array into it + int end = (i == bucketCount-1) ? this.pieces.size() : (i+1) * bucketSize; + + ArrayList bucket = new ArrayList(this.pieces.subList(start, end)); + + int doneCount = 0; + int downloadingCount = 0; + + // loop pieces in bucket + for(int j = 0; j < bucket.size(); j++) { + // Count downloading pieces + if (bucket.get(j) == 1) { + downloadingCount++; + } + // Count finished pieces + else if (bucket.get(j) == 2) { + doneCount++; + } + } + + int state; + // If a piece is downloading show bucket as downloading + if (downloadingCount > 0) { + state = 1; + } + // If all pieces are done, show bucket as done + else if (doneCount == bucket.size()) { + state = 2; + } + // Some done pieces, show bucket as partially done + else if (doneCount > 0) { + state = 3; + } + // bucket is not downloaded + else { + state = 0; + } + + piecesScaled.add(state); + } + + String scaledPiecesString = ""; + for (int s : piecesScaled) + { + scaledPiecesString += s; + } + + // Draw downscaled peices + for (int i = 0; i < piecesScaled.size(); i++) { + int piece = piecesScaled.get(i); + + if (piece == 0) { + continue; + } + + Paint paint = new Paint(); + switch (piece) { + case 1: + paint = downloadingPaint; + break; + case 2: + paint = donePaint; + break; + case 3: + paint = partialDonePaint; + break; + } + int x = i * pieceWidth; + + canvas.drawRect(x, 0, x + pieceWidth, height, paint); + } + } + +} + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7eb5dc56..d0a76a70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -128,6 +128,7 @@ Low priority Normal priority High priority + PIECES TRACKERS ERRORS FILES From d9594260d35688a8b5c5fc1f84633ee8760f28a6 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Fri, 6 Dec 2019 00:01:13 -0800 Subject: [PATCH 12/26] Remove duplicates from previous commit --- app/src/main/res/values-ru/strings.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cb9edf92..1cc49da8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -176,8 +176,6 @@ %1$s имеет %2$s приоритет первого и последнего куска обычый приоритет кусков - Изменено последовательное скачивание торрента - Изменен приоритет скачивания начала и конца торрента Проверка данных %1$s Торрент перемещен в \'%1$s\' Приоритеты файлов обновлены From c50dee31ea8404aab530eb98162454d386730106 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sun, 8 Dec 2019 11:12:47 -0800 Subject: [PATCH 13/26] Fix parsing bug - seq_dl and f_l_piece_prio can be missing if the torrent is new and hasn't downloaded metadata yet --- .../transdroid/daemon/Qbittorrent/QbittorrentAdapter.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 7f693b8f..b54452b4 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -521,8 +521,12 @@ public class QbittorrentAdapter implements IDaemonAdapter { ratio = tor.getDouble("ratio"); dlspeed = tor.getInt("dlspeed"); upspeed = tor.getInt("upspeed"); - dlseq = tor.getBoolean("seq_dl"); - dlflp = tor.getBoolean("f_l_piece_prio"); + if (tor.has("seq_dl")) { + dlseq = tor.getBoolean("seq_dl"); + } + if (tor.has("f_l_piece_prio")) { + dlflp = tor.getBoolean("f_l_piece_prio"); + } if (tor.has("uploaded")) { uploaded = tor.getLong("uploaded"); } else { From 548e866d2020227a7704381644fb7c310d1cb3f8 Mon Sep 17 00:00:00 2001 From: Firdaus Ahmad Date: Sat, 14 Dec 2019 01:32:09 +0800 Subject: [PATCH 14/26] Support new API --- .../Qbittorrent/QbittorrentAdapter.java | 251 ++++++++++++++---- 1 file changed, 202 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 56f8eb4a..5ace9a86 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -25,6 +25,8 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.DefaultHttpClient; @@ -70,7 +72,8 @@ public class QbittorrentAdapter implements IDaemonAdapter { private DaemonSettings settings; private DefaultHttpClient httpclient; private int version = -1; - private int apiVersion = -1; + private float apiVersion = -1; // starting from 2.3 old API is dropped so we are going to use float + private int http_response_code = -1; public QbittorrentAdapter(DaemonSettings settings) { this.settings = settings; @@ -83,18 +86,40 @@ public class QbittorrentAdapter implements IDaemonAdapter { try { + // Since 4.2.0, old API is dropped. Fallback to old one if the new one failed for version <4.2.0 // The API version is only supported since qBittorrent 3.2, so otherwise we assume version 1 + try { - String apiVerText = makeRequest(log, "/version/api"); - apiVersion = Integer.parseInt(apiVerText.trim()); + String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion", new BasicNameValuePair("username", settings.getUsername()), + new BasicNameValuePair("password", settings.getPassword())); + apiVersion = Float.parseFloat(apiVerText.trim()); } catch (DaemonException | NumberFormatException e) { - apiVersion = 1; + if (http_response_code == 403) { + try { + ensureAuthenticated(log); + String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e2) { + apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API + } + } else { + try { + String apiVerText = makeRequest(log, "/version/api"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e3) { + apiVersion = 1; + } + } } log.d(LOG_NAME, "qBittorrent API version is " + apiVersion); // The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it + // Since 4.2.0, new API version is used instead String versionText = ""; - if (apiVersion > 1) { + if (apiVersion >= (float) 2.3) { + ensureAuthenticated(log); + versionText = makeRequest(log, "/api/v2/app/version").substring(1); + } else if (apiVersion > (float) 1) { // Format is something like 'v3.2.0' versionText = makeRequest(log, "/version/qbittorrent").substring(1); } else { @@ -108,13 +133,14 @@ public class QbittorrentAdapter implements IDaemonAdapter { versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd); } } + log.d(LOG_NAME, "qBittorrent client version is " + versionText); // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) String[] parts = versionText.split("\\."); if (parts.length > 0) { version = Integer.parseInt(parts[0]) * 100 * 100; if (parts.length > 1) { - version += Integer.parseInt(parts[1]) * 100; + version += Float.parseFloat(parts[1]) * 100; if (parts.length > 2) { // For the last part only read until a non-numeric character is read // For example version 3.0.0-alpha5 is read as version code 30000 @@ -128,8 +154,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { break; } } - version += Integer.parseInt(numbers); - return; + version += Float.parseFloat(numbers); } } } @@ -146,7 +171,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { // API changed in 3.2.0, login is now handled by its own request, which provides you a cookie. // If we don't have that cookie, let's try and get it. - if (apiVersion < 2) { + if (apiVersion < (float) 2) { return; } @@ -159,8 +184,13 @@ public class QbittorrentAdapter implements IDaemonAdapter { } } - makeRequest(log, "/login", new BasicNameValuePair("username", settings.getUsername()), - new BasicNameValuePair("password", settings.getPassword())); + if (apiVersion >= (float) 2.3) { + makeRequest(log, "/api/v2/auth/login", new BasicNameValuePair("username", settings.getUsername()), + new BasicNameValuePair("password", settings.getPassword())); + } else { + makeRequest(log, "/login", new BasicNameValuePair("username", settings.getUsername()), + new BasicNameValuePair("password", settings.getPassword())); + } // The HttpClient will automatically remember the cookie for us, no need to parse it out. // However, we would like to see if authentication was successful or not... @@ -185,63 +215,113 @@ public class QbittorrentAdapter implements IDaemonAdapter { switch (task.getMethod()) { case Retrieve: + + // Request all torrents from server String path; - if (version >= 30200) { + if (version >= 40200) { + path = "/api/v2/torrents/info"; + } else if (version >= 30200) { path = "/query/torrents"; } else if (version >= 30000) { - path = "/json/torrents"; + path = "/json/torrents";; } else { path = "/json/events"; } - // Request all torrents from server JSONArray result = new JSONArray(makeRequest(log, path)); + return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonTorrents(result), parseJsonLabels(result)); case GetTorrentDetails: // Request tracker and error details for a specific teacher String mhash = task.getTargetTorrent().getUniqueID(); - JSONArray messages = - new JSONArray(makeRequest(log, (version >= 30200 ? "/query/propertiesTrackers/" : "/json/propertiesTrackers/") + mhash)); - JSONArray pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash)); + JSONArray messages; + JSONArray pieces; + if (version >= 40200) { + messages = new JSONArray(makeRequest(log, "/api/v2/torrents/trackers", new BasicNameValuePair("hash", mhash))); + pieces = new JSONArray(makeRequest(log, "/api/v2/torrents/pieceStates", new BasicNameValuePair("hash", mhash))); + } else { + messages = new JSONArray(makeRequest(log, "/query/propertiesTrackers/" + mhash)); + pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash)); + } + return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages, pieces)); case GetFileList: // Request files listing for a specific torrent String fhash = task.getTargetTorrent().getUniqueID(); - JSONArray files = - new JSONArray(makeRequest(log, (version >= 30200 ? "/query/propertiesFiles/" : "/json/propertiesFiles/") + fhash)); + JSONArray files; + if (version >= 40200) { + files = new JSONArray(makeRequest(log, "/api/v2/torrents/files", new BasicNameValuePair("hash", fhash))); + } else if (version >= 30200) { + files = new JSONArray(makeRequest(log, "/query/propertiesFiles/" + fhash)); + } else { + files = new JSONArray(makeRequest(log, "/json/propertiesFiles/" + fhash)); + } + return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFiles(files)); case AddByFile: // Upload a local .torrent file + if (version >= 40200) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/upload"; + } + String ufile = ((AddByFileTask) task).getFile(); - makeUploadRequest("/command/upload", ufile, log); + makeUploadRequest(path, ufile, log); return new DaemonTaskSuccessResult(task); case AddByUrl: // Request to add a torrent by URL String url = ((AddByUrlTask) task).getUrl(); - makeRequest(log, "/command/download", new BasicNameValuePair("urls", url)); + if (version >= 40200) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/upload"; + } + + makeRequest(log, path, new BasicNameValuePair("urls", url)); return new DaemonTaskSuccessResult(task); case AddByMagnetUrl: // Request to add a magnet link by URL String magnet = ((AddByMagnetUrlTask) task).getUrl(); - makeRequest(log, "/command/download", new BasicNameValuePair("urls", magnet)); + if (version >= 40200) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/download"; + } + + makeRequest(log, path, new BasicNameValuePair("urls", magnet)); return new DaemonTaskSuccessResult(task); case Remove: // Remove a torrent RemoveTask removeTask = (RemoveTask) task; - makeRequest(log, (removeTask.includingData() ? "/command/deletePerm" : "/command/delete"), - new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID())); + if (version >= 40200) { + if (removeTask.includingData()) { + makeRequest(log, "/api/v2/torrents/delete", + new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("deleteFiles", "true")); + } else { + makeRequest(log, "/api/v2/torrents/delete", + new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("deleteFiles", "false")); + } + + } else { + path = (removeTask.includingData() ? "/command/deletePerm" : "/command/delete"); + makeRequest(log, path, new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID())); + } + return new DaemonTaskSuccessResult(task); case Pause: @@ -253,19 +333,35 @@ public class QbittorrentAdapter implements IDaemonAdapter { case PauseAll: // Resume all torrents - makeRequest(log, "/command/pauseall"); + if (version >= 40200) { + path = "/api/v2/torrents/pause"; + } else { + path = "/command/pauseall"; + } + makeRequest(log, path); return new DaemonTaskSuccessResult(task); case Resume: // Resume a torrent - makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + if (version >= 40200) { + path = "/api/v2/torrents/resume"; + } else { + path = "/command/resume"; + } + makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); return new DaemonTaskSuccessResult(task); case ResumeAll: // Resume all torrents - makeRequest(log, "/command/resumeall"); + if (version >= 40200) { + path = "/api/v2/torrents/resume"; + makeRequest(log, path, new BasicNameValuePair("hash", "all")); + } else { + makeRequest(log, "/command/resumeall"); + } + return new DaemonTaskSuccessResult(task); case SetFilePriorities: @@ -282,33 +378,59 @@ public class QbittorrentAdapter implements IDaemonAdapter { } // We have to make a separate request per file, it seems for (TorrentFile file : setPrio.getForFiles()) { - makeRequest(log, "/command/setFilePrio", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()), + if (version >= 40200) { + path = "/api/v2/torrents/filePrio"; + } else { + path = "/command/setFilePrio"; + } + makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("id", file.getKey()), new BasicNameValuePair("priority", newPrio)); + } return new DaemonTaskSuccessResult(task); - case ForceRecheck: + case ForceRecheck: - // Force recheck a torrent - makeRequest(log, "/command/recheck", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); + // Force recheck a torrent + if (version >= 40200) { + path = "/api/v2/torrents/recheck"; + } else { + path = "/command/recheck"; + } + makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); - case ToggleSequentialDownload: + case ToggleSequentialDownload: - // Toggle sequential download mode on a torrent - makeRequest(log, "/command/toggleSequentialDownload", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); + // Toggle sequential download mode on a torrent + if (version >= 40200) { + path = "/api/v2/torrents/toggleSequentialDownload"; + } else { + path = "/command/toggleSequentialDownload"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); - case ToggleFirstLastPieceDownload: + case ToggleFirstLastPieceDownload: - // Set policy for downloading first and last piece first on a torrent - makeRequest(log, "/command/toggleFirstLastPiecePrio", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); + // Set policy for downloading first and last piece first on a torrent + if (version >= 40200) { + path = "/api/v2/torrents/toggleFirstLastPiecePrio"; + } else { + path = "/command/toggleFirstLastPiecePrio"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); case SetLabel: SetLabelTask labelTask = (SetLabelTask) task; - makeRequest(log, "/command/setCategory", + if (version >= 40200) { + path = "/api/v2/torrents/setCategory"; + } else { + path = "/command/setCategory"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("category", labelTask.getNewLabel())); return new DaemonTaskSuccessResult(task); @@ -316,7 +438,12 @@ public class QbittorrentAdapter implements IDaemonAdapter { case SetDownloadLocation: SetDownloadLocationTask setLocationTask = (SetDownloadLocationTask) task; - makeRequest(log, "/command/setLocation", + if (version >= 40200) { + path = "/api/v2/torrents/setLocation"; + } else { + path = "/command/setLocation"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), new BasicNameValuePair("location", setLocationTask.getNewLocation())); return new DaemonTaskSuccessResult(task); @@ -324,18 +451,33 @@ public class QbittorrentAdapter implements IDaemonAdapter { case SetTransferRates: // Request to set the maximum transfer rates + String pathDL; + String pathUL; SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; String dl = (ratesTask.getDownloadRate() == null ? "NaN" : Long.toString(ratesTask.getDownloadRate() * 1024)); String ul = (ratesTask.getUploadRate() == null ? "NaN" : Long.toString(ratesTask.getUploadRate() * 1024)); - makeRequest(log, "/command/setGlobalDlLimit", new BasicNameValuePair("limit", dl)); - makeRequest(log, "/command/setGlobalUpLimit", new BasicNameValuePair("limit", ul)); + if (version >= 40200) { + pathDL = "/api/v2/torrents/setDownloadLimit"; + pathUL = "/api/v2/torrents/setUploadLimit"; + } else { + pathDL = "/command/setGlobalDlLimit"; + pathUL = "/command/setGlobalUpLimit"; + } + + makeRequest(log, pathDL, new BasicNameValuePair("limit", dl)); + makeRequest(log, pathUL, new BasicNameValuePair("limit", ul)); return new DaemonTaskSuccessResult(task); case GetStats: // Refresh alternative download speeds setting - JSONObject stats = new JSONObject(makeRequest(log, "/sync/maindata?rid=0")); + if (version >= 40200) { + path = "/api/v2/sync/maindata?rid=0"; + } else { + path = "/sync/maindata?rid=0"; + } + JSONObject stats = new JSONObject(makeRequest(log, path)); JSONObject serverStats = stats.optJSONObject("server_state"); boolean alternativeSpeeds = false; if (serverStats != null) { @@ -346,7 +488,12 @@ public class QbittorrentAdapter implements IDaemonAdapter { case SetAlternativeMode: // Flip alternative speed mode - makeRequest(log, "/command/toggleAlternativeSpeedLimits"); + if (version >= 40200) { + path = "/api/v2/transfer/toggleSpeedLimitsMode"; + } else { + path = "/command/toggleAlternativeSpeedLimits"; + } + makeRequest(log, path); return new DaemonTaskSuccessResult(task); default: @@ -365,7 +512,10 @@ public class QbittorrentAdapter implements IDaemonAdapter { try { // Setup request using POST - HttpPost httppost = new HttpPost(buildWebUIUrl(path)); + String url_to_request = buildWebUIUrl(path); + HttpPost httppost = new HttpPost(url_to_request); + log.d(LOG_NAME, "URL to request: "+ url_to_request); + List nvps = new ArrayList<>(); Collections.addAll(nvps, params); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); @@ -377,6 +527,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } + private String makeUploadRequest(String path, String file, Log log) throws DaemonException { try { @@ -394,7 +545,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } - private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException { + private String makeWebRequest(HttpRequestBase httpmethod, Log log) throws DaemonException { try { @@ -404,7 +555,9 @@ public class QbittorrentAdapter implements IDaemonAdapter { } // Execute - HttpResponse response = httpclient.execute(httppost); + HttpResponse response = httpclient.execute(httpmethod); + http_response_code = response.getStatusLine().getStatusCode(); + log.d(LOG_NAME, "Response code is: " + http_response_code); HttpEntity entity = response.getEntity(); if (entity != null) { From d1facac2c1e86ae7a305ff061f655fc2dabcad08 Mon Sep 17 00:00:00 2001 From: Firdaus Ahmad Date: Mon, 16 Dec 2019 03:52:22 +0800 Subject: [PATCH 15/26] Fixed resume and pause in qBittorrent --- .../Qbittorrent/QbittorrentAdapter.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 5ace9a86..e0a50ad7 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -223,7 +223,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } else if (version >= 30200) { path = "/query/torrents"; } else if (version >= 30000) { - path = "/json/torrents";; + path = "/json/torrents"; } else { path = "/json/events"; } @@ -327,29 +327,34 @@ public class QbittorrentAdapter implements IDaemonAdapter { case Pause: // Pause a torrent - makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + if (version >= 40200) { + makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + } else { + makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + } + return new DaemonTaskSuccessResult(task); case PauseAll: // Resume all torrents if (version >= 40200) { - path = "/api/v2/torrents/pause"; + makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", "all")); } else { - path = "/command/pauseall"; + makeRequest(log, "/command/pauseall"); } - makeRequest(log, path); + return new DaemonTaskSuccessResult(task); case Resume: // Resume a torrent if (version >= 40200) { - path = "/api/v2/torrents/resume"; + makeRequest(log, "/api/v2/torrents/resume", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); } else { - path = "/command/resume"; + makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); } - makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); case ResumeAll: @@ -357,7 +362,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { // Resume all torrents if (version >= 40200) { path = "/api/v2/torrents/resume"; - makeRequest(log, path, new BasicNameValuePair("hash", "all")); + makeRequest(log, path, new BasicNameValuePair("hashes", "all")); } else { makeRequest(log, "/command/resumeall"); } @@ -527,7 +532,6 @@ public class QbittorrentAdapter implements IDaemonAdapter { } - private String makeUploadRequest(String path, String file, Log log) throws DaemonException { try { From 5cf18931e16f436b3f3f7d957178e4211421f547 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Fri, 3 Jan 2020 23:27:32 -0800 Subject: [PATCH 16/26] Untangle try-catch blocks for version checking --- .../Qbittorrent/QbittorrentAdapter.java | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index e0a50ad7..b23c1c60 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -89,28 +89,39 @@ public class QbittorrentAdapter implements IDaemonAdapter { // Since 4.2.0, old API is dropped. Fallback to old one if the new one failed for version <4.2.0 // The API version is only supported since qBittorrent 3.2, so otherwise we assume version 1 + boolean is_v2 = false; + + // First, try the v2 api version endpoint try { - String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion", new BasicNameValuePair("username", settings.getUsername()), - new BasicNameValuePair("password", settings.getPassword())); + String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { - if (http_response_code == 403) { - try { - ensureAuthenticated(log); - String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e2) { - apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API - } - } else { - try { - String apiVerText = makeRequest(log, "/version/api"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e3) { - apiVersion = 1; - } - } - } + } catch (DaemonException | NumberFormatException e) { + is_v2 = http_response_code == 403; + } + + // Keep trying + if (is_v2) { + // Preemptive assumption, for authentication + apiVersion = (float) 2.3; + + // Authenticate, and try v2 again + try { + ensureAuthenticated(log); + String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e) { + apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API + } + } else { + // Fall back to old api + try { + String apiVerText = makeRequest(log, "/version/api"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e) { + apiVersion = 1; + } + } + log.d(LOG_NAME, "qBittorrent API version is " + apiVersion); // The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it From 2cb1b0985854ece0e48ad237adcd198d60ca9b5f Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sat, 4 Jan 2020 00:06:35 -0800 Subject: [PATCH 17/26] Replace return code int flag with DaemonException for 403 detection --- .../Qbittorrent/QbittorrentAdapter.java | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index b23c1c60..233ee9d5 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -73,7 +73,6 @@ public class QbittorrentAdapter implements IDaemonAdapter { private DefaultHttpClient httpclient; private int version = -1; private float apiVersion = -1; // starting from 2.3 old API is dropped so we are going to use float - private int http_response_code = -1; public QbittorrentAdapter(DaemonSettings settings) { this.settings = settings; @@ -95,30 +94,36 @@ public class QbittorrentAdapter implements IDaemonAdapter { try { String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { - is_v2 = http_response_code == 403; + } catch (DaemonException e) { + // 403 Forbidden - endpoint exists. Keep trying v2 + is_v2 = e.getType() == ExceptionType.AuthenticationFailure; + } catch (NumberFormatException e) { + // Assume endpoint exists and is reachable, set lowest possible version and stop trying + apiVersion = (float) 2.3; } // Keep trying - if (is_v2) { - // Preemptive assumption, for authentication - apiVersion = (float) 2.3; - - // Authenticate, and try v2 again - try { - ensureAuthenticated(log); - String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { - apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API - } - } else { - // Fall back to old api - try { - String apiVerText = makeRequest(log, "/version/api"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { - apiVersion = 1; + if (apiVersion < 0) { + if (is_v2) { + // Preemptive assumption, for authentication + apiVersion = (float) 2.3; + + // Authenticate, and try v2 again + try { + ensureAuthenticated(log); + String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e) { + apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API + } + } else { + // Fall back to old api + try { + String apiVerText = makeRequest(log, "/version/api"); + apiVersion = Float.parseFloat(apiVerText.trim()); + } catch (DaemonException | NumberFormatException e) { + apiVersion = 1; + } } } @@ -571,8 +576,11 @@ public class QbittorrentAdapter implements IDaemonAdapter { // Execute HttpResponse response = httpclient.execute(httpmethod); - http_response_code = response.getStatusLine().getStatusCode(); - log.d(LOG_NAME, "Response code is: " + http_response_code); + + // Throw exception on 403 + if (response.getStatusLine().getStatusCode() == 403) { + throw new DaemonException(ExceptionType.AuthenticationFailure, "Response code 403"); + } HttpEntity entity = response.getEntity(); if (entity != null) { @@ -594,7 +602,13 @@ public class QbittorrentAdapter implements IDaemonAdapter { } catch (Exception e) { log.d(LOG_NAME, "Error: " + e.toString()); - throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + + if (e instanceof DaemonException) { + throw (DaemonException) e; + } + else { + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } } } From 8371b39cdfee34e788e80dbf585268ea2dc0b51d Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sat, 4 Jan 2020 14:31:15 -0800 Subject: [PATCH 18/26] Remove unnecessary http methods --- .../transdroid/daemon/Qbittorrent/QbittorrentAdapter.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 233ee9d5..53e20f88 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -25,8 +25,6 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.DefaultHttpClient; @@ -565,7 +563,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } - private String makeWebRequest(HttpRequestBase httpmethod, Log log) throws DaemonException { + private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException { try { @@ -575,7 +573,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } // Execute - HttpResponse response = httpclient.execute(httpmethod); + HttpResponse response = httpclient.execute(httppost); // Throw exception on 403 if (response.getStatusLine().getStatusCode() == 403) { From 21abb678f5ac793d80c9c04f1ebbc3dbecdcaf48 Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sat, 4 Jan 2020 21:50:33 -0800 Subject: [PATCH 19/26] Parse api version to int same as app version (2.3.0 => 20300) --- .../Qbittorrent/QbittorrentAdapter.java | 96 ++++++++++--------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 53e20f88..054f4d96 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -70,7 +70,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { private DaemonSettings settings; private DefaultHttpClient httpclient; private int version = -1; - private float apiVersion = -1; // starting from 2.3 old API is dropped so we are going to use float + private int apiVersion = -1; public QbittorrentAdapter(DaemonSettings settings) { this.settings = settings; @@ -87,53 +87,53 @@ public class QbittorrentAdapter implements IDaemonAdapter { // The API version is only supported since qBittorrent 3.2, so otherwise we assume version 1 boolean is_v2 = false; + String apiVersionText = ""; // First, try the v2 api version endpoint try { - String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); - apiVersion = Float.parseFloat(apiVerText.trim()); + apiVersionText = makeRequest(log, "/api/v2/app/webapiVersion"); } catch (DaemonException e) { // 403 Forbidden - endpoint exists. Keep trying v2 is_v2 = e.getType() == ExceptionType.AuthenticationFailure; - } catch (NumberFormatException e) { - // Assume endpoint exists and is reachable, set lowest possible version and stop trying - apiVersion = (float) 2.3; } // Keep trying if (apiVersion < 0) { if (is_v2) { // Preemptive assumption, for authentication - apiVersion = (float) 2.3; + apiVersion = 20300; //2.3.0 // Authenticate, and try v2 again try { ensureAuthenticated(log); - String apiVerText = makeRequest(log, "/api/v2/app/webapiVersion"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { - apiVersion = (float) 2.3; // assume this is new API since we are forbidden to access API + apiVersionText = makeRequest(log, "/api/v2/app/webapiVersion"); + } catch (DaemonException e) { + apiVersion = 20300; // assume this is new API 2.3.0 since we are forbidden to access API } } else { // Fall back to old api try { - String apiVerText = makeRequest(log, "/version/api"); - apiVersion = Float.parseFloat(apiVerText.trim()); - } catch (DaemonException | NumberFormatException e) { + apiVersionText = makeRequest(log, "/version/api"); + } catch (DaemonException e) { apiVersion = 1; } } } + if (apiVersion < 0) { + apiVersion = parseVersionNumber(apiVersionText); + } + log.d(LOG_NAME, "qBittorrent API version is " + apiVersion); + // The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it // Since 4.2.0, new API version is used instead String versionText = ""; - if (apiVersion >= (float) 2.3) { + if (apiVersion >= 20300) { ensureAuthenticated(log); versionText = makeRequest(log, "/api/v2/app/version").substring(1); - } else if (apiVersion > (float) 1) { + } else if (apiVersion > 10000) { // Format is something like 'v3.2.0' versionText = makeRequest(log, "/version/qbittorrent").substring(1); } else { @@ -149,43 +149,49 @@ public class QbittorrentAdapter implements IDaemonAdapter { } log.d(LOG_NAME, "qBittorrent client version is " + versionText); - // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) - String[] parts = versionText.split("\\."); - if (parts.length > 0) { - version = Integer.parseInt(parts[0]) * 100 * 100; - if (parts.length > 1) { - version += Float.parseFloat(parts[1]) * 100; - if (parts.length > 2) { - // For the last part only read until a non-numeric character is read - // For example version 3.0.0-alpha5 is read as version code 30000 - String numbers = ""; - for (char c : parts[2].toCharArray()) { - if (Character.isDigit(c)) - // Still a number; add it to the numbers string - numbers += Character.toString(c); - else { - // No longer reading numbers; stop reading - break; - } - } - version += Float.parseFloat(numbers); - } - } - } + version = parseVersionNumber(versionText); } catch (Exception e) { // Unable to establish version number; assume an old version by setting it to version 1 version = 10000; - apiVersion = 1; + apiVersion = 10000; } } + private int parseVersionNumber(String versionText) { + // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) + int version = -1; + String[] parts = versionText.split("\\."); + if (parts.length > 0) { + version = Integer.parseInt(parts[0]) * 100 * 100; + if (parts.length > 1) { + version += Float.parseFloat(parts[1]) * 100; + if (parts.length > 2) { + // For the last part only read until a non-numeric character is read + // For example version 3.0.0-alpha5 is read as version code 30000 + String numbers = ""; + for (char c : parts[2].toCharArray()) { + if (Character.isDigit(c)) + // Still a number; add it to the numbers string + numbers += Character.toString(c); + else { + // No longer reading numbers; stop reading + break; + } + } + version += Float.parseFloat(numbers); + } + } + } + return version; + } + private synchronized void ensureAuthenticated(Log log) throws DaemonException { // API changed in 3.2.0, login is now handled by its own request, which provides you a cookie. // If we don't have that cookie, let's try and get it. - if (apiVersion < (float) 2) { + if (apiVersion < 20000) { return; } @@ -198,7 +204,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } } - if (apiVersion >= (float) 2.3) { + if (apiVersion >= 20300) { makeRequest(log, "/api/v2/auth/login", new BasicNameValuePair("username", settings.getUsername()), new BasicNameValuePair("password", settings.getPassword())); } else { @@ -666,7 +672,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { Map labels = new HashMap<>(); for (int i = 0; i < response.length(); i++) { JSONObject tor = response.getJSONObject(i); - if (apiVersion >= 2) { + if (apiVersion >= 20000) { String label = tor.optString("category"); if (label != null && label.length() > 0) { final Label labelObject = labels.get(label); @@ -698,7 +704,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { Date completionOn = null; String label = null; - if (apiVersion >= 2) { + if (apiVersion >= 20000) { leechers = new int[2]; leechers[0] = tor.getInt("num_leechs"); leechers[1] = tor.getInt("num_complete") + tor.getInt("num_incomplete"); @@ -890,7 +896,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { JSONObject file = response.getJSONObject(i); long size; - if (apiVersion >= 2) { + if (apiVersion >= 20000) { size = file.getLong("size"); } else { size = parseSize(file.getString("size")); From c48ccf97ef99b629ea70327cb89aefe774c169fc Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sat, 4 Jan 2020 22:22:42 -0800 Subject: [PATCH 20/26] Fix recheck command for v2 --- .../org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index 054f4d96..e2b2e7e1 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -422,7 +422,7 @@ public class QbittorrentAdapter implements IDaemonAdapter { } else { path = "/command/recheck"; } - makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); return new DaemonTaskSuccessResult(task); case ToggleSequentialDownload: From 7120a1154067633999ec35b34640329911f6f62b Mon Sep 17 00:00:00 2001 From: Phillip Dykman Date: Sat, 4 Jan 2020 22:42:01 -0800 Subject: [PATCH 21/26] Remove debug messages --- .../transdroid/daemon/Qbittorrent/QbittorrentAdapter.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index e2b2e7e1..a0266f8a 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -124,9 +124,6 @@ public class QbittorrentAdapter implements IDaemonAdapter { apiVersion = parseVersionNumber(apiVersionText); } - log.d(LOG_NAME, "qBittorrent API version is " + apiVersion); - - // The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it // Since 4.2.0, new API version is used instead String versionText = ""; @@ -147,7 +144,6 @@ public class QbittorrentAdapter implements IDaemonAdapter { versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd); } } - log.d(LOG_NAME, "qBittorrent client version is " + versionText); version = parseVersionNumber(versionText); @@ -539,7 +535,6 @@ public class QbittorrentAdapter implements IDaemonAdapter { // Setup request using POST String url_to_request = buildWebUIUrl(path); HttpPost httppost = new HttpPost(url_to_request); - log.d(LOG_NAME, "URL to request: "+ url_to_request); List nvps = new ArrayList<>(); Collections.addAll(nvps, params); From b80430a6ff54628e31df5ad8a4501f1367061eeb Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Mon, 6 Jan 2020 11:29:02 +0100 Subject: [PATCH 22/26] More qBittorrent API version change fixes cf #516 --- .../Qbittorrent/QbittorrentAdapter.java | 1671 ++++++++--------- 1 file changed, 825 insertions(+), 846 deletions(-) diff --git a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java index a0266f8a..57fdc592 100644 --- a/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java +++ b/app/src/main/java/org/transdroid/daemon/Qbittorrent/QbittorrentAdapter.java @@ -1,19 +1,19 @@ /* * 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.Qbittorrent; @@ -45,7 +45,26 @@ 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.*; +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.GetStatsTask; +import org.transdroid.daemon.task.GetStatsTaskSuccessResult; +import org.transdroid.daemon.task.GetTorrentDetailsTask; +import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult; +import org.transdroid.daemon.task.RemoveTask; +import org.transdroid.daemon.task.RetrieveTask; +import org.transdroid.daemon.task.RetrieveTaskSuccessResult; +import org.transdroid.daemon.task.SetDownloadLocationTask; +import org.transdroid.daemon.task.SetFilePriorityTask; +import org.transdroid.daemon.task.SetLabelTask; +import org.transdroid.daemon.task.SetTransferRatesTask; import org.transdroid.daemon.util.HttpHelper; import java.io.File; @@ -61,872 +80,832 @@ import java.util.Map; /** * The daemon adapter for the qBittorrent torrent client. + * * @author erickok */ public class QbittorrentAdapter implements IDaemonAdapter { - private static final String LOG_NAME = "qBittorrent daemon"; - - private DaemonSettings settings; - private DefaultHttpClient httpclient; - private int version = -1; - private int apiVersion = -1; + private static final String LOG_NAME = "qBittorrent daemon"; + + private DaemonSettings settings; + private DefaultHttpClient httpclient; + private int version = -1; + + public QbittorrentAdapter(DaemonSettings settings) { + this.settings = settings; + } + + private synchronized void ensureVersion(Log log) { + // Still need to retrieve the API and qBittorrent version numbers from the server? + if (version > 0) + return; + + // Since 4.1, API v2 is used. Since qBittorrent 3.2, API v1 is used. Otherwise we use unofficial legacy json endpoints. + try { + String versionText = ""; + try { + // Try v2 API first, which returns version number in 'v4.1.9' format + versionText = makeRequest(log, "/api/v2/app/version").substring(1); + } catch (Exception e1) { + // Try v1 API, which returns version number in 'v3.2.0' format + try { + versionText = makeRequest(log, "/version/qbittorrent").substring(1); + } catch (Exception e2) { + // Legacy mode; format is something like 'qBittorrent v2.9.7 (Web UI)' or 'qBittorrent v3.0.0-alpha5 (Web UI)' + String about = makeRequest(log, "/about.html"); + String aboutStartText = "qBittorrent v"; + String aboutEndText = " (Web UI)"; + int aboutStart = about.indexOf(aboutStartText); + int aboutEnd = about.indexOf(aboutEndText); + if (aboutStart >= 0 && aboutEnd > aboutStart) { + versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd); + } + } + } - public QbittorrentAdapter(DaemonSettings settings) { - this.settings = settings; - } + version = parseVersionNumber(versionText); - private synchronized void ensureVersion(Log log) throws DaemonException { - // Still need to retrieve the API and qBittorrent version numbers from the server? - if (version > 0 && apiVersion > 0) - return; + } catch (Exception e) { + // Unable to establish version number; assume an old version by setting it to version 1 + version = 10000; + } - try { + } + + private int parseVersionNumber(String versionText) { + // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) + int version = -1; + String[] parts = versionText.split("\\."); + if (parts.length > 0) { + version = Integer.parseInt(parts[0]) * 100 * 100; + if (parts.length > 1) { + version += Integer.parseInt(parts[1]) * 100; + if (parts.length > 2) { + // For the last part only read until a non-numeric character is read + // For example version 3.0.0-alpha5 is read as version code 30000 + String numbers = ""; + for (char c : parts[2].toCharArray()) { + if (Character.isDigit(c)) + // Still a number; add it to the numbers string + numbers += Character.toString(c); + else { + // No longer reading numbers; stop reading + break; + } + } + version += Integer.parseInt(numbers); + } + } + } + return version; + } + + private synchronized void ensureAuthenticated(Log log) throws DaemonException { + // API changed in 3.2.0, login is now handled by its own request, which provides you a cookie. + // If we don't have that cookie, let's try and get it. + if (version != -1 && version < 30200) { + return; + } - // Since 4.2.0, old API is dropped. Fallback to old one if the new one failed for version <4.2.0 - // The API version is only supported since qBittorrent 3.2, so otherwise we assume version 1 + // Have we already authenticated? Check if we have the cookie that we need + if (isAuthenticated()) { + return; + } - boolean is_v2 = false; - String apiVersionText = ""; + final BasicNameValuePair usernameParam = new BasicNameValuePair("username", settings.getUsername()); + final BasicNameValuePair passwordParam = new BasicNameValuePair("password", settings.getPassword()); + if (version == -1 || version >= 40100) { + try { + makeRequest(log, "/api/v2/auth/login", usernameParam, passwordParam); + } catch (DaemonException ignored) { + } + } + if (!isAuthenticated()) { + try { + makeRequest(log, "/login", usernameParam, passwordParam); + } catch (DaemonException ignored) { + } + } - // First, try the v2 api version endpoint - try { - apiVersionText = makeRequest(log, "/api/v2/app/webapiVersion"); - } catch (DaemonException e) { - // 403 Forbidden - endpoint exists. Keep trying v2 - is_v2 = e.getType() == ExceptionType.AuthenticationFailure; + if (!isAuthenticated()) { + throw new DaemonException(ExceptionType.AuthenticationFailure, "Server rejected our login"); + } + } + + private boolean isAuthenticated() { + List cookies = httpclient.getCookieStore().getCookies(); + for (Cookie c : cookies) { + if (c.getName().equals("SID")) { + // And here it is! Okay, no need authenticate again. + return true; + } + } + return false; + } + + @Override + public DaemonTaskResult executeTask(Log log, DaemonTask task) { + + try { + initialise(); + ensureAuthenticated(log); + ensureVersion(log); + + switch (task.getMethod()) { + case Retrieve: + + // Request all torrents from server + String path; + if (version >= 40100) { + path = "/api/v2/torrents/info"; + } else if (version >= 30200) { + path = "/query/torrents"; + } else if (version >= 30000) { + path = "/json/torrents"; + } else { + path = "/json/events"; + } + + JSONArray result = new JSONArray(makeRequest(log, path)); + + return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonTorrents(result), parseJsonLabels(result)); + + case GetTorrentDetails: + + // Request tracker and error details for a specific teacher + String mhash = task.getTargetTorrent().getUniqueID(); + JSONArray messages; + JSONArray pieces; + if (version >= 40100) { + messages = new JSONArray(makeRequest(log, "/api/v2/torrents/trackers", new BasicNameValuePair("hash", mhash))); + pieces = new JSONArray(makeRequest(log, "/api/v2/torrents/pieceStates", new BasicNameValuePair("hash", mhash))); + } else { + messages = new JSONArray(makeRequest(log, "/query/propertiesTrackers/" + mhash)); + pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash)); + } + + return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages, pieces)); + + case GetFileList: + + // Request files listing for a specific torrent + String fhash = task.getTargetTorrent().getUniqueID(); + JSONArray files; + if (version >= 40100) { + files = new JSONArray(makeRequest(log, "/api/v2/torrents/files", new BasicNameValuePair("hash", fhash))); + } else if (version >= 30200) { + files = new JSONArray(makeRequest(log, "/query/propertiesFiles/" + fhash)); + } else { + files = new JSONArray(makeRequest(log, "/json/propertiesFiles/" + fhash)); + } + + return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFiles(files)); + + case AddByFile: + + // Upload a local .torrent file + if (version >= 40100) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/upload"; + } + + String ufile = ((AddByFileTask) task).getFile(); + makeUploadRequest(path, ufile, log); + return new DaemonTaskSuccessResult(task); + + case AddByUrl: + + // Request to add a torrent by URL + String url = ((AddByUrlTask) task).getUrl(); + if (version >= 40100) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/upload"; + } + + makeRequest(log, path, new BasicNameValuePair("urls", url)); + return new DaemonTaskSuccessResult(task); + + case AddByMagnetUrl: + + // Request to add a magnet link by URL + String magnet = ((AddByMagnetUrlTask) task).getUrl(); + if (version >= 40100) { + path = "/api/v2/torrents/add"; + } else { + path = "/command/download"; + } + + makeRequest(log, path, new BasicNameValuePair("urls", magnet)); + return new DaemonTaskSuccessResult(task); + + case Remove: + + // Remove a torrent + RemoveTask removeTask = (RemoveTask) task; + if (version >= 40100) { + if (removeTask.includingData()) { + makeRequest(log, "/api/v2/torrents/delete", + new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("deleteFiles", "true")); + } else { + makeRequest(log, "/api/v2/torrents/delete", + new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("deleteFiles", "false")); } - // Keep trying - if (apiVersion < 0) { - if (is_v2) { - // Preemptive assumption, for authentication - apiVersion = 20300; //2.3.0 - - // Authenticate, and try v2 again - try { - ensureAuthenticated(log); - apiVersionText = makeRequest(log, "/api/v2/app/webapiVersion"); - } catch (DaemonException e) { - apiVersion = 20300; // assume this is new API 2.3.0 since we are forbidden to access API - } - } else { - // Fall back to old api - try { - apiVersionText = makeRequest(log, "/version/api"); - } catch (DaemonException e) { - apiVersion = 1; - } - } + } else { + path = (removeTask.includingData() ? "/command/deletePerm" : "/command/delete"); + makeRequest(log, path, new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID())); + } + + return new DaemonTaskSuccessResult(task); + + case Pause: + + // Pause a torrent + if (version >= 40100) { + makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + } else { + makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + } + + return new DaemonTaskSuccessResult(task); + + case PauseAll: + + // Resume all torrents + if (version >= 40100) { + makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", "all")); + } else { + makeRequest(log, "/command/pauseall"); + } + + return new DaemonTaskSuccessResult(task); + + case Resume: + + // Resume a torrent + if (version >= 40100) { + makeRequest(log, "/api/v2/torrents/resume", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + } else { + makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); + } + + return new DaemonTaskSuccessResult(task); + + case ResumeAll: + + // Resume all torrents + if (version >= 40100) { + path = "/api/v2/torrents/resume"; + makeRequest(log, path, new BasicNameValuePair("hashes", "all")); + } else { + makeRequest(log, "/command/resumeall"); + } + + return new DaemonTaskSuccessResult(task); + + case SetFilePriorities: + + // Update the priorities to a set of files + SetFilePriorityTask setPrio = (SetFilePriorityTask) task; + String newPrio = "0"; + if (setPrio.getNewPriority() == Priority.Low) { + newPrio = "1"; + } else if (setPrio.getNewPriority() == Priority.Normal) { + newPrio = "2"; + } else if (setPrio.getNewPriority() == Priority.High) { + newPrio = "7"; + } + // We have to make a separate request per file, it seems + for (TorrentFile file : setPrio.getForFiles()) { + if (version >= 40100) { + path = "/api/v2/torrents/filePrio"; + } else { + path = "/command/setFilePrio"; } + makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("id", file.getKey()), new BasicNameValuePair("priority", newPrio)); + + } + return new DaemonTaskSuccessResult(task); + + case ForceRecheck: + + // Force recheck a torrent + if (version >= 40100) { + path = "/api/v2/torrents/recheck"; + } else { + path = "/command/recheck"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); + + case ToggleSequentialDownload: + + // Toggle sequential download mode on a torrent + if (version >= 40100) { + path = "/api/v2/torrents/toggleSequentialDownload"; + } else { + path = "/command/toggleSequentialDownload"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); + + case ToggleFirstLastPieceDownload: + + // Set policy for downloading first and last piece first on a torrent + if (version >= 40100) { + path = "/api/v2/torrents/toggleFirstLastPiecePrio"; + } else { + path = "/command/toggleFirstLastPiecePrio"; + } + makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); + return new DaemonTaskSuccessResult(task); + + case SetLabel: + + SetLabelTask labelTask = (SetLabelTask) task; + if (version >= 40100) { + path = "/api/v2/torrents/setCategory"; + } else { + path = "/command/setCategory"; + } + makeRequest(log, path, + new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("category", labelTask.getNewLabel())); + return new DaemonTaskSuccessResult(task); + + case SetDownloadLocation: + + SetDownloadLocationTask setLocationTask = (SetDownloadLocationTask) task; + if (version >= 40100) { + path = "/api/v2/torrents/setLocation"; + } else { + path = "/command/setLocation"; + } + makeRequest(log, path, + new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), + new BasicNameValuePair("location", setLocationTask.getNewLocation())); + return new DaemonTaskSuccessResult(task); + + case SetTransferRates: + + // Request to set the maximum transfer rates + String pathDL; + String pathUL; + SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; + String dl = (ratesTask.getDownloadRate() == null ? "NaN" : Long.toString(ratesTask.getDownloadRate() * 1024)); + String ul = (ratesTask.getUploadRate() == null ? "NaN" : Long.toString(ratesTask.getUploadRate() * 1024)); + + if (version >= 40100) { + pathDL = "/api/v2/torrents/setDownloadLimit"; + pathUL = "/api/v2/torrents/setUploadLimit"; + } else { + pathDL = "/command/setGlobalDlLimit"; + pathUL = "/command/setGlobalUpLimit"; + } + + makeRequest(log, pathDL, new BasicNameValuePair("limit", dl)); + makeRequest(log, pathUL, new BasicNameValuePair("limit", ul)); + return new DaemonTaskSuccessResult(task); + + case GetStats: + + // Refresh alternative download speeds setting + if (version >= 40100) { + path = "/api/v2/sync/maindata?rid=0"; + } else { + path = "/sync/maindata?rid=0"; + } + JSONObject stats = new JSONObject(makeRequest(log, path)); + JSONObject serverStats = stats.optJSONObject("server_state"); + boolean alternativeSpeeds = false; + if (serverStats != null) { + alternativeSpeeds = serverStats.optBoolean("use_alt_speed_limits"); + } + return new GetStatsTaskSuccessResult((GetStatsTask) task, alternativeSpeeds, -1); + + case SetAlternativeMode: + + // Flip alternative speed mode + if (version >= 40100) { + path = "/api/v2/transfer/toggleSpeedLimitsMode"; + } else { + path = "/command/toggleAlternativeSpeedLimits"; + } + makeRequest(log, path); + 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); + } + } - if (apiVersion < 0) { - apiVersion = parseVersionNumber(apiVersionText); - } + private String makeRequest(Log log, String path, NameValuePair... params) throws DaemonException { - // The qBittorent version is only supported since 3.2; for earlier versions we parse the about dialog and parse it - // Since 4.2.0, new API version is used instead - String versionText = ""; - if (apiVersion >= 20300) { - ensureAuthenticated(log); - versionText = makeRequest(log, "/api/v2/app/version").substring(1); - } else if (apiVersion > 10000) { - // Format is something like 'v3.2.0' - versionText = makeRequest(log, "/version/qbittorrent").substring(1); - } else { - // Format is something like 'qBittorrent v2.9.7 (Web UI)' or 'qBittorrent v3.0.0-alpha5 (Web UI)' - String about = makeRequest(log, "/about.html"); - String aboutStartText = "qBittorrent v"; - String aboutEndText = " (Web UI)"; - int aboutStart = about.indexOf(aboutStartText); - int aboutEnd = about.indexOf(aboutEndText); - if (aboutStart >= 0 && aboutEnd > aboutStart) { - versionText = about.substring(aboutStart + aboutStartText.length(), aboutEnd); - } - } - - version = parseVersionNumber(versionText); - - } catch (Exception e) { - // Unable to establish version number; assume an old version by setting it to version 1 - version = 10000; - apiVersion = 10000; - } - - } - - private int parseVersionNumber(String versionText) { - // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) - int version = -1; - String[] parts = versionText.split("\\."); - if (parts.length > 0) { - version = Integer.parseInt(parts[0]) * 100 * 100; - if (parts.length > 1) { - version += Float.parseFloat(parts[1]) * 100; - if (parts.length > 2) { - // For the last part only read until a non-numeric character is read - // For example version 3.0.0-alpha5 is read as version code 30000 - String numbers = ""; - for (char c : parts[2].toCharArray()) { - if (Character.isDigit(c)) - // Still a number; add it to the numbers string - numbers += Character.toString(c); - else { - // No longer reading numbers; stop reading - break; - } - } - version += Float.parseFloat(numbers); - } - } - } - return version; + try { + + // Setup request using POST + String url_to_request = buildWebUIUrl(path); + HttpPost httppost = new HttpPost(url_to_request); + + List nvps = new ArrayList<>(); + Collections.addAll(nvps, params); + httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); + return makeWebRequest(httppost, log); + + } catch (UnsupportedEncodingException e) { + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); } - private synchronized void ensureAuthenticated(Log log) throws DaemonException { - // API changed in 3.2.0, login is now handled by its own request, which provides you a cookie. - // If we don't have that cookie, let's try and get it. - - if (apiVersion < 20000) { - return; - } - - // Have we already authenticated? Check if we have the cookie that we need - List cookies = httpclient.getCookieStore().getCookies(); - for (Cookie c : cookies) { - if (c.getName().equals("SID")) { - // And here it is! Okay, no need authenticate again. - return; - } - } - - if (apiVersion >= 20300) { - makeRequest(log, "/api/v2/auth/login", new BasicNameValuePair("username", settings.getUsername()), - new BasicNameValuePair("password", settings.getPassword())); - } else { - makeRequest(log, "/login", new BasicNameValuePair("username", settings.getUsername()), - new BasicNameValuePair("password", settings.getPassword())); - } - // The HttpClient will automatically remember the cookie for us, no need to parse it out. - - // However, we would like to see if authentication was successful or not... - cookies = httpclient.getCookieStore().getCookies(); - for (Cookie c : cookies) { - if (c.getName().equals("SID")) { - // Good. Let's get out of here. - return; - } - } - - // No cookie found, we didn't authenticate. - throw new DaemonException(ExceptionType.AuthenticationFailure, "Server rejected our login"); - } - - @Override - public DaemonTaskResult executeTask(Log log, DaemonTask task) { - - try { - ensureVersion(log); - ensureAuthenticated(log); - - switch (task.getMethod()) { - case Retrieve: - - // Request all torrents from server - String path; - if (version >= 40200) { - path = "/api/v2/torrents/info"; - } else if (version >= 30200) { - path = "/query/torrents"; - } else if (version >= 30000) { - path = "/json/torrents"; - } else { - path = "/json/events"; - } - - JSONArray result = new JSONArray(makeRequest(log, path)); - - return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonTorrents(result), parseJsonLabels(result)); - - case GetTorrentDetails: - - // Request tracker and error details for a specific teacher - String mhash = task.getTargetTorrent().getUniqueID(); - JSONArray messages; - JSONArray pieces; - if (version >= 40200) { - messages = new JSONArray(makeRequest(log, "/api/v2/torrents/trackers", new BasicNameValuePair("hash", mhash))); - pieces = new JSONArray(makeRequest(log, "/api/v2/torrents/pieceStates", new BasicNameValuePair("hash", mhash))); - } else { - messages = new JSONArray(makeRequest(log, "/query/propertiesTrackers/" + mhash)); - pieces = new JSONArray(makeRequest(log, "/query/getPieceStates/" + mhash)); - } - - return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(messages, pieces)); - - case GetFileList: - - // Request files listing for a specific torrent - String fhash = task.getTargetTorrent().getUniqueID(); - JSONArray files; - if (version >= 40200) { - files = new JSONArray(makeRequest(log, "/api/v2/torrents/files", new BasicNameValuePair("hash", fhash))); - } else if (version >= 30200) { - files = new JSONArray(makeRequest(log, "/query/propertiesFiles/" + fhash)); - } else { - files = new JSONArray(makeRequest(log, "/json/propertiesFiles/" + fhash)); - } - - return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFiles(files)); - - case AddByFile: - - // Upload a local .torrent file - if (version >= 40200) { - path = "/api/v2/torrents/add"; - } else { - path = "/command/upload"; - } - - String ufile = ((AddByFileTask) task).getFile(); - makeUploadRequest(path, ufile, log); - return new DaemonTaskSuccessResult(task); - - case AddByUrl: - - // Request to add a torrent by URL - String url = ((AddByUrlTask) task).getUrl(); - if (version >= 40200) { - path = "/api/v2/torrents/add"; - } else { - path = "/command/upload"; - } - - makeRequest(log, path, new BasicNameValuePair("urls", url)); - return new DaemonTaskSuccessResult(task); - - case AddByMagnetUrl: - - // Request to add a magnet link by URL - String magnet = ((AddByMagnetUrlTask) task).getUrl(); - if (version >= 40200) { - path = "/api/v2/torrents/add"; - } else { - path = "/command/download"; - } - - makeRequest(log, path, new BasicNameValuePair("urls", magnet)); - return new DaemonTaskSuccessResult(task); - - case Remove: - - // Remove a torrent - RemoveTask removeTask = (RemoveTask) task; - if (version >= 40200) { - if (removeTask.includingData()) { - makeRequest(log, "/api/v2/torrents/delete", - new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), - new BasicNameValuePair("deleteFiles", "true")); - } else { - makeRequest(log, "/api/v2/torrents/delete", - new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID()), - new BasicNameValuePair("deleteFiles", "false")); - } - - } else { - path = (removeTask.includingData() ? "/command/deletePerm" : "/command/delete"); - makeRequest(log, path, new BasicNameValuePair("hashes", removeTask.getTargetTorrent().getUniqueID())); - } - - return new DaemonTaskSuccessResult(task); - - case Pause: - - // Pause a torrent - if (version >= 40200) { - makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - } else { - makeRequest(log, "/command/pause", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); - } - - return new DaemonTaskSuccessResult(task); - - case PauseAll: - - // Resume all torrents - if (version >= 40200) { - makeRequest(log, "/api/v2/torrents/pause", new BasicNameValuePair("hashes", "all")); - } else { - makeRequest(log, "/command/pauseall"); - } - - return new DaemonTaskSuccessResult(task); - - case Resume: - - // Resume a torrent - if (version >= 40200) { - makeRequest(log, "/api/v2/torrents/resume", new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - } else { - makeRequest(log, "/command/resume", new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID())); - } - - return new DaemonTaskSuccessResult(task); - - case ResumeAll: - - // Resume all torrents - if (version >= 40200) { - path = "/api/v2/torrents/resume"; - makeRequest(log, path, new BasicNameValuePair("hashes", "all")); - } else { - makeRequest(log, "/command/resumeall"); - } - - return new DaemonTaskSuccessResult(task); - - case SetFilePriorities: - - // Update the priorities to a set of files - SetFilePriorityTask setPrio = (SetFilePriorityTask) task; - String newPrio = "0"; - if (setPrio.getNewPriority() == Priority.Low) { - newPrio = "1"; - } else if (setPrio.getNewPriority() == Priority.Normal) { - newPrio = "2"; - } else if (setPrio.getNewPriority() == Priority.High) { - newPrio = "7"; - } - // We have to make a separate request per file, it seems - for (TorrentFile file : setPrio.getForFiles()) { - if (version >= 40200) { - path = "/api/v2/torrents/filePrio"; - } else { - path = "/command/setFilePrio"; - } - makeRequest(log, path, new BasicNameValuePair("hash", task.getTargetTorrent().getUniqueID()), - new BasicNameValuePair("id", file.getKey()), new BasicNameValuePair("priority", newPrio)); - - } - return new DaemonTaskSuccessResult(task); - - case ForceRecheck: - - // Force recheck a torrent - if (version >= 40200) { - path = "/api/v2/torrents/recheck"; - } else { - path = "/command/recheck"; - } - makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); - - case ToggleSequentialDownload: - - // Toggle sequential download mode on a torrent - if (version >= 40200) { - path = "/api/v2/torrents/toggleSequentialDownload"; - } else { - path = "/command/toggleSequentialDownload"; - } - makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); - - case ToggleFirstLastPieceDownload: - - // Set policy for downloading first and last piece first on a torrent - if (version >= 40200) { - path = "/api/v2/torrents/toggleFirstLastPiecePrio"; - } else { - path = "/command/toggleFirstLastPiecePrio"; - } - makeRequest(log, path, new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID())); - return new DaemonTaskSuccessResult(task); - - case SetLabel: - - SetLabelTask labelTask = (SetLabelTask) task; - if (version >= 40200) { - path = "/api/v2/torrents/setCategory"; - } else { - path = "/command/setCategory"; - } - makeRequest(log, path, - new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), - new BasicNameValuePair("category", labelTask.getNewLabel())); - return new DaemonTaskSuccessResult(task); - - case SetDownloadLocation: - - SetDownloadLocationTask setLocationTask = (SetDownloadLocationTask) task; - if (version >= 40200) { - path = "/api/v2/torrents/setLocation"; - } else { - path = "/command/setLocation"; - } - makeRequest(log, path, - new BasicNameValuePair("hashes", task.getTargetTorrent().getUniqueID()), - new BasicNameValuePair("location", setLocationTask.getNewLocation())); - return new DaemonTaskSuccessResult(task); - - case SetTransferRates: - - // Request to set the maximum transfer rates - String pathDL; - String pathUL; - SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; - String dl = (ratesTask.getDownloadRate() == null ? "NaN" : Long.toString(ratesTask.getDownloadRate() * 1024)); - String ul = (ratesTask.getUploadRate() == null ? "NaN" : Long.toString(ratesTask.getUploadRate() * 1024)); - - if (version >= 40200) { - pathDL = "/api/v2/torrents/setDownloadLimit"; - pathUL = "/api/v2/torrents/setUploadLimit"; - } else { - pathDL = "/command/setGlobalDlLimit"; - pathUL = "/command/setGlobalUpLimit"; - } - - makeRequest(log, pathDL, new BasicNameValuePair("limit", dl)); - makeRequest(log, pathUL, new BasicNameValuePair("limit", ul)); - return new DaemonTaskSuccessResult(task); - - case GetStats: - - // Refresh alternative download speeds setting - if (version >= 40200) { - path = "/api/v2/sync/maindata?rid=0"; - } else { - path = "/sync/maindata?rid=0"; - } - JSONObject stats = new JSONObject(makeRequest(log, path)); - JSONObject serverStats = stats.optJSONObject("server_state"); - boolean alternativeSpeeds = false; - if (serverStats != null) { - alternativeSpeeds = serverStats.optBoolean("use_alt_speed_limits"); - } - return new GetStatsTaskSuccessResult((GetStatsTask) task, alternativeSpeeds, -1); - - case SetAlternativeMode: - - // Flip alternative speed mode - if (version >= 40200) { - path = "/api/v2/transfer/toggleSpeedLimitsMode"; - } else { - path = "/command/toggleAlternativeSpeedLimits"; - } - makeRequest(log, path); - 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 String makeRequest(Log log, String path, NameValuePair... params) throws DaemonException { - - try { - - // Setup request using POST - String url_to_request = buildWebUIUrl(path); - HttpPost httppost = new HttpPost(url_to_request); - - List nvps = new ArrayList<>(); - Collections.addAll(nvps, params); - httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); - return makeWebRequest(httppost, log); - - } catch (UnsupportedEncodingException e) { - throw new DaemonException(ExceptionType.ConnectionError, e.toString()); - } - - } - - private String makeUploadRequest(String path, String file, Log log) throws DaemonException { - - try { - - // Setup request using POST - HttpPost httppost = new HttpPost(buildWebUIUrl(path)); - File upload = new File(URI.create(file)); - Part[] parts = {new FilePart("torrentfile", upload)}; - httppost.setEntity(new MultipartEntity(parts, httppost.getParams())); - return makeWebRequest(httppost, log); - - } catch (FileNotFoundException e) { - throw new DaemonException(ExceptionType.FileAccessError, e.toString()); - } - - } - - private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException { - - try { - - // Initialise the HTTP client - if (httpclient == null) { - initialise(); - } - - // Execute - HttpResponse response = httpclient.execute(httppost); - - // Throw exception on 403 - if (response.getStatusLine().getStatusCode() == 403) { - throw new DaemonException(ExceptionType.AuthenticationFailure, "Response code 403"); - } + } - HttpEntity entity = response.getEntity(); - if (entity != null) { + private String makeUploadRequest(String path, String file, Log log) throws DaemonException { - // Read JSON response - java.io.InputStream instream = entity.getContent(); - String result = HttpHelper.convertStreamToString(instream); - instream.close(); + try { - // TLog.d(LOG_NAME, "Success: " + (result.length() > 300? result.substring(0, 300) + "... (" + - // result.length() + " chars)": result)); + // Setup request using POST + HttpPost httppost = new HttpPost(buildWebUIUrl(path)); + File upload = new File(URI.create(file)); + Part[] parts = {new FilePart("torrentfile", upload)}; + httppost.setEntity(new MultipartEntity(parts, httppost.getParams())); + return makeWebRequest(httppost, log); - // Return raw result - return result; - } + } catch (FileNotFoundException e) { + throw new DaemonException(ExceptionType.FileAccessError, e.toString()); + } - log.d(LOG_NAME, "Error: No entity in HTTP response"); - throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response."); + } - } catch (Exception e) { - log.d(LOG_NAME, "Error: " + e.toString()); + private String makeWebRequest(HttpPost httppost, Log log) throws DaemonException { - if (e instanceof DaemonException) { - throw (DaemonException) e; - } - else { - throw new DaemonException(ExceptionType.ConnectionError, e.toString()); - } - } - - } - - /** - * Instantiates an HTTP client with proper credentials that can be used for all qBittorrent requests. - * @throws DaemonException On conflicting or missing settings - */ - private void initialise() throws DaemonException { - httpclient = HttpHelper.createStandardHttpClient(settings, true); - } - - /** - * Build the URL of the web UI request from the user settings - * @return The URL to request - */ - private String buildWebUIUrl(String path) { - String proxyFolder = settings.getFolder(); - if (proxyFolder == null) - proxyFolder = ""; - else if (proxyFolder.endsWith("/")) - proxyFolder = proxyFolder.substring(0, proxyFolder.length() - 1); - return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + proxyFolder + path; - } - - private TorrentDetails parseJsonTorrentDetails(JSONArray messages, JSONArray pieceStates) throws JSONException { - - ArrayList trackers = new ArrayList<>(); - ArrayList errors = new ArrayList<>(); - - // Parse response - if (messages.length() > 0) { - for (int i = 0; i < messages.length(); i++) { - JSONObject tor = messages.getJSONObject(i); - trackers.add(tor.getString("url")); - String msg = tor.getString("msg"); - if (msg != null && !msg.equals("")) - errors.add(msg); - } - } - - ArrayList pieces = new ArrayList<>(); - if (pieceStates.length() > 0) { - for (int i = 0; i < pieceStates.length(); i++) { - pieces.add(pieceStates.getInt(i)); - } + try { + // Execute + HttpResponse response = httpclient.execute(httppost); + + // Throw exception on 403 + if (response.getStatusLine().getStatusCode() == 403) { + throw new DaemonException(ExceptionType.AuthenticationFailure, "Response code 403"); + } + + HttpEntity entity = response.getEntity(); + if (entity != null) { + + // Read JSON response + java.io.InputStream instream = entity.getContent(); + String result = HttpHelper.convertStreamToString(instream); + instream.close(); + + // TLog.d(LOG_NAME, "Success: " + (result.length() > 300? result.substring(0, 300) + "... (" + + // result.length() + " chars)": result)); + + // Return raw result + return result; + } + + log.d(LOG_NAME, "Error: No entity in HTTP response"); + throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response."); + + } catch (Exception e) { + log.d(LOG_NAME, "Error: " + e.toString()); + + if (e instanceof DaemonException) { + throw (DaemonException) e; + } else { + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + } + + } + + /** + * Instantiates an HTTP client with proper credentials that can be used for all qBittorrent requests. + * + * @throws DaemonException On conflicting or missing settings + */ + private void initialise() throws DaemonException { + if (httpclient == null) { + httpclient = HttpHelper.createStandardHttpClient(settings, true); + } + } + + /** + * Build the URL of the web UI request from the user settings + * + * @return The URL to request + */ + private String buildWebUIUrl(String path) { + String proxyFolder = settings.getFolder(); + if (proxyFolder == null) + proxyFolder = ""; + else if (proxyFolder.endsWith("/")) + proxyFolder = proxyFolder.substring(0, proxyFolder.length() - 1); + return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + proxyFolder + path; + } + + private TorrentDetails parseJsonTorrentDetails(JSONArray messages, JSONArray pieceStates) throws JSONException { + + ArrayList trackers = new ArrayList<>(); + ArrayList errors = new ArrayList<>(); + + // Parse response + if (messages.length() > 0) { + for (int i = 0; i < messages.length(); i++) { + JSONObject tor = messages.getJSONObject(i); + trackers.add(tor.getString("url")); + String msg = tor.getString("msg"); + if (msg != null && !msg.equals("")) + errors.add(msg); + } + } + + ArrayList pieces = new ArrayList<>(); + if (pieceStates.length() > 0) { + for (int i = 0; i < pieceStates.length(); i++) { + pieces.add(pieceStates.getInt(i)); + } + } + + // Return the list + return new TorrentDetails(trackers, errors, pieces); + + } + + private List