Skip to content

2.0.0 - Swift concurrency & authentication update #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ca0de79
Initial Swift 6 support, and improved response handling with async API
MaxHasADHD Feb 9, 2025
9034b05
Use typealias for all models to conform to Codable, Hashable, and Sen…
MaxHasADHD Feb 19, 2025
534eaf6
Replace URLSession mocking with URLProtocol
MaxHasADHD Feb 19, 2025
b8234f3
Update async endpoints
MaxHasADHD Feb 19, 2025
885d414
Update README
MaxHasADHD Feb 19, 2025
2375bfc
Implement EmptyRoute
MaxHasADHD Feb 20, 2025
d6136f4
Implement saved filters VIP request
MaxHasADHD Feb 20, 2025
d28f16a
Update example app to build with Xcode 16
MaxHasADHD Feb 20, 2025
cdb4b1f
Add endpoints for Trakt interacted series
MaxHasADHD Feb 20, 2025
5c79c15
Update TraktComment model
MaxHasADHD Feb 20, 2025
d37329f
Pass TraktManager to Route instead of using injection context, add mo…
MaxHasADHD Feb 22, 2025
919e1ad
Add all Movie endpoints
MaxHasADHD Feb 22, 2025
7b4262a
Add remaining show endpoints
MaxHasADHD Feb 22, 2025
ef5b0d3
Add remaining season endpoints
MaxHasADHD Feb 22, 2025
42a991a
Add remaining episode resources
MaxHasADHD Feb 22, 2025
2b6413a
Add additional User and Sync endpoints, update models and tests
MaxHasADHD Mar 1, 2025
f5b2a6d
Fix decoding account settings due to optional values
MaxHasADHD Mar 1, 2025
579b1e4
Mark hidden item sections as public
MaxHasADHD Mar 1, 2025
c741204
Add more sync endpoints, update models and tests
MaxHasADHD Mar 2, 2025
beaa25e
Add functions for paginated requests to fetch all pages, or stream ea…
MaxHasADHD Mar 5, 2025
54d7ec3
Moved completion handler endpoints into a folder
MaxHasADHD Mar 6, 2025
89b3b94
Refactored authentication into its own class for better testability a…
MaxHasADHD Mar 8, 2025
5814b26
Code cleanup
MaxHasADHD Mar 8, 2025
8859d6b
Replace ObjectsCompletionHandler with ObjectCompletionHandler<[T]>
MaxHasADHD Mar 8, 2025
803676c
Remove custom request handling for `/users/*/watching`
MaxHasADHD Mar 8, 2025
d1786c8
Remove custom request handling for `/checkin`
MaxHasADHD Mar 8, 2025
d990b9f
Add additional User routes
MaxHasADHD Mar 8, 2025
4e172a4
Add checkin resource
MaxHasADHD Mar 8, 2025
60a3b9c
Add additional User endpoints related to lists
MaxHasADHD Mar 9, 2025
318f13b
Replace generic use of CustomStringConvertible
MaxHasADHD Mar 9, 2025
ba41782
Make pagination functions public
MaxHasADHD Mar 9, 2025
4fec202
Fix missing body for creating custom lists
MaxHasADHD Mar 23, 2025
4a1195a
Add list endpoints
MaxHasADHD Mar 23, 2025
47ba8ca
Update sync id for removing ratings
MaxHasADHD Mar 29, 2025
13daa1d
Add series dropped date to last activities
MaxHasADHD Mar 31, 2025
7b8d9c1
Use URLProtocolCachePolicy
MaxHasADHD Apr 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .swiftpm/xcode/xcshareddata/xcschemes/TraktKit.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand All @@ -26,7 +26,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
systemAttachmentLifetime = "keepNever"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TraktKit"
BuildableName = "TraktKit"
BlueprintName = "TraktKit"
ReferencedContainer = "container:">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
Expand Down
30 changes: 11 additions & 19 deletions Common/DateParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,24 @@ internal extension Date {

enum DateParserError: Error {
case failedToParseDateFromString(String)
case typeUnhandled(Any?)
}

// MARK: - Class

static func dateFromString(_ string: Any?) throws -> Date {
if let dateString = string as? String {

let count = dateString.count
if count <= 10 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd"
} else if count == 23 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"
} else {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
}
static func dateFromString(_ dateString: String) throws(DateParserError) -> Date {
let count = dateString.count
if count <= 10 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd"
} else if count == 23 {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"
} else {
ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
}

if let date = ISO8601DateFormatter.date(from: dateString) {
return date
} else {
throw DateParserError.failedToParseDateFromString("String to parse: \(dateString), date format: \(String(describing: ISO8601DateFormatter.dateFormat))")
}
} else if let date = string as? Date {
if let date = ISO8601DateFormatter.date(from: dateString) {
return date
} else {
throw DateParserError.typeUnhandled(string)
throw .failedToParseDateFromString("String to parse: \(dateString), date format: \(String(describing: ISO8601DateFormatter.dateFormat))")
}
}

Expand Down
39 changes: 39 additions & 0 deletions Common/Extensions/URL+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// URL+Extensions.swift
// TraktKit
//
// Created by Maximilian Litteral on 2/22/25.
//

import Foundation

extension URL {
func queryDict() -> [String: Any] {
var info: [String: Any] = [String: Any]()
if let queryString = self.query{
for parameter in queryString.components(separatedBy: "&"){
let parts = parameter.components(separatedBy: "=")
if parts.count > 1 {
let key = parts[0].removingPercentEncoding
let value = parts[1].removingPercentEncoding
if key != nil && value != nil{
info[key!] = value
}
}
}
}
return info
}

/// Compares components, which doesn't require query parameters to be in any particular order
public func compareComponents(_ url: URL) -> Bool {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false),
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }

return components.scheme == urlComponents.scheme &&
components.host == urlComponents.host &&
components.path == urlComponents.path &&
components.queryItems?.enumerated().compactMap { $0.element.name }.sorted() == urlComponents.queryItems?.enumerated().compactMap { $0.element.name }.sorted() &&
components.queryItems?.enumerated().compactMap { $0.element.value }.sorted() == urlComponents.queryItems?.enumerated().compactMap { $0.element.value }.sorted()
}
}
3 changes: 2 additions & 1 deletion Common/MLKeychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ let kSecAttrAccessibleValue = kSecAttrAccessible as String
let kSecAttrAccessibleAfterFirstUnlockValue = kSecAttrAccessibleAfterFirstUnlock as String

public class MLKeychain {


@discardableResult
class func setString(value: String, forKey key: String) -> Bool {
let data = value.data(using: String.Encoding.utf8, allowLossyConversion: false)!

Expand Down
2 changes: 1 addition & 1 deletion Common/Models/Authentication/AuthenticationInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public struct AuthenticationInfo: Decodable, Hashable {
public struct AuthenticationInfo: TraktObject {
public let accessToken: String
public let tokenType: String
public let expiresIn: TimeInterval
Expand Down
52 changes: 26 additions & 26 deletions Common/Models/Authentication/DeviceCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,13 @@
// Copyright © 2020 Maximilian Litteral. All rights reserved.
//

#if canImport(UIKit)
import UIKit
#endif

public struct DeviceCode: Codable {
public struct DeviceCode: TraktObject {
public let deviceCode: String
public let userCode: String
public let verificationURL: String
public let expiresIn: Int
public let interval: Int

#if canImport(UIKit)
#if canImport(CoreImage)
public func getQRCode() -> UIImage? {
let data = self.verificationURL.data(using: String.Encoding.ascii)

if let filter = CIFilter(name: "CIQRCodeGenerator") {
filter.setValue(data, forKey: "inputMessage")
let transform = CGAffineTransform(scaleX: 3, y: 3)

if let output = filter.outputImage?.transformed(by: transform) {
return UIImage(ciImage: output)
}
}
public let expiresIn: TimeInterval
public let interval: TimeInterval

return nil
}
#endif
#endif

enum CodingKeys: String, CodingKey {
case deviceCode = "device_code"
case userCode = "user_code"
Expand All @@ -43,3 +20,26 @@ public struct DeviceCode: Codable {
case interval
}
}

#if canImport(UIKit) && canImport(CoreImage)
import UIKit
import CoreImage

extension DeviceCode {
public func getQRCode(scale: CGFloat = 3) -> UIImage? {
guard
let data = "\(verificationURL)/\(userCode)".data(using: .ascii),
let filter = CIFilter(name: "CIQRCodeGenerator")
else { return nil }

filter.setValue(data, forKey: "inputMessage")

guard let output = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) else {
return nil
}

return UIImage(ciImage: output)
}
}

#endif
46 changes: 46 additions & 0 deletions Common/Models/Authentication/OAuthBody.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// OAuthBody.swift
// TraktKit
//
// Created by Maximilian Litteral on 3/5/25.
//

struct OAuthBody: TraktObject {
let code: String?
let accessToken: String?
let refreshToken: String?

let clientId: String?
let clientSecret: String?

let redirectURI: String?
let grantType: String?

enum CodingKeys: String, CodingKey {
case code
case accessToken = "token"
case refreshToken = "refresh_token"
case clientId = "client_id"
case clientSecret = "client_secret"
case redirectURI = "redirect_uri"
case grantType = "grant_type"
}

init(
code: String? = nil,
accessToken: String? = nil,
refreshToken: String? = nil,
clientId: String? = nil,
clientSecret: String? = nil,
redirectURI: String? = nil,
grantType: String? = nil
) {
self.code = code
self.accessToken = accessToken
self.refreshToken = refreshToken
self.clientId = clientId
self.clientSecret = clientSecret
self.redirectURI = redirectURI
self.grantType = grantType
}
}
46 changes: 33 additions & 13 deletions Common/Models/BodyPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,32 @@
import Foundation

/// Body data for endpoints like `/sync/history` that contains Trakt Ids.
struct TraktMediaBody<ID: Encodable>: Encodable {
struct TraktMediaBody<ID: EncodableTraktObject>: EncodableTraktObject {
let movies: [ID]?
let shows: [ID]?
let seasons: [ID]?
let episodes: [ID]?
let ids: [Int]?
/// Cast and crew, not users
let people: [ID]?

init(movies: [ID]? = nil, shows: [ID]? = nil, seasons: [ID]? = nil, episodes: [ID]? = nil, ids: [Int]? = nil, people: [ID]? = nil) {
let users: [ID]?

init(
movies: [ID]? = nil,
shows: [ID]? = nil,
seasons: [ID]? = nil,
episodes: [ID]? = nil,
ids: [Int]? = nil,
people: [ID]? = nil,
users: [ID]? = nil
) {
self.movies = movies
self.shows = shows
self.seasons = seasons
self.episodes = episodes
self.ids = ids
self.people = people
self.users = users
}
}

Expand Down Expand Up @@ -55,37 +66,46 @@ class TraktCommentBody: TraktSingleObjectBody<SyncId> {
}
}

/// ID used to sync with Trakt.
public struct SyncId: Codable, Hashable {
/**
Trakt or Slug ID to send to Trakt in POST requests related to media objects and users.
*/
public struct SyncId: TraktObject {
/// Trakt id of the movie / show / season / episode
public let trakt: Int

public let trakt: Int?
/// Slug id for movie / show / season / episode / user
public let slug: String?

enum CodingKeys: String, CodingKey {
case ids
}

enum IDCodingKeys: String, CodingKey {
case trakt
case slug
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var nested = container.nestedContainer(keyedBy: IDCodingKeys.self, forKey: .ids)
try nested.encode(trakt, forKey: .trakt)
try nested.encodeIfPresent(trakt, forKey: .trakt)
try nested.encodeIfPresent(slug, forKey: .slug)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nested = try container.nestedContainer(keyedBy: IDCodingKeys.self, forKey: .ids)
self.trakt = try nested.decode(Int.self, forKey: .trakt)
self.trakt = try nested.decodeIfPresent(Int.self, forKey: .trakt)
self.slug = try nested.decodeIfPresent(String.self, forKey: .slug)
}

public init(trakt: Int) {
public init(trakt: Int? = nil, slug: String? = nil) {
assert(!(trakt == nil && slug == nil), "One of the ids must be set.")
self.trakt = trakt
self.slug = slug
}
}

public struct AddToHistoryId: Encodable, Hashable {
public struct AddToHistoryId: EncodableTraktObject {
/// Trakt id of the movie / show / season / episode
public let trakt: Int
/// UTC datetime when the item was watched.
Expand All @@ -112,7 +132,7 @@ public struct AddToHistoryId: Encodable, Hashable {
}
}

public struct RatingId: Encodable, Hashable {
public struct RatingId: EncodableTraktObject {
/// Trakt id of the movie / show / season / episode
public let trakt: Int
/// Between 1 and 10.
Expand Down Expand Up @@ -143,7 +163,7 @@ public struct RatingId: Encodable, Hashable {
}
}

public struct CollectionId: Encodable, Hashable {
public struct CollectionId: EncodableTraktObject {
/// Trakt id of the movie / show / season / episode
public let trakt: Int
/// UTC datetime when the item was collected. Set to `released` to automatically use the inital release date.
Expand Down
2 changes: 1 addition & 1 deletion Common/Models/Calendar/CalendarMovie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public struct CalendarMovie: Codable, Hashable {
public struct CalendarMovie: TraktObject {
public let released: Date
public let movie: TraktMovie
}
2 changes: 1 addition & 1 deletion Common/Models/Calendar/CalendarShow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

public struct CalendarShow: Codable, Hashable {
public struct CalendarShow: TraktObject {
public let firstAired: Date
public let episode: TraktEpisode
public let show: TraktShow
Expand Down
4 changes: 2 additions & 2 deletions Common/Models/Certifications/Certification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

import Foundation

public struct Certifications: Codable, Hashable {
public struct Certifications: TraktObject {
public let us: [Certification]

enum CodingKeys: String, CodingKey {
case us
}

public struct Certification: Codable, Hashable {
public struct Certification: TraktObject {
public let name: String
public let slug: String
public let description: String
Expand Down
Loading