RealDebrid, Github: Reorganize models

Prep for more debrid services

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-11-25 14:41:54 -05:00
parent a1cd62d3b9
commit 06d4f8e84e
7 changed files with 230 additions and 217 deletions

View file

@ -0,0 +1,8 @@
//
// AllDebridWrapper.swift
// Ferrite
//
// Created by Brian Dashore on 11/25/22.
//
import Foundation

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -11,7 +11,7 @@ struct SettingsAppVersionView: View {
@EnvironmentObject var toastModel: ToastViewModel
@State private var viewTask: Task<Void, Never>?
@State private var releases: [GithubRelease] = []
@State private var releases: [Github.Release] = []
@State private var loadedReleases = false