From 4a53eb052c7ad5bb76b35ab34c6348802b8f3b5b Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 21 Jul 2022 11:15:22 -0400 Subject: [PATCH] RealDebrid: Add universal request wrapper Every request has to add an authentication header. If the response is 401 (Unauthorized), log the user out of RealDebrid. Signed-off-by: kingbri --- Ferrite/API/RealDebridWrapper.swift | 150 +++++++++------------------- Ferrite/Models/DebridManager.swift | 23 +++-- 2 files changed, 60 insertions(+), 113 deletions(-) diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index fd547b2..5c6c0d1 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -14,12 +14,8 @@ public enum RealDebridError: Error { case InvalidResponse case InvalidToken case EmptyData + case FailedRequest(description: String) case AuthQuery(description: String) - case InstantAvailabilityQuery(description: String) - case AddMagnetQuery(description: String) - case SelectFilesQuery(description: String) - case TorrentInfoQuery(description: String) - case UnrestrictLinkQuery(description: String) } public class RealDebrid: ObservableObject { @@ -57,11 +53,11 @@ public class RealDebrid: ObservableObject { do { try await getDeviceCredentials(deviceCode: rawResponse.deviceCode) } catch { - print("Authentication error: \(error)") + print("Authentication error in \(#function): \(error)") authTask?.cancel() Task { @MainActor in - parentManager?.toastModel?.toastDescription = "Authentication error: \(error)" + parentManager?.toastModel?.toastDescription = "Authentication error in \(#function): \(error)" } } } @@ -113,9 +109,8 @@ public class RealDebrid: ObservableObject { } } } - + if case let .failure(error) = await authTask?.result { - print(error) throw error } } @@ -198,45 +193,45 @@ public class RealDebrid: ObservableObject { } } - // Checks if the magnet is streamable on RD - // Currently does not work for batch links - public func instantAvailability(magnetHashes: [String]) async -> [String]? { - var availableHashes: [String] = [] - - var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!) - + // Wrapper request function which matches the responses and returns data + @discardableResult public func performRequest(request: inout URLRequest, requestName: String) async throws -> Data { guard let token = await fetchToken() else { - return nil + throw RealDebridError.InvalidToken } request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - do { - // Assume that RealDebrid can be called here - let (data, response) = try await URLSession.shared.data(for: request) + let (data, response) = try await URLSession.shared.data(for: request) - // Unauthorized, auto-logout of RD, wrap this into a request function - if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 { - try await deleteTokens() - } + guard let response = response as? HTTPURLResponse else { + throw RealDebridError.FailedRequest(description: "No HTTP response given") + } - // Does not account for torrent packs at the moment - if let rawResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] { - for (key, value) in rawResponse { - if value as? [String: Any] != nil { - availableHashes.append(key) - } + if response.statusCode >= 200, response.statusCode <= 299 { + return data + } else if response.statusCode == 401 { + try await deleteTokens() + throw RealDebridError.FailedRequest(description: "The request \(requestName) failed because you were unauthorized. Please relogin to RealDebrid in Settings.") + } else { + throw RealDebridError.FailedRequest(description: "The request \(requestName) failed with status code \(response.statusCode).") + } + } + + // Checks if the magnet is streamable on RD + // Currently does not work for batch links + public func instantAvailability(magnetHashes: [String]) async throws -> [String] { + var availableHashes: [String] = [] + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!) + + let data = try await performRequest(request: &request, requestName: #function) + + // Does not account for torrent packs at the moment + if let rawResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + for (key, value) in rawResponse { + if value as? [String: Any] != nil { + availableHashes.append(key) } } - } catch { - // Assume that RealDebrid cannot be used here - print("RealDebrid request error: \(error)") - - Task { @MainActor in - parentManager?.toastModel?.toastDescription = "RealDebrid InstantAvailability error: \(error)" - } - - return nil } return availableHashes @@ -245,14 +240,7 @@ public class RealDebrid: ObservableObject { // Adds a magnet link to the user's RD account public func addMagnet(magnetLink: String) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!) - - guard let token = await fetchToken() else { - throw RealDebridError.InvalidToken - } - request.httpMethod = "POST" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") var bodyComponents = URLComponents() @@ -260,28 +248,16 @@ public class RealDebrid: ObservableObject { request.httpBody = bodyComponents.query?.data(using: .utf8) - do { - let (data, _) = try await URLSession.shared.data(for: request) - let rawResponse = try jsonDecoder.decode(AddMagnetResponse.self, from: data) + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(AddMagnetResponse.self, from: data) - return rawResponse.id - } catch { - print("Magnet link query error! \(error)") - throw RealDebridError.AddMagnetQuery(description: error.localizedDescription) - } + return rawResponse.id } // Queues the magnet link for downloading - public func selectFiles(debridID: String) async throws -> HTTPURLResponse? { + public func selectFiles(debridID: String) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/selectFiles/\(debridID)")!) - - guard let token = await fetchToken() else { - throw RealDebridError.InvalidToken - } - request.httpMethod = "POST" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") var bodyComponents = URLComponents() @@ -289,52 +265,27 @@ public class RealDebrid: ObservableObject { request.httpBody = bodyComponents.query?.data(using: .utf8) - do { - let (_, response) = try await URLSession.shared.data(for: request) - - return response as? HTTPURLResponse - } catch { - print("Magnet file query error! \(error)") - throw RealDebridError.SelectFilesQuery(description: error.localizedDescription) - } + try await performRequest(request: &request, requestName: #function) } // Fetches the info of a torrent public func torrentInfo(debridID: String) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!) - guard let token = await fetchToken() else { - throw RealDebridError.InvalidToken - } + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - - do { - let (data, _) = try await URLSession.shared.data(for: request) - let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) - - if let torrentLink = rawResponse.links[safe: 0] { - return torrentLink - } else { - throw RealDebridError.EmptyData - } - } catch { - print("Torrent info query error: \(error)") - throw RealDebridError.TorrentInfoQuery(description: error.localizedDescription) + if let torrentLink = rawResponse.links[safe: 0] { + return torrentLink + } else { + throw RealDebridError.EmptyData } } // Downloads link from selectFiles for playback public func unrestrictLink(debridDownloadLink: String) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!) - - guard let token = await fetchToken() else { - throw RealDebridError.InvalidToken - } - request.httpMethod = "POST" - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") var bodyComponents = URLComponents() @@ -342,14 +293,9 @@ public class RealDebrid: ObservableObject { request.httpBody = bodyComponents.query?.data(using: .utf8) - do { - let (data, _) = try await URLSession.shared.data(for: request) - let rawResponse = try jsonDecoder.decode(UnrestrictLinkResponse.self, from: data) + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode(UnrestrictLinkResponse.self, from: data) - return rawResponse.download - } catch { - print("Unrestrict link error: \(error)") - throw RealDebridError.UnrestrictLinkQuery(description: error.localizedDescription) - } + return rawResponse.download } } diff --git a/Ferrite/Models/DebridManager.swift b/Ferrite/Models/DebridManager.swift index 00f2296..345e11f 100644 --- a/Ferrite/Models/DebridManager.swift +++ b/Ferrite/Models/DebridManager.swift @@ -35,12 +35,18 @@ public class DebridManager: ObservableObject { } } - guard let debridHashes = await realDebrid.instantAvailability(magnetHashes: hashes) else { - return - } + do { + let debridHashes = try await realDebrid.instantAvailability(magnetHashes: hashes) - Task { @MainActor in - realDebridHashes = debridHashes + Task { @MainActor in + realDebridHashes = debridHashes + } + } catch { + Task { @MainActor in + toastModel?.toastDescription = "RealDebrid hash error: \(error)" + } + + print(error) } } @@ -64,12 +70,7 @@ public class DebridManager: ObservableObject { public func fetchRdDownload(searchResult: SearchResult) async { do { let realDebridId = try await realDebrid.addMagnet(magnetLink: searchResult.magnetLink) - let httpResponse = try await realDebrid.selectFiles(debridID: realDebridId) - - if httpResponse?.statusCode != 204 { - // Throw error here - return - } + try await realDebrid.selectFiles(debridID: realDebridId) let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId) let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)