From 548e866d2020227a7704381644fb7c310d1cb3f8 Mon Sep 17 00:00:00 2001 From: Firdaus Ahmad Date: Sat, 14 Dec 2019 01:32:09 +0800 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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);