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 <bdashore3@gmail.com>
This commit is contained in:
kingbri 2022-07-21 11:15:22 -04:00
parent c2f267dbc3
commit 4a53eb052c
2 changed files with 60 additions and 113 deletions

View file

@ -14,12 +14,8 @@ public enum RealDebridError: Error {
case InvalidResponse case InvalidResponse
case InvalidToken case InvalidToken
case EmptyData case EmptyData
case FailedRequest(description: String)
case AuthQuery(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 { public class RealDebrid: ObservableObject {
@ -57,11 +53,11 @@ public class RealDebrid: ObservableObject {
do { do {
try await getDeviceCredentials(deviceCode: rawResponse.deviceCode) try await getDeviceCredentials(deviceCode: rawResponse.deviceCode)
} catch { } catch {
print("Authentication error: \(error)") print("Authentication error in \(#function): \(error)")
authTask?.cancel() authTask?.cancel()
Task { @MainActor in 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 { if case let .failure(error) = await authTask?.result {
print(error)
throw error throw error
} }
} }
@ -198,45 +193,45 @@ public class RealDebrid: ObservableObject {
} }
} }
// Checks if the magnet is streamable on RD // Wrapper request function which matches the responses and returns data
// Currently does not work for batch links @discardableResult public func performRequest(request: inout URLRequest, requestName: String) async throws -> Data {
public func instantAvailability(magnetHashes: [String]) async -> [String]? {
var availableHashes: [String] = []
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!)
guard let token = await fetchToken() else { guard let token = await fetchToken() else {
return nil throw RealDebridError.InvalidToken
} }
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
do { let (data, response) = try await URLSession.shared.data(for: request)
// Assume that RealDebrid can be called here
let (data, response) = try await URLSession.shared.data(for: request)
// Unauthorized, auto-logout of RD, wrap this into a request function guard let response = response as? HTTPURLResponse else {
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 401 { throw RealDebridError.FailedRequest(description: "No HTTP response given")
try await deleteTokens() }
}
// Does not account for torrent packs at the moment if response.statusCode >= 200, response.statusCode <= 299 {
if let rawResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] { return data
for (key, value) in rawResponse { } else if response.statusCode == 401 {
if value as? [String: Any] != nil { try await deleteTokens()
availableHashes.append(key) 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 return availableHashes
@ -245,14 +240,7 @@ public class RealDebrid: ObservableObject {
// Adds a magnet link to the user's RD account // Adds a magnet link to the user's RD account
public func addMagnet(magnetLink: String) async throws -> String { public func addMagnet(magnetLink: String) async throws -> String {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/addMagnet")!)
guard let token = await fetchToken() else {
throw RealDebridError.InvalidToken
}
request.httpMethod = "POST" request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents() var bodyComponents = URLComponents()
@ -260,28 +248,16 @@ public class RealDebrid: ObservableObject {
request.httpBody = bodyComponents.query?.data(using: .utf8) request.httpBody = bodyComponents.query?.data(using: .utf8)
do { let data = try await performRequest(request: &request, requestName: #function)
let (data, _) = try await URLSession.shared.data(for: request) let rawResponse = try jsonDecoder.decode(AddMagnetResponse.self, from: data)
let rawResponse = try jsonDecoder.decode(AddMagnetResponse.self, from: data)
return rawResponse.id return rawResponse.id
} catch {
print("Magnet link query error! \(error)")
throw RealDebridError.AddMagnetQuery(description: error.localizedDescription)
}
} }
// Queues the magnet link for downloading // 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)")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/selectFiles/\(debridID)")!)
guard let token = await fetchToken() else {
throw RealDebridError.InvalidToken
}
request.httpMethod = "POST" request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents() var bodyComponents = URLComponents()
@ -289,52 +265,27 @@ public class RealDebrid: ObservableObject {
request.httpBody = bodyComponents.query?.data(using: .utf8) request.httpBody = bodyComponents.query?.data(using: .utf8)
do { try await performRequest(request: &request, requestName: #function)
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)
}
} }
// Fetches the info of a torrent // Fetches the info of a torrent
public func torrentInfo(debridID: String) async throws -> String { public func torrentInfo(debridID: String) async throws -> String {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
guard let token = await fetchToken() else { let data = try await performRequest(request: &request, requestName: #function)
throw RealDebridError.InvalidToken let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
}
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") if let torrentLink = rawResponse.links[safe: 0] {
return torrentLink
do { } else {
let (data, _) = try await URLSession.shared.data(for: request) throw RealDebridError.EmptyData
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)
} }
} }
// Downloads link from selectFiles for playback // Downloads link from selectFiles for playback
public func unrestrictLink(debridDownloadLink: String) async throws -> String { public func unrestrictLink(debridDownloadLink: String) async throws -> String {
var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!) var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!)
guard let token = await fetchToken() else {
throw RealDebridError.InvalidToken
}
request.httpMethod = "POST" request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
var bodyComponents = URLComponents() var bodyComponents = URLComponents()
@ -342,14 +293,9 @@ public class RealDebrid: ObservableObject {
request.httpBody = bodyComponents.query?.data(using: .utf8) request.httpBody = bodyComponents.query?.data(using: .utf8)
do { let data = try await performRequest(request: &request, requestName: #function)
let (data, _) = try await URLSession.shared.data(for: request) let rawResponse = try jsonDecoder.decode(UnrestrictLinkResponse.self, from: data)
let rawResponse = try jsonDecoder.decode(UnrestrictLinkResponse.self, from: data)
return rawResponse.download return rawResponse.download
} catch {
print("Unrestrict link error: \(error)")
throw RealDebridError.UnrestrictLinkQuery(description: error.localizedDescription)
}
} }
} }

View file

@ -35,12 +35,18 @@ public class DebridManager: ObservableObject {
} }
} }
guard let debridHashes = await realDebrid.instantAvailability(magnetHashes: hashes) else { do {
return let debridHashes = try await realDebrid.instantAvailability(magnetHashes: hashes)
}
Task { @MainActor in Task { @MainActor in
realDebridHashes = debridHashes 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 { public func fetchRdDownload(searchResult: SearchResult) async {
do { do {
let realDebridId = try await realDebrid.addMagnet(magnetLink: searchResult.magnetLink) let realDebridId = try await realDebrid.addMagnet(magnetLink: searchResult.magnetLink)
let httpResponse = try await realDebrid.selectFiles(debridID: realDebridId) try await realDebrid.selectFiles(debridID: realDebridId)
if httpResponse?.statusCode != 204 {
// Throw error here
return
}
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId) let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId)
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)