From 06d4f8e84e7aa179cb9cac942b87be7014ab5477 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 25 Nov 2022 14:41:54 -0500 Subject: [PATCH] RealDebrid, Github: Reorganize models Prep for more debrid services Signed-off-by: kingbri --- Ferrite/API/AllDebridWrapper.swift | 8 + Ferrite/API/GithubWrapper.swift | 8 +- Ferrite/API/RealDebridWrapper.swift | 55 ++- Ferrite/Models/GithubModels.swift | 14 +- Ferrite/Models/RealDebridModels.swift | 350 +++++++++--------- Ferrite/ViewModels/DebridManager.swift | 10 +- .../SettingsAppVersionView.swift | 2 +- 7 files changed, 230 insertions(+), 217 deletions(-) create mode 100644 Ferrite/API/AllDebridWrapper.swift diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift new file mode 100644 index 0000000..9d9459c --- /dev/null +++ b/Ferrite/API/AllDebridWrapper.swift @@ -0,0 +1,8 @@ +// +// AllDebridWrapper.swift +// Ferrite +// +// Created by Brian Dashore on 11/25/22. +// + +import Foundation diff --git a/Ferrite/API/GithubWrapper.swift b/Ferrite/API/GithubWrapper.swift index 294164e..5d77b7d 100644 --- a/Ferrite/API/GithubWrapper.swift +++ b/Ferrite/API/GithubWrapper.swift @@ -8,21 +8,21 @@ import Foundation public class Github { - public func fetchLatestRelease() async throws -> GithubRelease? { + public func fetchLatestRelease() async throws -> Release? { let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases/latest")! let (data, _) = try await URLSession.shared.data(from: url) - let rawResponse = try JSONDecoder().decode(GithubRelease.self, from: data) + let rawResponse = try JSONDecoder().decode(Release.self, from: data) return rawResponse } - public func fetchReleases() async throws -> [GithubRelease]? { + public func fetchReleases() async throws -> [Release]? { let url = URL(string: "https://api.github.com/repos/bdashore3/Ferrite/releases")! let (data, _) = try await URLSession.shared.data(from: url) - let rawResponse = try JSONDecoder().decode([GithubRelease].self, from: data) + let rawResponse = try JSONDecoder().decode([Release].self, from: data) return rawResponse } } diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index ecefd2a..92bd60a 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -8,17 +8,6 @@ import Foundation import KeychainSwift -public enum RealDebridError: Error { - case InvalidUrl - case InvalidPostBody - case InvalidResponse - case InvalidToken - case EmptyData - case EmptyTorrents - case FailedRequest(description: String) - case AuthQuery(description: String) -} - public class RealDebrid { let jsonDecoder = JSONDecoder() let keychain = KeychainSwift() @@ -38,7 +27,7 @@ public class RealDebrid { ] guard let url = urlComponents.url else { - throw RealDebridError.InvalidUrl + throw RDError.InvalidUrl } let request = URLRequest(url: url) @@ -49,7 +38,7 @@ public class RealDebrid { return rawResponse } catch { print("Couldn't get the new client creds!") - throw RealDebridError.AuthQuery(description: error.localizedDescription) + throw RDError.AuthQuery(description: error.localizedDescription) } } @@ -62,7 +51,7 @@ public class RealDebrid { ] guard let url = urlComponents.url else { - throw RealDebridError.InvalidUrl + throw RDError.InvalidUrl } let request = URLRequest(url: url) @@ -73,7 +62,7 @@ public class RealDebrid { while count < 20 { if Task.isCancelled { - throw RealDebridError.AuthQuery(description: "Token request cancelled.") + throw RDError.AuthQuery(description: "Token request cancelled.") } let (data, _) = try await URLSession.shared.data(for: request) @@ -95,7 +84,7 @@ public class RealDebrid { } } - throw RealDebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") + throw RDError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.") } if case let .failure(error) = await authTask?.result { @@ -106,11 +95,11 @@ public class RealDebrid { // Fetch all tokens for the user and store in keychain public func getTokens(deviceCode: String) async throws { guard let clientId = UserDefaults.standard.string(forKey: "RealDebrid.ClientId") else { - throw RealDebridError.EmptyData + throw RDError.EmptyData } guard let clientSecret = keychain.get("RealDebrid.ClientSecret") else { - throw RealDebridError.EmptyData + throw RDError.EmptyData } var request = URLRequest(url: URL(string: "\(baseAuthUrl)/token")!) @@ -174,7 +163,7 @@ public class RealDebrid { // 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 { - throw RealDebridError.InvalidToken + throw RDError.InvalidToken } request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") @@ -182,23 +171,23 @@ public class RealDebrid { let (data, response) = try await URLSession.shared.data(for: request) guard let response = response as? HTTPURLResponse else { - throw RealDebridError.FailedRequest(description: "No HTTP response given") + throw RDError.FailedRequest(description: "No HTTP response given") } 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.") + throw RDError.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).") + throw RDError.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 -> [RealDebridIA] { - var availableHashes: [RealDebridIA] = [] + public func instantAvailability(magnetHashes: [String]) async throws -> [RealDebrid.IA] { + var availableHashes: [RealDebrid.IA] = [] var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/instantAvailability/\(magnetHashes.joined(separator: "/"))")!) let data = try await performRequest(request: &request, requestName: #function) @@ -219,17 +208,17 @@ public class RealDebrid { if data.rd.count > 1 || data.rd[0].count > 1 { // Batch array let batches = data.rd.map { fileDict in - let batchFiles: [RealDebridIABatchFile] = fileDict.map { key, value in + let batchFiles: [RealDebrid.IABatchFile] = fileDict.map { key, value in // Force unwrapped ID. Is safe because ID is guaranteed on a successful response - RealDebridIABatchFile(id: Int(key)!, fileName: value.filename) + RealDebrid.IABatchFile(id: Int(key)!, fileName: value.filename) }.sorted(by: { $0.id < $1.id }) - return RealDebridIABatch(files: batchFiles) + return RealDebrid.IABatch(files: batchFiles) } // RD files array // Possibly sort this in the future, but not sure how at the moment - var files: [RealDebridIAFile] = [] + var files: [RealDebrid.IAFile] = [] for index in batches.indices { let batchFiles = batches[index].files @@ -239,7 +228,7 @@ public class RealDebrid { if !files.contains(where: { $0.name == batchFile.fileName }) { files.append( - RealDebridIAFile( + RealDebrid.IAFile( name: batchFile.fileName, batchIndex: index, batchFileIndex: batchFileIndex @@ -251,7 +240,7 @@ public class RealDebrid { // TTL: 5 minutes availableHashes.append( - RealDebridIA( + RealDebrid.IA( hash: hash, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files, @@ -260,7 +249,7 @@ public class RealDebrid { ) } else { availableHashes.append( - RealDebridIA( + RealDebrid.IA( hash: hash, expiryTimeStamp: Date().timeIntervalSince1970 + 300 ) @@ -319,9 +308,9 @@ public class RealDebrid { if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" { return torrentLink } else if rawResponse.status == "downloading" || rawResponse.status == "queued" { - throw RealDebridError.EmptyTorrents + throw RDError.EmptyTorrents } else { - throw RealDebridError.EmptyData + throw RDError.EmptyData } } diff --git a/Ferrite/Models/GithubModels.swift b/Ferrite/Models/GithubModels.swift index fb4a066..242d5e0 100644 --- a/Ferrite/Models/GithubModels.swift +++ b/Ferrite/Models/GithubModels.swift @@ -7,12 +7,14 @@ import Foundation -public struct GithubRelease: Codable, Hashable, Sendable { - let htmlUrl: String - let tagName: String +extension Github { + public struct Release: Codable, Hashable, Sendable { + let htmlUrl: String + let tagName: String - enum CodingKeys: String, CodingKey { - case htmlUrl = "html_url" - case tagName = "tag_name" + enum CodingKeys: String, CodingKey { + case htmlUrl = "html_url" + case tagName = "tag_name" + } } } diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index b58de72..114a202 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -8,187 +8,201 @@ import Foundation -// MARK: - device code endpoint - -public struct DeviceCodeResponse: Codable, Sendable { - let deviceCode, userCode: String - let interval, expiresIn: Int - let verificationURL, directVerificationURL: String - - enum CodingKeys: String, CodingKey { - case deviceCode = "device_code" - case userCode = "user_code" - case interval - case expiresIn = "expires_in" - case verificationURL = "verification_url" - case directVerificationURL = "direct_verification_url" +extension RealDebrid { + // MARK: - Errors + public enum RDError: Error { + case InvalidUrl + case InvalidPostBody + case InvalidResponse + case InvalidToken + case EmptyData + case EmptyTorrents + case FailedRequest(description: String) + case AuthQuery(description: String) } -} -// MARK: - device credentials endpoint + // MARK: - device code endpoint -public struct DeviceCredentialsResponse: Codable, Sendable { - let clientID, clientSecret: String? + public struct DeviceCodeResponse: Codable, Sendable { + let deviceCode, userCode: String + let interval, expiresIn: Int + let verificationURL, directVerificationURL: String - enum CodingKeys: String, CodingKey { - case clientID = "client_id" - case clientSecret = "client_secret" + enum CodingKeys: String, CodingKey { + case deviceCode = "device_code" + case userCode = "user_code" + case interval + case expiresIn = "expires_in" + case verificationURL = "verification_url" + case directVerificationURL = "direct_verification_url" + } } -} -// MARK: - token endpoint + // MARK: - device credentials endpoint -public struct TokenResponse: Codable, Sendable { - let accessToken: String - let expiresIn: Int - let refreshToken, tokenType: String + public struct DeviceCredentialsResponse: Codable, Sendable { + let clientID, clientSecret: String? - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } } -} -// MARK: - instantAvailability endpoint + // MARK: - token endpoint -// Thanks Skitty! -public struct InstantAvailabilityResponse: Codable, Sendable { - var data: InstantAvailabilityData? + public struct TokenResponse: Codable, Sendable { + let accessToken: String + let expiresIn: Int + let refreshToken, tokenType: String - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case tokenType = "token_type" + } + } - if let data = try? container.decode(InstantAvailabilityData.self) { - self.data = data + // MARK: - instantAvailability endpoint + + // Thanks Skitty! + public struct InstantAvailabilityResponse: Codable, Sendable { + var data: InstantAvailabilityData? + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let data = try? container.decode(InstantAvailabilityData.self) { + self.data = data + } + } + } + + struct InstantAvailabilityData: Codable, Sendable { + var rd: [[String: InstantAvailabilityInfo]] + } + + struct InstantAvailabilityInfo: Codable, Sendable { + var filename: String + var filesize: Int + } + + // MARK: - Instant Availability client side structures + + public struct IA: Codable, Hashable, Sendable { + let hash: String + let expiryTimeStamp: Double + var files: [IAFile] = [] + var batches: [IABatch] = [] + } + + public struct IABatch: Codable, Hashable, Sendable { + let files: [IABatchFile] + } + + public struct IABatchFile: Codable, Hashable, Sendable { + let id: Int + let fileName: String + } + + public struct IAFile: Codable, Hashable, Sendable { + let name: String + let batchIndex: Int + let batchFileIndex: Int + } + + public enum IAStatus: Codable, Hashable, Sendable { + case full + case partial + case none + } + + // MARK: - addMagnet endpoint + + public struct AddMagnetResponse: Codable, Sendable { + let id: String + let uri: String + } + + // MARK: - torrentInfo endpoint + + struct TorrentInfoResponse: Codable, Sendable { + let id, filename, originalFilename, hash: String + let bytes, originalBytes: Int + let host: String + let split, progress: Int + let status, added: String + let files: [TorrentInfoFile] + let links: [String] + let ended: String? + let speed: Int? + let seeders: Int? + + enum CodingKeys: String, CodingKey { + case id, filename + case originalFilename = "original_filename" + case hash, bytes + case originalBytes = "original_bytes" + case host, split, progress, status, added, files, links, ended, speed, seeders + } + } + + struct TorrentInfoFile: Codable, Sendable { + let id: Int + let path: String + let bytes, selected: Int + } + + public struct UserTorrentsResponse: Codable, Sendable { + let id, filename, hash: String + let bytes: Int + let host: String + let split, progress: Int + let status, added: String + let links: [String] + let speed, seeders: Int? + let ended: String? + } + + // MARK: - unrestrictLink endpoint + + struct UnrestrictLinkResponse: Codable, Sendable { + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks, crc: Int + let download: String + let streamable: Int + + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, crc, download, streamable + } + } + + // MARK: - User downloads list + + public struct UserDownloadsResponse: Codable, Sendable { + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks: Int + let download: String + let streamable: Int + let generated: String + + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, download, streamable, generated } } } - -// MARK: - Instant Availability client side structures - -struct InstantAvailabilityData: Codable, Sendable { - var rd: [[String: InstantAvailabilityInfo]] -} - -struct InstantAvailabilityInfo: Codable, Sendable { - var filename: String - var filesize: Int -} - -public struct RealDebridIA: Codable, Hashable, Sendable { - let hash: String - let expiryTimeStamp: Double - var files: [RealDebridIAFile] = [] - var batches: [RealDebridIABatch] = [] -} - -public struct RealDebridIABatch: Codable, Hashable, Sendable { - let files: [RealDebridIABatchFile] -} - -public struct RealDebridIABatchFile: Codable, Hashable, Sendable { - let id: Int - let fileName: String -} - -public struct RealDebridIAFile: Codable, Hashable, Sendable { - let name: String - let batchIndex: Int - let batchFileIndex: Int -} - -public enum RealDebridIAStatus: Codable, Hashable, Sendable { - case full - case partial - case none -} - -// MARK: - addMagnet endpoint - -public struct AddMagnetResponse: Codable, Sendable { - let id: String - let uri: String -} - -// MARK: - torrentInfo endpoint - -struct TorrentInfoResponse: Codable, Sendable { - let id, filename, originalFilename, hash: String - let bytes, originalBytes: Int - let host: String - let split, progress: Int - let status, added: String - let files: [TorrentInfoFile] - let links: [String] - let ended: String? - let speed: Int? - let seeders: Int? - - enum CodingKeys: String, CodingKey { - case id, filename - case originalFilename = "original_filename" - case hash, bytes - case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended, speed, seeders - } -} - -struct TorrentInfoFile: Codable, Sendable { - let id: Int - let path: String - let bytes, selected: Int -} - -public struct UserTorrentsResponse: Codable, Sendable { - let id, filename, hash: String - let bytes: Int - let host: String - let split, progress: Int - let status, added: String - let links: [String] - let speed, seeders: Int? - let ended: String? -} - -// MARK: - unrestrictLink endpoint - -struct UnrestrictLinkResponse: Codable, Sendable { - let id, filename: String - let mimeType: String? - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks, crc: Int - let download: String - let streamable: Int - - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, crc, download, streamable - } -} - -// MARK: - User downloads list - -public struct UserDownloadsResponse: Codable, Sendable { - let id, filename: String - let mimeType: String? - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks: Int - let download: String - let streamable: Int - let generated: String - - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, download, streamable, generated - } -} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 5758b0d..175694a 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -32,14 +32,14 @@ public class DebridManager: ObservableObject { var realDebridAuthUrl: String = "" // RealDebrid fetch variables - @Published var realDebridIAValues: [RealDebridIA] = [] + @Published var realDebridIAValues: [RealDebrid.IA] = [] var realDebridDownloadUrl: String = "" @Published var showDeleteAlert: Bool = false // TODO: Switch to an individual item based sheet system to remove these variables - var selectedRealDebridItem: RealDebridIA? - var selectedRealDebridFile: RealDebridIAFile? + var selectedRealDebridItem: RealDebrid.IA? + var selectedRealDebridFile: RealDebrid.IAFile? var selectedRealDebridID: String? init() { @@ -81,7 +81,7 @@ public class DebridManager: ObservableObject { } } - public func matchSearchResult(result: SearchResult?) -> RealDebridIAStatus { + public func matchSearchResult(result: SearchResult?) -> RealDebrid.IAStatus { guard let result else { return .none } @@ -202,7 +202,7 @@ public class DebridManager: ObservableObject { } } catch { switch error { - case RealDebridError.EmptyTorrents: + case RealDebrid.RDError.EmptyTorrents: showDeleteAlert.toggle() default: let error = error as NSError diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index f15f15d..d340ccb 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -11,7 +11,7 @@ struct SettingsAppVersionView: View { @EnvironmentObject var toastModel: ToastViewModel @State private var viewTask: Task? - @State private var releases: [GithubRelease] = [] + @State private var releases: [Github.Release] = [] @State private var loadedReleases = false