diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/TraktKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/TraktKit.xcscheme index 957c110..83eb193 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/TraktKit.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/TraktKit.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES" + systemAttachmentLifetime = "keepNever" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + diff --git a/Common/DateParser.swift b/Common/DateParser.swift index 8ee76a5..d1000c4 100644 --- a/Common/DateParser.swift +++ b/Common/DateParser.swift @@ -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))") } } diff --git a/Common/Extensions/URL+Extensions.swift b/Common/Extensions/URL+Extensions.swift new file mode 100644 index 0000000..a8b1778 --- /dev/null +++ b/Common/Extensions/URL+Extensions.swift @@ -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() + } +} diff --git a/Common/MLKeychain.swift b/Common/MLKeychain.swift index bfeb400..4fc112b 100644 --- a/Common/MLKeychain.swift +++ b/Common/MLKeychain.swift @@ -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)! diff --git a/Common/Models/Authentication/AuthenticationInfo.swift b/Common/Models/Authentication/AuthenticationInfo.swift index af30c53..bf8d8da 100644 --- a/Common/Models/Authentication/AuthenticationInfo.swift +++ b/Common/Models/Authentication/AuthenticationInfo.swift @@ -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 diff --git a/Common/Models/Authentication/DeviceCode.swift b/Common/Models/Authentication/DeviceCode.swift index fcee36b..c6f6b97 100644 --- a/Common/Models/Authentication/DeviceCode.swift +++ b/Common/Models/Authentication/DeviceCode.swift @@ -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" @@ -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 diff --git a/Common/Models/Authentication/OAuthBody.swift b/Common/Models/Authentication/OAuthBody.swift new file mode 100644 index 0000000..6c78e00 --- /dev/null +++ b/Common/Models/Authentication/OAuthBody.swift @@ -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 + } +} diff --git a/Common/Models/BodyPost.swift b/Common/Models/BodyPost.swift index 6d663a3..29e68a3 100644 --- a/Common/Models/BodyPost.swift +++ b/Common/Models/BodyPost.swift @@ -9,21 +9,32 @@ import Foundation /// Body data for endpoints like `/sync/history` that contains Trakt Ids. -struct TraktMediaBody: Encodable { +struct TraktMediaBody: 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 } } @@ -55,37 +66,46 @@ class TraktCommentBody: TraktSingleObjectBody { } } -/// 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. @@ -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. @@ -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. diff --git a/Common/Models/Calendar/CalendarMovie.swift b/Common/Models/Calendar/CalendarMovie.swift index 356b766..de19ea3 100644 --- a/Common/Models/Calendar/CalendarMovie.swift +++ b/Common/Models/Calendar/CalendarMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct CalendarMovie: Codable, Hashable { +public struct CalendarMovie: TraktObject { public let released: Date public let movie: TraktMovie } diff --git a/Common/Models/Calendar/CalendarShow.swift b/Common/Models/Calendar/CalendarShow.swift index 451ad3e..30f38b1 100644 --- a/Common/Models/Calendar/CalendarShow.swift +++ b/Common/Models/Calendar/CalendarShow.swift @@ -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 diff --git a/Common/Models/Certifications/Certification.swift b/Common/Models/Certifications/Certification.swift index 7375c5e..16a2e4d 100644 --- a/Common/Models/Certifications/Certification.swift +++ b/Common/Models/Certifications/Certification.swift @@ -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 diff --git a/Common/Models/Checkin/TraktCheckin.swift b/Common/Models/Checkin/TraktCheckin.swift index c47348d..6bac5d5 100644 --- a/Common/Models/Checkin/TraktCheckin.swift +++ b/Common/Models/Checkin/TraktCheckin.swift @@ -11,12 +11,14 @@ import Foundation /** The sharing object is optional and will apply the user's settings if not sent. If sharing is sent, each key will override the user's setting for that social network. Send true to post or false to not post on the indicated social network. You can see which social networks a user has connected with the /users/settings method. */ -public struct ShareSettings: Codable, Hashable { +public struct ShareSettings: TraktObject { + public let facebook: Bool public let twitter: Bool + public let mastodon: Bool public let tumblr: Bool } -public struct TraktCheckinBody: Codable { +public struct TraktCheckinBody: TraktObject { /// `movie` or `episode` must not be nil public let movie: SyncId? /// `movie` or `episode` must not be nil @@ -25,42 +27,27 @@ public struct TraktCheckinBody: Codable { public let sharing: ShareSettings? /// Message used for sharing. If not sent, it will use the watching string in the user settings. public let message: String? - /// Foursquare venue ID. - public let venueId: String? - /// Foursquare venue name. - public let venueName: String? - /// Version number of the app. - public let appVersion: String? - /// Build date of the app. - public let appDate: String? - - enum CodingKeys: String, CodingKey { - case movie, episode, sharing, message - case venueId = "venue_id" - case venueName = "venueName" - case appVersion = "app_version" - case appDate = "app_date" - } - - public init(movie: SyncId? = nil, episode: SyncId? = nil, sharing: ShareSettings? = nil, message: String? = nil, venueId: String? = nil, venueName: String? = nil, appVersion: String? = nil, appDate: String? = nil) { + + /** + - parameters: + - sharing: The sharing object is optional and will apply the user's settings if not sent. If `sharing` is sent, each key will override the user's setting for that social network. Send `true` to post or `false` to not post on the indicated social network. You can see which social networks a user has connected with the `/users/settings` method. + - message: Message used for sharing. If not sent, it will use the watching string in the user settings. + */ + public init(movie: SyncId? = nil, episode: SyncId? = nil, sharing: ShareSettings? = nil, message: String? = nil) { self.movie = movie self.episode = episode self.sharing = sharing self.message = message - self.venueId = venueId - self.venueName = venueName - self.appVersion = appVersion - self.appDate = appDate } } -public struct TraktCheckinResponse: Codable, Hashable { +public struct TraktCheckinResponse: TraktObject { /// A unique history id (64-bit integer) used to reference this checkin directly. public let id: Int public let watchedAt: Date public let sharing: ShareSettings - + /// If the user checked in to a movie, a movie object will be returned with it's name public let movie: TraktMovie? /// If the user checked in to an episode, a show object will be returned with it's name diff --git a/Common/Models/Comments/TraktAttachedMediaItem.swift b/Common/Models/Comments/TraktAttachedMediaItem.swift index 39e1bdb..b65f9ba 100644 --- a/Common/Models/Comments/TraktAttachedMediaItem.swift +++ b/Common/Models/Comments/TraktAttachedMediaItem.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktAttachedMediaItem: Codable, Hashable { +public struct TraktAttachedMediaItem: TraktObject { public let type: String public let movie: TraktMovie? public let show: TraktShow? diff --git a/Common/Models/Comments/TraktComment.swift b/Common/Models/Comments/TraktComment.swift index d004481..748e361 100644 --- a/Common/Models/Comments/TraktComment.swift +++ b/Common/Models/Comments/TraktComment.swift @@ -8,30 +8,46 @@ import Foundation -public struct Comment: Codable, Hashable { +public struct Comment: TraktObject { public let id: Int - public let parentId: Int - public let createdAt: Date public var comment: String public let spoiler: Bool public let review: Bool + public let parentId: Int + public let createdAt: Date + public let updatedAt: Date? public let replies: Int public let likes: Int public let userRating: Int? + public let userStats: UserStats public let user: User - + enum CodingKeys: String, CodingKey { case id - case parentId = "parent_id" - case createdAt = "created_at" case comment case spoiler case review + case parentId = "parent_id" + case createdAt = "created_at" + case updatedAt = "updated_at" case replies case likes case userRating = "user_rating" + case userStats = "user_stats" case user } + + public struct UserStats: TraktObject { + public let rating: Int? + public let playCount: Int + public let completedCount: Int + + enum CodingKeys: String, CodingKey { + case rating + case playCount = "play_count" + case completedCount = "completed_count" + } + } } public extension Sequence where Iterator.Element == Comment { diff --git a/Common/Models/Comments/TraktCommentLikedUser.swift b/Common/Models/Comments/TraktCommentLikedUser.swift index ddb3c09..049f87e 100644 --- a/Common/Models/Comments/TraktCommentLikedUser.swift +++ b/Common/Models/Comments/TraktCommentLikedUser.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktCommentLikedUser: Codable, Hashable { +public struct TraktCommentLikedUser: TraktObject { public let likedAt: Date public let user: User diff --git a/Common/Models/Comments/TraktTrendingComments.swift b/Common/Models/Comments/TraktTrendingComments.swift index c7c1023..65bc512 100644 --- a/Common/Models/Comments/TraktTrendingComments.swift +++ b/Common/Models/Comments/TraktTrendingComments.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktTrendingComment: Codable, Hashable { +public struct TraktTrendingComment: TraktObject { public let type: String public let comment: Comment public let movie: TraktMovie? diff --git a/Common/Models/Filters.swift b/Common/Models/Filters.swift index 55bb8b4..afff9b9 100644 --- a/Common/Models/Filters.swift +++ b/Common/Models/Filters.swift @@ -8,7 +8,7 @@ import Foundation -public protocol FilterType { +public protocol FilterType: Sendable { func value() -> (key: String, value: String) } diff --git a/Common/Models/Genres/Genre.swift b/Common/Models/Genres/Genre.swift index 5834b4b..72c04b9 100644 --- a/Common/Models/Genres/Genre.swift +++ b/Common/Models/Genres/Genre.swift @@ -8,7 +8,7 @@ import Foundation -public struct Genres: Codable, Hashable { +public struct Genres: TraktObject { public let name: String public let slug: String } diff --git a/Common/Models/Languages/Language.swift b/Common/Models/Languages/Language.swift index ee8dbab..430a9de 100644 --- a/Common/Models/Languages/Language.swift +++ b/Common/Models/Languages/Language.swift @@ -8,7 +8,7 @@ import Foundation -public struct Languages: Codable, Hashable { +public struct Languages: TraktObject { public let name: String public let code: String } diff --git a/Common/Models/Movies/TraktAnticipatedMovie.swift b/Common/Models/Movies/TraktAnticipatedMovie.swift index 49bcefc..a937b7f 100644 --- a/Common/Models/Movies/TraktAnticipatedMovie.swift +++ b/Common/Models/Movies/TraktAnticipatedMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktAnticipatedMovie: Codable, Hashable { +public struct TraktAnticipatedMovie: TraktObject { // Extended: Min public let listCount: Int public let movie: TraktMovie diff --git a/Common/Models/Movies/TraktBoxOfficeMovie.swift b/Common/Models/Movies/TraktBoxOfficeMovie.swift index 3d378b5..fb534ee 100644 --- a/Common/Models/Movies/TraktBoxOfficeMovie.swift +++ b/Common/Models/Movies/TraktBoxOfficeMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktBoxOfficeMovie: Codable, Hashable { +public struct TraktBoxOfficeMovie: TraktObject { // Extended: Min public let revenue: Int public let movie: TraktMovie diff --git a/Common/Models/Movies/TraktDVDReleaseMovie.swift b/Common/Models/Movies/TraktDVDReleaseMovie.swift index 4043ad1..b445230 100644 --- a/Common/Models/Movies/TraktDVDReleaseMovie.swift +++ b/Common/Models/Movies/TraktDVDReleaseMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktDVDReleaseMovie: Codable, Hashable { +public struct TraktDVDReleaseMovie: TraktObject { // Extended: Min public let released: Date public let movie: TraktMovie diff --git a/Common/Models/Movies/TraktFavoritedMovie.swift b/Common/Models/Movies/TraktFavoritedMovie.swift new file mode 100644 index 0000000..a173e61 --- /dev/null +++ b/Common/Models/Movies/TraktFavoritedMovie.swift @@ -0,0 +1,16 @@ +// +// TraktFavoritedMovie.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/22/25. +// + +public struct TraktFavoritedMovie: TraktObject { + public let userCount: Int + public let movie: TraktMovie + + enum CodingKeys: String, CodingKey { + case userCount = "user_count" + case movie + } +} diff --git a/Common/Models/Movies/TraktMostMovie.swift b/Common/Models/Movies/TraktMostMovie.swift index 163314a..67f95fc 100644 --- a/Common/Models/Movies/TraktMostMovie.swift +++ b/Common/Models/Movies/TraktMostMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktMostMovie: Codable, Hashable { +public struct TraktMostMovie: TraktObject { // Extended: Min public let watcherCount: Int diff --git a/Common/Models/Movies/TraktMovie.swift b/Common/Models/Movies/TraktMovie.swift index 6cd1d41..3b20ada 100644 --- a/Common/Models/Movies/TraktMovie.swift +++ b/Common/Models/Movies/TraktMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktMovie: Codable, Hashable { +public struct TraktMovie: TraktObject { // Extended: Min public let title: String @@ -29,7 +29,10 @@ public struct TraktMovie: Codable, Hashable { public let availableTranslations: [String]? public let genres: [String]? public let certification: String? - + + // Extended: Images + public let images: TraktImages? + enum CodingKeys: String, CodingKey { case title case year @@ -48,6 +51,8 @@ public struct TraktMovie: Codable, Hashable { case availableTranslations = "available_translations" case genres case certification + + case images } public init(from decoder: Decoder) throws { @@ -61,17 +66,36 @@ public struct TraktMovie: Codable, Hashable { released = try container.decodeIfPresent(Date.self, forKey: CodingKeys.released) runtime = try container.decodeIfPresent(Int.self, forKey: CodingKeys.runtime) certification = try container.decodeIfPresent(String.self, forKey: .certification) - trailer = try? container.decode(URL.self, forKey: .trailer) - homepage = try? container.decode(URL.self, forKey: .homepage) + trailer = try container.decodeIfPresent(URL.self, forKey: .trailer) + homepage = try container.decodeIfPresent(URL.self, forKey: .homepage) rating = try container.decodeIfPresent(Double.self, forKey: .rating) votes = try container.decodeIfPresent(Int.self, forKey: .votes) updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) language = try container.decodeIfPresent(String.self, forKey: .language) availableTranslations = try container.decodeIfPresent([String].self, forKey: .availableTranslations) genres = try container.decodeIfPresent([String].self, forKey: .genres) + images = try container.decodeIfPresent(TraktImages.self, forKey: .images) } - public init(title: String, year: Int? = nil, ids: ID, tagline: String? = nil, overview: String? = nil, released: Date? = nil, runtime: Int? = nil, trailer: URL? = nil, homepage: URL? = nil, rating: Double? = nil, votes: Int? = nil, updatedAt: Date? = nil, language: String? = nil, availableTranslations: [String]? = nil, genres: [String]? = nil, certification: String? = nil) { + public init( + title: String, + year: Int? = nil, + ids: ID, + tagline: String? = nil, + overview: String? = nil, + released: Date? = nil, + runtime: Int? = nil, + trailer: URL? = nil, + homepage: URL? = nil, + rating: Double? = nil, + votes: Int? = nil, + updatedAt: Date? = nil, + language: String? = nil, + availableTranslations: [String]? = nil, + genres: [String]? = nil, + certification: String? = nil, + images: TraktImages? = nil + ) { self.title = title self.year = year self.ids = ids @@ -88,5 +112,6 @@ public struct TraktMovie: Codable, Hashable { self.availableTranslations = availableTranslations self.genres = genres self.certification = certification + self.images = images } } diff --git a/Common/Models/Movies/TraktMovieRelease.swift b/Common/Models/Movies/TraktMovieRelease.swift index 5f266d0..eab5a73 100644 --- a/Common/Models/Movies/TraktMovieRelease.swift +++ b/Common/Models/Movies/TraktMovieRelease.swift @@ -8,11 +8,12 @@ import Foundation -public struct TraktMovieRelease: Codable, Hashable { +public struct TraktMovieRelease: TraktObject { public let country: String public let certification: String public let releaseDate: Date public let releaseType: ReleaseType + /// The `note` might have optional info such as the film festival name for a premiere release or Blu-ray specs for a `physical` release. public let note: String? enum CodingKeys: String, CodingKey { @@ -23,7 +24,7 @@ public struct TraktMovieRelease: Codable, Hashable { case note } - public enum ReleaseType: String, Codable { + public enum ReleaseType: String, TraktObject { case unknown case premiere case limited diff --git a/Common/Models/Movies/TraktMovieTranslation.swift b/Common/Models/Movies/TraktMovieTranslation.swift index e39ab37..277ac81 100644 --- a/Common/Models/Movies/TraktMovieTranslation.swift +++ b/Common/Models/Movies/TraktMovieTranslation.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktMovieTranslation: Codable, Hashable { +public struct TraktMovieTranslation: TraktObject { public let title: String public let overview: String public let tagline: String diff --git a/Common/Models/Movies/TraktTrendingMovie.swift b/Common/Models/Movies/TraktTrendingMovie.swift index 25cc747..cf0f5b1 100644 --- a/Common/Models/Movies/TraktTrendingMovie.swift +++ b/Common/Models/Movies/TraktTrendingMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktTrendingMovie: Codable, Hashable { +public struct TraktTrendingMovie: TraktObject { // Extended: Min public let watchers: Int public let movie: TraktMovie diff --git a/Common/Models/Movies/TraktWatchedMovie.swift b/Common/Models/Movies/TraktWatchedMovie.swift index fc8f500..2e96ad0 100644 --- a/Common/Models/Movies/TraktWatchedMovie.swift +++ b/Common/Models/Movies/TraktWatchedMovie.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktWatchedMovie: Codable, Hashable { +public struct TraktWatchedMovie: TraktObject { // Extended: Min public let plays: Int // Total number of plays public let lastWatchedAt: Date diff --git a/Common/Models/People/CastAndCrew.swift b/Common/Models/People/CastAndCrew.swift index 1ea9013..a763078 100644 --- a/Common/Models/People/CastAndCrew.swift +++ b/Common/Models/People/CastAndCrew.swift @@ -8,7 +8,7 @@ import Foundation -public struct CastAndCrew: Hashable { +public struct CastAndCrew: TraktObject { public let cast: [Cast]? public let guestStars: [Cast]? public let directors: [Crew]? @@ -45,7 +45,7 @@ public struct CastAndCrew: H } } -extension CastAndCrew: Decodable { +extension CastAndCrew { public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) cast = try values.decodeIfPresent([Cast].self, forKey: .cast) @@ -64,9 +64,7 @@ extension CastAndCrew: Decodable { crew = try crewContainer?.decodeIfPresent([Crew].self, forKey: .crew) lighting = try crewContainer?.decodeIfPresent([Crew].self, forKey: .lighting) } -} -extension CastAndCrew: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(cast, forKey: .cast) diff --git a/Common/Models/People/Person.swift b/Common/Models/People/Person.swift index 25e9e71..cef0e2f 100644 --- a/Common/Models/People/Person.swift +++ b/Common/Models/People/Person.swift @@ -8,7 +8,7 @@ import Foundation // Actor/Actress/Crew member -public struct Person: Codable, Hashable { +public struct Person: TraktObject { // Extended: Min public let name: String public let ids: ID diff --git a/Common/Models/People/TraktCastMember.swift b/Common/Models/People/TraktCastMember.swift index de1f6d9..3112f3b 100644 --- a/Common/Models/People/TraktCastMember.swift +++ b/Common/Models/People/TraktCastMember.swift @@ -9,7 +9,7 @@ import Foundation /// Cast member for (show/season/episode)/.../people API -public struct TVCastMember: Codable, Hashable { +public struct TVCastMember: TraktObject { public let characters: [String] @available(*, deprecated, renamed: "characters") public let character: String @@ -26,21 +26,13 @@ public struct TVCastMember: Codable, Hashable { } /// Cast member for /movies/.../people API -public struct MovieCastMember: Codable, Hashable { +public struct MovieCastMember: TraktObject { public let characters: [String] - @available(*, deprecated, renamed: "characters") - public let character: String public let person: Person - - enum CodingKeys: String, CodingKey { - case characters - case character - case person - } } /// Cast member for /people/.../shows API -public struct PeopleTVCastMember: Codable, Hashable { +public struct PeopleTVCastMember: TraktObject { public let characters: [String] @available(*, deprecated, renamed: "characters") public let character: String @@ -58,7 +50,7 @@ public struct PeopleTVCastMember: Codable, Hashable { } /// Cast member for /people/.../movies API -public struct PeopleMovieCastMember: Codable, Hashable { +public struct PeopleMovieCastMember: TraktObject { public let characters: [String] @available(*, deprecated, renamed: "characters") public let character: String diff --git a/Common/Models/People/TraktCrewMember.swift b/Common/Models/People/TraktCrewMember.swift index 796752f..4f17696 100644 --- a/Common/Models/People/TraktCrewMember.swift +++ b/Common/Models/People/TraktCrewMember.swift @@ -9,7 +9,7 @@ import Foundation /// Cast member for (show/season/episode)/people API -public struct TVCrewMember: Codable, Hashable { +public struct TVCrewMember: TraktObject { public let jobs: [String] @available(*, deprecated, renamed: "jobs") public let job: String @@ -26,21 +26,13 @@ public struct TVCrewMember: Codable, Hashable { } /// Cast member for /movies/.../people API -public struct MovieCrewMember: Codable, Hashable { +public struct MovieCrewMember: TraktObject { public let jobs: [String] - @available(*, deprecated, renamed: "jobs") - public let job: String public let person: Person - - enum CodingKeys: String, CodingKey { - case jobs - case job - case person - } } /// Cast member for /people/.../shows API -public struct PeopleTVCrewMember: Codable, Hashable { +public struct PeopleTVCrewMember: TraktObject { public let jobs: [String] @available(*, deprecated, renamed: "jobs") public let job: String @@ -57,7 +49,7 @@ public struct PeopleTVCrewMember: Codable, Hashable { /// Cast member for /people/.../movies API -public struct PeopleMovieCrewMember: Codable, Hashable { +public struct PeopleMovieCrewMember: TraktObject { public let jobs: [String] @available(*, deprecated, renamed: "jobs") public let job: String diff --git a/Common/Models/Scrobbble/ScrobbleResult.swift b/Common/Models/Scrobbble/ScrobbleResult.swift index e27a7ae..25b453c 100644 --- a/Common/Models/Scrobbble/ScrobbleResult.swift +++ b/Common/Models/Scrobbble/ScrobbleResult.swift @@ -8,7 +8,7 @@ import Foundation -public struct ScrobbleResult: Codable, Hashable { +public struct ScrobbleResult: TraktObject { public let id: Int public let action: String public let progress: Float diff --git a/Common/Models/Shared/Alias.swift b/Common/Models/Shared/Alias.swift index 8a2b2b0..665674d 100644 --- a/Common/Models/Shared/Alias.swift +++ b/Common/Models/Shared/Alias.swift @@ -8,7 +8,7 @@ import Foundation -public struct Alias: Codable, Hashable { +public struct Alias: TraktObject { public let title: String public let country: String } diff --git a/Common/Models/Shared/RatingDistribution.swift b/Common/Models/Shared/RatingDistribution.swift index 108e4f0..e3c733a 100644 --- a/Common/Models/Shared/RatingDistribution.swift +++ b/Common/Models/Shared/RatingDistribution.swift @@ -8,12 +8,12 @@ import Foundation -public struct RatingDistribution: Codable, Hashable { +public struct RatingDistribution: TraktObject { public let rating: Double public let votes: Int public let distribution: Distribution - public struct Distribution: Codable, Hashable { + public struct Distribution: TraktObject { public let one: Int public let two: Int public let three: Int diff --git a/Common/Models/Shared/Update.swift b/Common/Models/Shared/Update.swift index 01827af..e89d8e6 100644 --- a/Common/Models/Shared/Update.swift +++ b/Common/Models/Shared/Update.swift @@ -8,7 +8,7 @@ import Foundation -public struct Update: Codable, Hashable { +public struct Update: TraktObject { public let updatedAt: Date public let movie: TraktMovie? public let show: TraktShow? diff --git a/Common/Models/Shows/ShowCollectionProgress.swift b/Common/Models/Shows/ShowCollectionProgress.swift index 4c38ed5..94bb40a 100644 --- a/Common/Models/Shows/ShowCollectionProgress.swift +++ b/Common/Models/Shows/ShowCollectionProgress.swift @@ -8,7 +8,7 @@ import Foundation -public struct ShowCollectionProgress: Codable, Hashable { +public struct ShowCollectionProgress: TraktObject { public let aired: Int public let completed: Int @@ -26,14 +26,14 @@ public struct ShowCollectionProgress: Codable, Hashable { case nextEpisode = "next_episode" } - public struct CollectedSeason: Codable, Hashable { + public struct CollectedSeason: TraktObject { public let number: Int public let aired: Int public let completed: Int public let episodes: [CollectedEpisode] } - public struct CollectedEpisode: Codable, Hashable { + public struct CollectedEpisode: TraktObject { public let number: Int public let completed: Bool public let collectedAt: Date? diff --git a/Common/Models/Shows/TraktAnticipatedShow.swift b/Common/Models/Shows/TraktAnticipatedShow.swift index e0a06ff..5836222 100644 --- a/Common/Models/Shows/TraktAnticipatedShow.swift +++ b/Common/Models/Shows/TraktAnticipatedShow.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktAnticipatedShow: Codable, Hashable { +public struct TraktAnticipatedShow: TraktObject { // Extended: Min public let listCount: Int diff --git a/Common/Models/Shows/TraktEpisode.swift b/Common/Models/Shows/TraktEpisode.swift index ac5065b..e2a274b 100644 --- a/Common/Models/Shows/TraktEpisode.swift +++ b/Common/Models/Shows/TraktEpisode.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktEpisode: Codable, Hashable { +public struct TraktEpisode: TraktObject { // Extended: Min public let season: Int @@ -26,7 +26,12 @@ public struct TraktEpisode: Codable, Hashable { public let absoluteNumber: Int? public let runtime: Int? public let commentCount: Int? - + /// When getting full extended info, the `episodeType` field can have a value of `standard`, `series_premiere` (season 1, episode 1), `season_premiere` (episode 1), `mid_season_finale`,` mid_season_premiere` (the next episode after the mid season finale), `season_finale`, or `series_finale` (last episode to air for an ended show). + public let episodeType: String? + + // Extended: Images + public let images: TraktImages? + enum CodingKeys: String, CodingKey { case season case number @@ -42,9 +47,28 @@ public struct TraktEpisode: Codable, Hashable { case absoluteNumber = "number_abs" case runtime case commentCount = "comment_count" + case episodeType = "episode_type" + + case images } - public init(season: Int, number: Int, title: String? = nil, ids: EpisodeId, overview: String? = nil, rating: Double? = nil, votes: Int? = nil, firstAired: Date? = nil, updatedAt: Date? = nil, availableTranslations: [String]? = nil, absoluteNumber: Int? = nil, runtime: Int? = nil, commentCount: Int? = nil) { + public init( + season: Int, + number: Int, + title: String? = nil, + ids: EpisodeId, + overview: String? = nil, + rating: Double? = nil, + votes: Int? = nil, + firstAired: Date? = nil, + updatedAt: Date? = nil, + availableTranslations: [String]? = nil, + absoluteNumber: Int? = nil, + runtime: Int? = nil, + commentCount: Int? = nil, + episodeType: String? = nil, + images: TraktImages? = nil + ) { self.season = season self.number = number self.title = title @@ -56,7 +80,9 @@ public struct TraktEpisode: Codable, Hashable { self.updatedAt = updatedAt self.availableTranslations = availableTranslations self.absoluteNumber = absoluteNumber - self.runtime = runtime self.commentCount = commentCount + self.runtime = runtime + self.episodeType = episodeType + self.images = images } } diff --git a/Common/Models/Shows/TraktEpisodeTranslation.swift b/Common/Models/Shows/TraktEpisodeTranslation.swift index 121edbc..ececd8c 100644 --- a/Common/Models/Shows/TraktEpisodeTranslation.swift +++ b/Common/Models/Shows/TraktEpisodeTranslation.swift @@ -8,8 +8,15 @@ import Foundation -public struct TraktEpisodeTranslation: Codable, Hashable { +public struct TraktEpisodeTranslation: TraktObject { public let title: String public let overview: String public let language: String } + +public struct TraktSeasonTranslation: TraktObject { + public let title: String + public let overview: String + public let language: String + public let country: String +} diff --git a/Common/Models/Shows/TraktFavoritedShow.swift b/Common/Models/Shows/TraktFavoritedShow.swift new file mode 100644 index 0000000..d4f0e46 --- /dev/null +++ b/Common/Models/Shows/TraktFavoritedShow.swift @@ -0,0 +1,16 @@ +// +// TraktFavoritedShow.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/22/25. +// + +public struct TraktFavoritedShow: TraktObject { + public let userCount: Int + public let show: TraktShow + + enum CodingKeys: String, CodingKey { + case userCount = "user_count" + case show + } +} diff --git a/Common/Models/Shows/TraktMostShow.swift b/Common/Models/Shows/TraktMostShow.swift index a649873..bb1f4ee 100644 --- a/Common/Models/Shows/TraktMostShow.swift +++ b/Common/Models/Shows/TraktMostShow.swift @@ -9,7 +9,7 @@ import Foundation /// Used for most played, watched, and collected shows -public struct TraktMostShow: Codable, Hashable { +public struct TraktMostShow: TraktObject { // Extended: Min public let watcherCount: Int diff --git a/Common/Models/Shows/TraktSeason.swift b/Common/Models/Shows/TraktSeason.swift index e5ba92a..174474f 100644 --- a/Common/Models/Shows/TraktSeason.swift +++ b/Common/Models/Shows/TraktSeason.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktSeason: Codable, Hashable { +public struct TraktSeason: TraktObject { // Extended: Min public let number: Int @@ -27,7 +27,10 @@ public struct TraktSeason: Codable, Hashable { // Extended: Episodes public let episodes: [TraktEpisode]? - + + // Extended: Images + public let images: TraktImages? + enum CodingKeys: String, CodingKey { case number case ids @@ -43,9 +46,25 @@ public struct TraktSeason: Codable, Hashable { case network case episodes + + case images } - public init(number: Int, ids: SeasonId, rating: Double? = nil, votes: Int? = nil, episodeCount: Int? = nil, airedEpisodes: Int? = nil, title: String? = nil, overview: String? = nil, firstAired: Date? = nil, updatedAt: Date? = nil, network: String? = nil, episodes: [TraktEpisode]? = nil) { + public init( + number: Int, + ids: SeasonId, + rating: Double? = nil, + votes: Int? = nil, + episodeCount: Int? = nil, + airedEpisodes: Int? = nil, + title: String? = nil, + overview: String? = nil, + firstAired: Date? = nil, + updatedAt: Date? = nil, + network: String? = nil, + episodes: [TraktEpisode]? = nil, + images: TraktImages? = nil + ) { self.number = number self.ids = ids self.rating = rating @@ -58,5 +77,6 @@ public struct TraktSeason: Codable, Hashable { self.updatedAt = updatedAt self.network = network self.episodes = episodes + self.images = images } } diff --git a/Common/Models/Shows/TraktShow.swift b/Common/Models/Shows/TraktShow.swift index 5f19227..74019f2 100644 --- a/Common/Models/Shows/TraktShow.swift +++ b/Common/Models/Shows/TraktShow.swift @@ -8,7 +8,7 @@ import Foundation -public struct Airs: Codable, Hashable { +public struct Airs: TraktObject { public let day: String? public let time: String? public let timezone: String? @@ -20,7 +20,7 @@ public struct Airs: Codable, Hashable { } } -public struct TraktShow: Codable, Hashable { +public struct TraktShow: TraktObject { // Extended: Min public let title: String @@ -45,7 +45,10 @@ public struct TraktShow: Codable, Hashable { public let availableTranslations: [String]? public let genres: [String]? public let airedEpisodes: Int? - + + // Extended: Images + public let images: TraktImages? + enum CodingKeys: String, CodingKey { case title case year @@ -68,6 +71,8 @@ public struct TraktShow: Codable, Hashable { case availableTranslations = "available_translations" case genres case airedEpisodes = "aired_episodes" + + case images } public init(from decoder: Decoder) throws { @@ -83,8 +88,8 @@ public struct TraktShow: Codable, Hashable { certification = try container.decodeIfPresent(String.self, forKey: .certification) network = try container.decodeIfPresent(String.self, forKey: .network) country = try container.decodeIfPresent(String.self, forKey: .country) - trailer = try? container.decode(URL.self, forKey: .trailer) - homepage = try? container.decode(URL.self, forKey: .homepage) + trailer = try container.decodeIfPresent(URL.self, forKey: .trailer) + homepage = try container.decodeIfPresent(URL.self, forKey: .homepage) status = try container.decodeIfPresent(String.self, forKey: .status) rating = try container.decodeIfPresent(Double.self, forKey: .rating) votes = try container.decodeIfPresent(Int.self, forKey: .votes) @@ -93,9 +98,33 @@ public struct TraktShow: Codable, Hashable { availableTranslations = try container.decodeIfPresent([String].self, forKey: .availableTranslations) genres = try container.decodeIfPresent([String].self, forKey: .genres) airedEpisodes = try container.decodeIfPresent(Int.self, forKey: .airedEpisodes) + + images = try container.decodeIfPresent(TraktImages.self, forKey: .images) } - public init(title: String, year: Int? = nil, ids: ID, overview: String? = nil, firstAired: Date? = nil, airs: Airs? = nil, runtime: Int? = nil, certification: String? = nil, network: String? = nil, country: String? = nil, trailer: URL? = nil, homepage: URL? = nil, status: String? = nil, rating: Double? = nil, votes: Int? = nil, updatedAt: Date? = nil, language: String? = nil, availableTranslations: [String]? = nil, genres: [String]? = nil, airedEpisodes: Int? = nil) { + public init( + title: String, + year: Int? = nil, + ids: ID, + overview: String? = nil, + firstAired: Date? = nil, + airs: Airs? = nil, + runtime: Int? = nil, + certification: String? = nil, + network: String? = nil, + country: String? = nil, + trailer: URL? = nil, + homepage: URL? = nil, + status: String? = nil, + rating: Double? = nil, + votes: Int? = nil, + updatedAt: Date? = nil, + language: String? = nil, + availableTranslations: [String]? = nil, + genres: [String]? = nil, + airedEpisodes: Int? = nil, + images: TraktImages? = nil + ) { self.title = title self.year = year self.ids = ids @@ -116,5 +145,6 @@ public struct TraktShow: Codable, Hashable { self.availableTranslations = availableTranslations self.genres = genres self.airedEpisodes = airedEpisodes + self.images = images } } diff --git a/Common/Models/Shows/TraktShowTranslation.swift b/Common/Models/Shows/TraktShowTranslation.swift index dc0af9e..e59885f 100644 --- a/Common/Models/Shows/TraktShowTranslation.swift +++ b/Common/Models/Shows/TraktShowTranslation.swift @@ -8,8 +8,10 @@ import Foundation -public struct TraktShowTranslation: Codable, Hashable { +public struct TraktShowTranslation: TraktObject { public let title: String? - public let overview: String + public let overview: String? + public let tagline: String? public let language: String + public let country: String } diff --git a/Common/Models/Shows/TraktTrendingShow.swift b/Common/Models/Shows/TraktTrendingShow.swift index 7c389e1..6eec167 100644 --- a/Common/Models/Shows/TraktTrendingShow.swift +++ b/Common/Models/Shows/TraktTrendingShow.swift @@ -8,9 +8,7 @@ import Foundation -public struct TraktTrendingShow: Codable, Hashable { - - // Extended: Min +public struct TraktTrendingShow: TraktObject { public let watchers: Int public let show: TraktShow } diff --git a/Common/Models/Shows/TraktWatchedEpisodes.swift b/Common/Models/Shows/TraktWatchedEpisodes.swift index 3e6e59d..625ce50 100644 --- a/Common/Models/Shows/TraktWatchedEpisodes.swift +++ b/Common/Models/Shows/TraktWatchedEpisodes.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktWatchedEpisodes: Codable, Hashable { +public struct TraktWatchedEpisodes: TraktObject { // Extended: Min public let number: Int public let plays: Int diff --git a/Common/Models/Shows/TraktWatchedProgress.swift b/Common/Models/Shows/TraktWatchedProgress.swift index c565a87..b5827bf 100644 --- a/Common/Models/Shows/TraktWatchedProgress.swift +++ b/Common/Models/Shows/TraktWatchedProgress.swift @@ -9,7 +9,7 @@ import Foundation /// Watched progress. Shows/Progress/Watched -public struct TraktShowWatchedProgress: Codable, Hashable { +public struct TraktShowWatchedProgress: TraktObject { // Extended: Min /// Number of episodes that have aired @@ -30,7 +30,7 @@ public struct TraktShowWatchedProgress: Codable, Hashable { } } -public struct TraktSeasonWatchedProgress: Codable, Hashable { +public struct TraktSeasonWatchedProgress: TraktObject { // Extended: Min /// Season number @@ -42,7 +42,7 @@ public struct TraktSeasonWatchedProgress: Codable, Hashable { public let episodes: [TraktEpisodeWatchedProgress] } -public struct TraktEpisodeWatchedProgress: Codable, Hashable { +public struct TraktEpisodeWatchedProgress: TraktObject { // Extended: Min /// Season number diff --git a/Common/Models/Shows/TraktWatchedSeason.swift b/Common/Models/Shows/TraktWatchedSeason.swift index acd9213..68b2a10 100644 --- a/Common/Models/Shows/TraktWatchedSeason.swift +++ b/Common/Models/Shows/TraktWatchedSeason.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktWatchedSeason: Codable, Hashable { +public struct TraktWatchedSeason: TraktObject { // Extended: Min public let number: Int // Season number diff --git a/Common/Models/Shows/TraktWatchedShow.swift b/Common/Models/Shows/TraktWatchedShow.swift index 9cd71b7..89e375c 100644 --- a/Common/Models/Shows/TraktWatchedShow.swift +++ b/Common/Models/Shows/TraktWatchedShow.swift @@ -8,28 +8,24 @@ import Foundation -public struct TraktWatchedShow: Codable, Hashable { +public struct TraktWatchedShow: TraktObject { // Extended: Min - public let plays: Int // Total number of plays - public let lastWatchedAt: Date? - public let lastUpdatedAt: Date? + + public let plays: Int + public let lastWatchedAt: Date + public let lastUpdatedAt: Date + public let resetAt: Date? public let show: TraktShow + /// nil if you use `?extended=noseasons` public let seasons: [TraktWatchedSeason]? enum CodingKeys: String, CodingKey { case plays case lastWatchedAt = "last_watched_at" case lastUpdatedAt = "last_updated_at" + case resetAt = "reset_at" case show case seasons } - - public init(plays: Int, lastWatchedAt: Date?, lastUpdatedAt: Date?, show: TraktShow, seasons: [TraktWatchedSeason]?) { - self.plays = plays - self.lastWatchedAt = lastWatchedAt - self.lastUpdatedAt = lastUpdatedAt - self.show = show - self.seasons = seasons - } } diff --git a/Common/Models/Structures.swift b/Common/Models/Structures.swift index 6a680f5..ed144d7 100644 --- a/Common/Models/Structures.swift +++ b/Common/Models/Structures.swift @@ -12,7 +12,7 @@ public typealias RawJSON = [String: Any] // Dictionary // MARK: - TV & Movies -public struct ID: Codable, Hashable { +public struct ID: TraktObject { public let trakt: Int public let slug: String @@ -40,8 +40,8 @@ public struct ID: Codable, Hashable { } } -public struct SeasonId: Codable, Hashable { - +public struct SeasonId: TraktObject { + public let trakt: Int public let tvdb: Int? public let tmdb: Int? @@ -62,7 +62,7 @@ public struct SeasonId: Codable, Hashable { } } -public struct EpisodeId: Codable, Hashable { +public struct EpisodeId: TraktObject { public let trakt: Int public let tvdb: Int? public let imdb: String? @@ -86,7 +86,7 @@ public struct EpisodeId: Codable, Hashable { } } -public struct ListId: Codable, Hashable { +public struct ListId: TraktObject { public let trakt: Int public let slug: String @@ -103,7 +103,7 @@ public struct ListId: Codable, Hashable { // MARK: - Stats -public struct TraktStats: Codable, Hashable { +public struct TraktStats: TraktObject { public let watchers: Int public let plays: Int public let collectors: Int @@ -123,100 +123,168 @@ public struct TraktStats: Codable, Hashable { } } +public struct TraktMovieStats: TraktObject { + public let watchers: Int + public let plays: Int + public let collectors: Int + public let comments: Int + public let lists: Int + public let votes: Int + public let favorited: Int +} + // MARK: - Last Activities -public struct TraktLastActivities: Codable, Hashable { +public struct TraktLastActivities: TraktObject { public let all: Date - public let movies: TraktLastActivityMovies - public let episodes: TraktLastActivityEpisodes - public let shows: TraktLastActivityShows - public let seasons: TraktLastActivitySeasons - public let comments: TraktLastActivityComments - public let lists: TraktLastActivityLists -} + public let movies: Movies + public let episodes: Episodes + public let shows: Shows + public let seasons: Seasons + public let comments: Comments + public let lists: Lists + public let watchlist: LastUpdated + public let favorites: LastUpdated + public let account: Account + public let savedFilters: LastUpdated + public let notes: LastUpdated -public struct TraktLastActivityMovies: Codable, Hashable { - public let watchedAt: Date - public let collectedAt: Date - public let ratedAt: Date - public let watchlistedAt: Date - public let commentedAt: Date - public let pausedAt: Date - public let hiddenAt: Date - enum CodingKeys: String, CodingKey { - case watchedAt = "watched_at" - case collectedAt = "collected_at" - case ratedAt = "rated_at" - case watchlistedAt = "watchlisted_at" - case commentedAt = "commented_at" - case pausedAt = "paused_at" - case hiddenAt = "hidden_at" + case all + case movies + case episodes + case shows + case seasons + case comments + case lists + case watchlist + case favorites + case account + case savedFilters = "saved_filters" + case notes } -} -public struct TraktLastActivityEpisodes: Codable, Hashable { - public let watchedAt: Date - public let collectedAt: Date - public let ratedAt: Date - public let watchlistedAt: Date - public let commentedAt: Date - public let pausedAt: Date - - enum CodingKeys: String, CodingKey { - case watchedAt = "watched_at" - case collectedAt = "collected_at" - case ratedAt = "rated_at" - case watchlistedAt = "watchlisted_at" - case commentedAt = "commented_at" - case pausedAt = "paused_at" + public struct Movies: TraktObject { + public let watchedAt: Date + public let collectedAt: Date + public let ratedAt: Date + public let watchlistedAt: Date + public let favoritesAt: Date + public let commentedAt: Date + public let pausedAt: Date + public let hiddenAt: Date + + enum CodingKeys: String, CodingKey { + case watchedAt = "watched_at" + case collectedAt = "collected_at" + case ratedAt = "rated_at" + case watchlistedAt = "watchlisted_at" + case favoritesAt = "favorited_at" + case commentedAt = "commented_at" + case pausedAt = "paused_at" + case hiddenAt = "hidden_at" + } } -} -public struct TraktLastActivityShows: Codable, Hashable { - public let ratedAt: Date - public let watchlistedAt: Date - public let commentedAt: Date - public let hiddenAt: Date - - enum CodingKeys: String, CodingKey { - case ratedAt = "rated_at" - case watchlistedAt = "watchlisted_at" - case commentedAt = "commented_at" - case hiddenAt = "hidden_at" + public struct Episodes: TraktObject { + public let watchedAt: Date + public let collectedAt: Date + public let ratedAt: Date + public let watchlistedAt: Date + public let commentedAt: Date + public let pausedAt: Date + + enum CodingKeys: String, CodingKey { + case watchedAt = "watched_at" + case collectedAt = "collected_at" + case ratedAt = "rated_at" + case watchlistedAt = "watchlisted_at" + case commentedAt = "commented_at" + case pausedAt = "paused_at" + } } -} -public struct TraktLastActivitySeasons: Codable, Hashable { - public let ratedAt: Date - public let watchlistedAt: Date - public let commentedAt: Date - public let hiddenAt: Date - - enum CodingKeys: String, CodingKey { - case ratedAt = "rated_at" - case watchlistedAt = "watchlisted_at" - case commentedAt = "commented_at" - case hiddenAt = "hidden_at" + public struct Shows: TraktObject { + public let ratedAt: Date + public let watchlistedAt: Date + public let favoritesAt: Date + public let commentedAt: Date + public let hiddenAt: Date + public let droppedAt: Date + + enum CodingKeys: String, CodingKey { + case ratedAt = "rated_at" + case watchlistedAt = "watchlisted_at" + case favoritesAt = "favorited_at" + case commentedAt = "commented_at" + case hiddenAt = "hidden_at" + case droppedAt = "dropped_at" + } } -} -public struct TraktLastActivityComments: Codable, Hashable { - public let likedAt: Date - - enum CodingKeys: String, CodingKey { - case likedAt = "liked_at" + public struct Seasons: TraktObject { + public let ratedAt: Date + public let watchlistedAt: Date + public let commentedAt: Date + public let hiddenAt: Date + + enum CodingKeys: String, CodingKey { + case ratedAt = "rated_at" + case watchlistedAt = "watchlisted_at" + case commentedAt = "commented_at" + case hiddenAt = "hidden_at" + } } -} -public struct TraktLastActivityLists: Codable, Hashable { - public let likedAt: Date - public let updatedAt: Date - public let commentedAt: Date - - enum CodingKeys: String, CodingKey { - case likedAt = "liked_at" - case updatedAt = "updated_at" - case commentedAt = "commented_at" + public struct Comments: TraktObject { + public let likedAt: Date + public let blockedAt: Date + + enum CodingKeys: String, CodingKey { + case likedAt = "liked_at" + case blockedAt = "blocked_at" + } + } + + public struct Lists: TraktObject { + public let likedAt: Date + public let updatedAt: Date + public let commentedAt: Date + + enum CodingKeys: String, CodingKey { + case likedAt = "liked_at" + case updatedAt = "updated_at" + case commentedAt = "commented_at" + } + } + + public struct Account: TraktObject { + /// When the OAuth user updates any of their Trakt settings on the website + public let settingsChanged: Date + /// When another Trakt user follows or unfollows the OAuth user. + public let followedChanged: Date + /// When the OAuth user follows or unfollows another Trakt user. + public let followingChanged: Date + /// When the OAuth user follows a private account, which requires their approval. + public let pendingFollowingChanged: Date + /// When the OAuth user has a private account and someone requests to follow them. + public let requestedFollowingChanged: Date + + enum CodingKeys: String, CodingKey { + case settingsChanged = "settings_at" + case followedChanged = "followed_at" + case followingChanged = "following_at" + case pendingFollowingChanged = "pending_at" + case requestedFollowingChanged = "requested_at" + } + } + + /// Used for watchlist, favorites, saved filters, and notes. + public struct LastUpdated: TraktObject { + public let updatedAt: Date + + enum CodingKeys: String, CodingKey { + case updatedAt = "updated_at" + } } } diff --git a/Common/Models/Sync/AddRatingsResult.swift b/Common/Models/Sync/AddRatingsResult.swift index e8f4fac..7f3de69 100644 --- a/Common/Models/Sync/AddRatingsResult.swift +++ b/Common/Models/Sync/AddRatingsResult.swift @@ -8,18 +8,18 @@ import Foundation -public struct AddRatingsResult: Codable, Hashable { +public struct AddRatingsResult: TraktObject { public let added: Added public let notFound: NotFound - public struct Added: Codable, Hashable { + public struct Added: TraktObject { public let movies: Int public let shows: Int public let seasons: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/AddToCollectionResult.swift b/Common/Models/Sync/AddToCollectionResult.swift index 88eb773..a94b378 100644 --- a/Common/Models/Sync/AddToCollectionResult.swift +++ b/Common/Models/Sync/AddToCollectionResult.swift @@ -8,18 +8,18 @@ import Foundation -public struct AddToCollectionResult: Codable, Hashable { +public struct AddToCollectionResult: TraktObject { public let added: Added public let updated: Added public let existing: Added public let notFound: NotFound - public struct Added: Codable, Hashable { + public struct Added: TraktObject { public let movies: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/AddToHistoryResult.swift b/Common/Models/Sync/AddToHistoryResult.swift index c7cd62d..95013e4 100644 --- a/Common/Models/Sync/AddToHistoryResult.swift +++ b/Common/Models/Sync/AddToHistoryResult.swift @@ -8,7 +8,7 @@ import Foundation -public struct NotFoundIds: Codable, Hashable { +public struct NotFoundIds: TraktObject { public let trakt: Int? public let slug: String? public let tvdb: Int? @@ -52,16 +52,16 @@ public struct NotFoundIds: Codable, Hashable { } } -public struct AddToHistoryResult: Codable, Hashable { +public struct AddToHistoryResult: TraktObject { public let added: Added public let notFound: NotFound - public struct Added: Codable, Hashable { + public struct Added: TraktObject { public let movies: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] @@ -74,16 +74,16 @@ public struct AddToHistoryResult: Codable, Hashable { } } -public struct RemoveFromHistoryResult: Codable, Hashable { +public struct RemoveFromHistoryResult: TraktObject { public let deleted: Deleted public let notFound: NotFound - public struct Deleted: Codable, Hashable { + public struct Deleted: TraktObject { public let movies: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/PlaybackProgress.swift b/Common/Models/Sync/PlaybackProgress.swift index b0de8fe..2a25adf 100644 --- a/Common/Models/Sync/PlaybackProgress.swift +++ b/Common/Models/Sync/PlaybackProgress.swift @@ -8,7 +8,7 @@ import Foundation -public struct PlaybackProgress: Codable, Hashable { +public struct PlaybackProgress: TraktObject { public let progress: Float public let pausedAt: Date public let id: Int diff --git a/Common/Models/Sync/RemoveFromCollectionResult.swift b/Common/Models/Sync/RemoveFromCollectionResult.swift index 94227f9..acf7caf 100644 --- a/Common/Models/Sync/RemoveFromCollectionResult.swift +++ b/Common/Models/Sync/RemoveFromCollectionResult.swift @@ -8,16 +8,16 @@ import Foundation -public struct RemoveFromCollectionResult: Codable, Hashable { +public struct RemoveFromCollectionResult: TraktObject { public let deleted: deleted public let notFound: NotFound - public struct deleted: Codable, Hashable { + public struct deleted: TraktObject { public let movies: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/RemoveFromWatchlistResult.swift b/Common/Models/Sync/RemoveFromWatchlistResult.swift index 53b27d4..44bea76 100644 --- a/Common/Models/Sync/RemoveFromWatchlistResult.swift +++ b/Common/Models/Sync/RemoveFromWatchlistResult.swift @@ -8,18 +8,18 @@ import Foundation -public struct RemoveFromWatchlistResult: Codable, Hashable { +public struct RemoveFromWatchlistResult: TraktObject { public let deleted: Deleted public let notFound: NotFound - public struct Deleted: Codable, Hashable { + public struct Deleted: TraktObject { public let movies: Int public let shows: Int public let seasons: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/RemoveRatingsResult.swift b/Common/Models/Sync/RemoveRatingsResult.swift index e127728..4ad0e80 100644 --- a/Common/Models/Sync/RemoveRatingsResult.swift +++ b/Common/Models/Sync/RemoveRatingsResult.swift @@ -8,18 +8,18 @@ import Foundation -public struct RemoveRatingsResult: Codable, Hashable { +public struct RemoveRatingsResult: TraktObject { public let deleted: Deleted public let notFound: NotFound - public struct Deleted: Codable, Hashable { + public struct Deleted: TraktObject { public let movies: Int public let shows: Int public let seasons: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Sync/TraktCollectedItem.swift b/Common/Models/Sync/TraktCollectedItem.swift index 8772954..e9b2c92 100644 --- a/Common/Models/Sync/TraktCollectedItem.swift +++ b/Common/Models/Sync/TraktCollectedItem.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktCollectedItem: Codable, Hashable { +public struct TraktCollectedItem: TraktObject { public var lastCollectedAt: Date public let lastUpdatedAt: Date @@ -64,7 +64,7 @@ public struct TraktCollectedItem: Codable, Hashable { } } - public struct Metadata: Codable, Hashable { + public struct Metadata: TraktObject { public let mediaType: MediaType? public let resolution: Resolution? public let hdr: HDR? @@ -89,11 +89,11 @@ public struct TraktCollectedItem: Codable, Hashable { hdr = try? container.decodeIfPresent(HDR.self, forKey: .hdr) audio = try? container.decodeIfPresent(Audio.self, forKey: .audio) audioChannels = try? container.decodeIfPresent(AudioChannels.self, forKey: .audioChannels) - is3D = try container.decodeIfPresent(Bool.self, forKey: .is3D) ?? false + is3D = (try? container.decodeIfPresent(Bool.self, forKey: .is3D)) ?? false } } - public enum MediaType: String, Codable { + public enum MediaType: String, TraktObject { case digital case bluray case hdDVD = "hddvd" @@ -104,7 +104,7 @@ public struct TraktCollectedItem: Codable, Hashable { case videoCD = "vcd" } - public enum Resolution: String, Codable { + public enum Resolution: String, TraktObject { case udh4k = "uhd_4k" case hd1080p = "hd_1080p" case hd1080i = "hd_1080i" @@ -115,14 +115,14 @@ public struct TraktCollectedItem: Codable, Hashable { case sd576i = "sd_576i" } - public enum HDR: String, Codable { + public enum HDR: String, TraktObject { case dolbyVision = "dolby_vision" case hdr10 = "hdr10" case hdr10Plus = "hdr10_plus" case hlg } - public enum Audio: String, Codable { + public enum Audio: String, TraktObject { case dolbyDigital = "dolby_digital" case dolbyDigitalPlus = "dolby_digital_plus" case dolbyDigitalPlusAtmos = "dolby_digital_plus_atmos" @@ -144,7 +144,7 @@ public struct TraktCollectedItem: Codable, Hashable { case flac } - public enum AudioChannels: String, Codable { + public enum AudioChannels: String, TraktObject { case tenOne = "10.1" case nineOne = "9.1" case sevenOneFour = "7.1.4" @@ -165,14 +165,14 @@ public struct TraktCollectedItem: Codable, Hashable { } } -public struct TraktCollectedSeason: Codable, Hashable { +public struct TraktCollectedSeason: TraktObject { /// Season number public var number: Int public var episodes: [TraktCollectedEpisode] } -public struct TraktCollectedEpisode: Codable, Hashable { +public struct TraktCollectedEpisode: TraktObject { public var number: Int public var collectedAt: Date diff --git a/Common/Models/Sync/TraktHistoryItem.swift b/Common/Models/Sync/TraktHistoryItem.swift index d6b9276..a7a2958 100644 --- a/Common/Models/Sync/TraktHistoryItem.swift +++ b/Common/Models/Sync/TraktHistoryItem.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktHistoryItem: Codable, Hashable { +public struct TraktHistoryItem: TraktObject { public var id: Int public var watchedAt: Date diff --git a/Common/Models/Sync/TraktRating.swift b/Common/Models/Sync/TraktRating.swift index 0fe3674..210e6e5 100644 --- a/Common/Models/Sync/TraktRating.swift +++ b/Common/Models/Sync/TraktRating.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktRating: Codable, Hashable { +public struct TraktRating: TraktObject { public var ratedAt: Date public var rating: Int diff --git a/Common/Models/Sync/WatchlistUpdate.swift b/Common/Models/Sync/WatchlistUpdate.swift new file mode 100644 index 0000000..1933562 --- /dev/null +++ b/Common/Models/Sync/WatchlistUpdate.swift @@ -0,0 +1,20 @@ +// +// WatchlistUpdate.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/2/25. +// + +extension TraktManager { + struct WatchlistUpdate: TraktObject { + let description: String? + let sortBy: String? + let sortHow: String? + + enum CodingKeys: String, CodingKey { + case description + case sortBy = "sort_by" + case sortHow = "sort_how" + } + } +} diff --git a/Common/Models/TraktImages.swift b/Common/Models/TraktImages.swift new file mode 100644 index 0000000..0649dde --- /dev/null +++ b/Common/Models/TraktImages.swift @@ -0,0 +1,36 @@ +// +// TraktImages.swift +// TraktKit +// +// Created by Maximilian Litteral on 9/16/25. +// + +import Foundation + +public struct TraktImages: TraktObject { + // Movies / Series / Season + public let fanart: [URL] + public let poster: [URL] + public let logo: [URL] + public let clearart: [URL] + public let banner: [URL] + public let thumb: [URL] + + // Episodes + public let screenshot: [URL] + + // Cast / Crew + public let headshot: [URL] + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.fanart = (try? container.decodeIfPresent([URL].self, forKey: .fanart)) ?? [] + self.poster = (try? container.decodeIfPresent([URL].self, forKey: .poster)) ?? [] + self.logo = (try? container.decodeIfPresent([URL].self, forKey: .logo)) ?? [] + self.clearart = (try? container.decodeIfPresent([URL].self, forKey: .clearart)) ?? [] + self.banner = (try? container.decodeIfPresent([URL].self, forKey: .banner)) ?? [] + self.thumb = (try? container.decodeIfPresent([URL].self, forKey: .thumb)) ?? [] + self.screenshot = (try? container.decodeIfPresent([URL].self, forKey: .screenshot)) ?? [] + self.headshot = (try? container.decodeIfPresent([URL].self, forKey: .headshot)) ?? [] + } +} diff --git a/Common/Models/TraktSearchResult.swift b/Common/Models/TraktSearchResult.swift index ef88ce6..5fc230d 100644 --- a/Common/Models/TraktSearchResult.swift +++ b/Common/Models/TraktSearchResult.swift @@ -8,7 +8,7 @@ import Foundation -public struct TraktSearchResult: Codable, Hashable { +public struct TraktSearchResult: TraktObject { public let type: String // Can be movie, show, episode, person, list public let score: Double? diff --git a/Common/Models/TraktStudio.swift b/Common/Models/TraktStudio.swift new file mode 100644 index 0000000..a54e4d5 --- /dev/null +++ b/Common/Models/TraktStudio.swift @@ -0,0 +1,20 @@ +// +// TraktStudio.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/22/25. +// + +import Foundation + +public struct TraktStudio: TraktObject { + public let name: String + public let country: String + public let ids: IDS + + public struct IDS: TraktObject { + public let trakt: Int + public let slug: String + public let tmdb: Int + } +} diff --git a/Common/Models/TraktVideo.swift b/Common/Models/TraktVideo.swift new file mode 100644 index 0000000..208f156 --- /dev/null +++ b/Common/Models/TraktVideo.swift @@ -0,0 +1,32 @@ +// +// TraktVideo.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/9/25. +// + +import Foundation + +public struct TraktVideo: TraktObject { + public let title: String + public let url: String + public let site: String + public let type: String + public let size: Int + public let official: Bool + public let publishedAt: Date + public let country: String + public let language: String + + enum CodingKeys: String, CodingKey { + case title + case url + case site + case type + case size + case official + case publishedAt = "published_at" + case country + case language + } +} diff --git a/Common/Models/Users/AccountSettings.swift b/Common/Models/Users/AccountSettings.swift index 59428ea..e8d578a 100644 --- a/Common/Models/Users/AccountSettings.swift +++ b/Common/Models/Users/AccountSettings.swift @@ -8,16 +8,101 @@ import Foundation -public struct AccountSettings: Codable, Hashable { +public struct AccountSettings: TraktObject { public let user: User public let connections: Connections - - public struct Connections: Codable, Hashable { + public let sharingText: SharingText + public let limits: Limits + + enum CodingKeys: String, CodingKey { + case user + case connections + case sharingText = "sharing_text" + case limits + } + + public struct Connections: TraktObject { public let facebook: Bool public let twitter: Bool + public let mastodon: Bool public let google: Bool public let tumblr: Bool public let medium: Bool public let slack: Bool + public let apple: Bool + public let dropbox: Bool + public let microsoft: Bool + } + + public struct SharingText: TraktObject { + public let watching: String? + public let watched: String? + public let rated: String? + } + + public struct Limits: TraktObject { + public let list: List + public let watchlist: Watchlist + public let favorites: Favorites + public let search: Search + public let collection: Collection + public let notes: Notes + + public struct List: TraktObject { + /// Total lists + public let count: Int + /// Item per list + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case count + case itemCount = "item_count" + } + } + + public struct Watchlist: TraktObject { + /// Number of items that can be added to the watchlist + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case itemCount = "item_count" + } + } + + public struct Favorites: TraktObject { + /// Number of items that can be favorited. + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case itemCount = "item_count" + } + } + + public struct Search: TraktObject { + /// Number of saved recent searches + public let recentCount: Int + + enum CodingKeys: String, CodingKey { + case recentCount = "recent_count" + } + } + + public struct Collection: TraktObject { + /// Number of items that can be collected. + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case itemCount = "item_count" + } + } + + public struct Notes: TraktObject { + /// Number of items that can have a personal note. + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case itemCount = "item_count" + } + } } } diff --git a/Common/Models/Users/FollowRequest.swift b/Common/Models/Users/FollowRequest.swift index 5f414db..665b5a3 100644 --- a/Common/Models/Users/FollowRequest.swift +++ b/Common/Models/Users/FollowRequest.swift @@ -8,7 +8,7 @@ import Foundation -public struct FollowRequest: Codable, Hashable { +public struct FollowRequest: TraktObject { public let id: Int public let requestedAt: Date public let user: User @@ -20,7 +20,7 @@ public struct FollowRequest: Codable, Hashable { } } -public struct FollowResult: Codable, Hashable { +public struct FollowResult: TraktObject { public let followedAt: Date public let user: User diff --git a/Common/Models/Users/FollowUserResult.swift b/Common/Models/Users/FollowUserResult.swift index bb662ad..6598c3b 100644 --- a/Common/Models/Users/FollowUserResult.swift +++ b/Common/Models/Users/FollowUserResult.swift @@ -8,7 +8,7 @@ import Foundation -public struct FollowUserResult: Codable, Hashable { +public struct FollowUserResult: TraktObject { public let approvedAt: Date public let user: User diff --git a/Common/Models/Users/Friend.swift b/Common/Models/Users/Friend.swift index b80ce24..34add95 100644 --- a/Common/Models/Users/Friend.swift +++ b/Common/Models/Users/Friend.swift @@ -8,7 +8,7 @@ import Foundation -public struct Friend: Codable, Hashable { +public struct Friend: TraktObject { public let friendsAt: Date public let user: User diff --git a/Common/Models/Users/HiddenItem.swift b/Common/Models/Users/HiddenItem.swift index 0d2f47c..d19ba38 100644 --- a/Common/Models/Users/HiddenItem.swift +++ b/Common/Models/Users/HiddenItem.swift @@ -8,19 +8,22 @@ import Foundation -public struct HiddenItem: Codable, Hashable { +public struct HiddenItem: TraktObject { public let hiddenAt: Date public let type: String public let movie: TraktMovie? public let show: TraktShow? public let season: TraktSeason? - + public let user: User? + enum CodingKeys: String, CodingKey { case hiddenAt = "hidden_at" case type + case movie case show case season + case user } } diff --git a/Common/Models/Users/HideItemResult.swift b/Common/Models/Users/HideItemResult.swift index 7906eb9..cc9a0ee 100644 --- a/Common/Models/Users/HideItemResult.swift +++ b/Common/Models/Users/HideItemResult.swift @@ -8,20 +8,22 @@ import Foundation -public struct HideItemResult: Codable, Hashable { +public struct HideItemResult: TraktObject { public let added: Added public let notFound: NotFound - public struct Added: Codable, Hashable { + public struct Added: TraktObject { public let movies: Int public let shows: Int public let seasons: Int + public let users: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] + public let users: [NotFoundIds] } enum CodingKeys: String, CodingKey { diff --git a/Common/Models/Users/Likes.swift b/Common/Models/Users/Likes.swift index 8a4b3e5..0411def 100644 --- a/Common/Models/Users/Likes.swift +++ b/Common/Models/Users/Likes.swift @@ -8,13 +8,13 @@ import Foundation -public struct Like: Codable, Hashable { +public struct Like: TraktObject { public let likedAt: Date public let type: LikeType public let list: TraktList? public let comment: Comment? - public enum LikeType: String, Codable { + public enum LikeType: String, TraktObject { case comment case list } diff --git a/Common/Models/Users/ListItemPostResult.swift b/Common/Models/Users/ListItemPostResult.swift index 4fb46c3..2ed0e6d 100644 --- a/Common/Models/Users/ListItemPostResult.swift +++ b/Common/Models/Users/ListItemPostResult.swift @@ -8,12 +8,12 @@ import Foundation -public struct ListItemPostResult: Codable, Hashable { +public struct ListItemPostResult: TraktObject { public let added: ObjectCount public let existing: ObjectCount public let notFound: NotFound - public struct ObjectCount: Codable, Hashable { + public struct ObjectCount: TraktObject { public let movies: Int public let shows: Int public let seasons: Int @@ -21,7 +21,7 @@ public struct ListItemPostResult: Codable, Hashable { public let people: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] @@ -36,29 +36,41 @@ public struct ListItemPostResult: Codable, Hashable { } } -public struct WatchlistItemPostResult: Codable, Hashable { +public struct WatchlistItemPostResult: TraktObject { public let added: ObjectCount public let existing: ObjectCount public let notFound: NotFound - - public struct ObjectCount: Codable, Hashable { + public let list: List + + public struct ObjectCount: TraktObject { public let movies: Int public let shows: Int public let seasons: Int public let episodes: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] public let episodes: [NotFoundIds] } - + + public struct List: TraktObject { + public let updatedAt: Date + public let itemCount: Int + + enum CodingKeys: String, CodingKey { + case updatedAt = "updated_at" + case itemCount = "item_count" + } + } + enum CodingKeys: String, CodingKey { case added case existing case notFound = "not_found" + case list } } diff --git a/Common/Models/Users/RemoveListItemResult.swift b/Common/Models/Users/RemoveListItemResult.swift index 822cc36..4dcf3e4 100644 --- a/Common/Models/Users/RemoveListItemResult.swift +++ b/Common/Models/Users/RemoveListItemResult.swift @@ -8,11 +8,11 @@ import Foundation -public struct RemoveListItemResult: Codable, Hashable { +public struct RemoveListItemResult: TraktObject { public let deleted: Deleted public let notFound: NotFound - public struct Deleted: Codable, Hashable { + public struct Deleted: TraktObject { public let movies: Int public let shows: Int public let seasons: Int @@ -20,7 +20,7 @@ public struct RemoveListItemResult: Codable, Hashable { public let people: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] diff --git a/Common/Models/Users/SavedFilter.swift b/Common/Models/Users/SavedFilter.swift new file mode 100644 index 0000000..339c172 --- /dev/null +++ b/Common/Models/Users/SavedFilter.swift @@ -0,0 +1,28 @@ +// +// SavedFilter.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/18/25. +// + +import Foundation + +public struct SavedFilter: TraktObject { + public let rank: Int + public let id: Int + public let section: String + public let name: String + public let path: String + public let query: String + public let updatedAt: Date + + enum CodingKeys: String, CodingKey { + case rank + case id + case section + case name + case path + case query + case updatedAt = "updated_at" + } +} diff --git a/Common/Models/Users/TraktList.swift b/Common/Models/Users/TraktList.swift index ef5876f..94e0af4 100644 --- a/Common/Models/Users/TraktList.swift +++ b/Common/Models/Users/TraktList.swift @@ -8,13 +8,94 @@ import Foundation -public enum ListPrivacy: String, Codable { +public enum ListPrivacy: String, TraktObject { + /// Only you can see the list. case `private` + /// Anyone with the `share_link` can see the list. + case link + /// Only your friends can see the list. case friends + /// Anyone can see the list. case `public` } -public struct TraktList: Codable, Hashable { +public struct TraktNewList: TraktObject { + public let name: String + public let description: String? + public let privacy: ListPrivacy? + public let displayNumbers: Bool? + public let allowComments: Bool? + public let sortBy: String? + public let sortHow: String? + + public init( + name: String, + description: String? = nil, + privacy: ListPrivacy? = nil, + displayNumbers: Bool? = nil, + allowComments: Bool? = nil, + sortBy: String? = nil, + sortHow: String? = nil + ) { + self.name = name + self.description = description + self.privacy = privacy + self.displayNumbers = displayNumbers + self.allowComments = allowComments + self.sortBy = sortBy + self.sortHow = sortHow + } + + enum CodingKeys: String, CodingKey { + case name + case description + case privacy + case displayNumbers = "display_numbers" + case allowComments = "allow_comments" + case sortBy = "sort_by" + case sortHow = "sort_how" + } +} + +public struct TraktUpdateList: TraktObject { + public let name: String? + public let description: String? + public let privacy: ListPrivacy? + public let displayNumbers: Bool? + public let allowComments: Bool? + public let sortBy: String? + public let sortHow: String? + + public init( + name: String? = nil, + description: String? = nil, + privacy: ListPrivacy? = nil, + displayNumbers: Bool? = nil, + allowComments: Bool? = nil, + sortBy: String? = nil, + sortHow: String? = nil + ) { + self.name = name + self.description = description + self.privacy = privacy + self.displayNumbers = displayNumbers + self.allowComments = allowComments + self.sortBy = sortBy + self.sortHow = sortHow + } + + enum CodingKeys: String, CodingKey { + case name + case description + case privacy + case displayNumbers = "display_numbers" + case allowComments = "allow_comments" + case sortBy = "sort_by" + case sortHow = "sort_how" + } +} + +public struct TraktList: TraktObject { public let allowComments: Bool public let commentCount: Int public let createdAt: Date? @@ -42,7 +123,7 @@ public struct TraktList: Codable, Hashable { } } -public struct TraktTrendingList: Codable, Hashable { +public struct TraktTrendingList: TraktObject { public let likeCount: Int public let commentCount: Int public let list: TraktList @@ -53,3 +134,13 @@ public struct TraktTrendingList: Codable, Hashable { case list } } + +public struct TraktReorderListsResponse: TraktObject { + public let updated: Int + public let skipped: [Int] + + enum CodingKeys: String, CodingKey { + case updated + case skipped = "skipped_ids" + } +} diff --git a/Common/Models/Users/TraktListItem.swift b/Common/Models/Users/TraktListItem.swift index 1c3f7c6..fc06299 100644 --- a/Common/Models/Users/TraktListItem.swift +++ b/Common/Models/Users/TraktListItem.swift @@ -8,9 +8,11 @@ import Foundation -public struct TraktListItem: Codable, Hashable { +public struct TraktListItem: TraktObject { public let rank: Int + public let id: Int public let listedAt: Date + public let notes: String? public let type: String public var show: TraktShow? = nil public var season: TraktSeason? = nil @@ -20,7 +22,9 @@ public struct TraktListItem: Codable, Hashable { enum CodingKeys: String, CodingKey { case rank + case id case listedAt = "listed_at" + case notes case type case show case season diff --git a/Common/Models/Users/TraktUser.swift b/Common/Models/Users/TraktUser.swift index 0567e5c..555f5fe 100644 --- a/Common/Models/Users/TraktUser.swift +++ b/Common/Models/Users/TraktUser.swift @@ -8,7 +8,7 @@ import Foundation -public struct User: Codable, Hashable { +public struct User: TraktObject { // Min public let username: String? @@ -47,15 +47,15 @@ public struct User: Codable, Hashable { case vipYears = "vip_years" } - public struct IDs: Codable, Hashable { + public struct IDs: TraktObject { public let slug: String } - public struct Images: Codable, Hashable { + public struct Images: TraktObject { public let avatar: Image } - public struct Image: Codable, Hashable { + public struct Image: TraktObject { public let full: String } } diff --git a/Common/Models/Users/TraktWatchedItem.swift b/Common/Models/Users/TraktWatchedItem.swift index 3824f69..444a877 100644 --- a/Common/Models/Users/TraktWatchedItem.swift +++ b/Common/Models/Users/TraktWatchedItem.swift @@ -8,9 +8,10 @@ import Foundation -public struct TraktWatchedItem: Codable, Hashable { +public struct TraktWatchedItem: TraktObject { public let plays: Int public let lastWatchedAt: Date + public let lastUpdatedAt: Date public var show: TraktShow? = nil public var seasons: [TraktWatchedSeason]? = nil public var movie: TraktMovie? = nil @@ -18,6 +19,7 @@ public struct TraktWatchedItem: Codable, Hashable { enum CodingKeys: String, CodingKey { case plays case lastWatchedAt = "last_watched_at" + case lastUpdatedAt = "last_updated_at" case show case seasons case movie diff --git a/Common/Models/Users/TraktWatching.swift b/Common/Models/Users/TraktWatching.swift index 3f9e1b6..9d3eb07 100644 --- a/Common/Models/Users/TraktWatching.swift +++ b/Common/Models/Users/TraktWatching.swift @@ -8,24 +8,137 @@ import Foundation -public struct TraktWatching: Codable, Hashable { - public let expiresAt: Date - public let startedAt: Date - public let action: String - public let type: String - - public let episode: TraktEpisode? - public let show: TraktShow? - public let movie: TraktMovie? - - enum CodingKeys: String, CodingKey { +public enum TraktWatching: TraktObject { + case watching(WatchingItem) + case notWatchingAnything + + public struct WatchingItem: TraktObject { + public let expiresAt: Date + public let startedAt: Date + public let action: String + public let mediaType: MediaType + public let mediaItem: MediaItem + + public enum MediaType: String, TraktObject { + case episode + case movie + } + + public enum MediaItem: TraktObject { + case episode(TraktEpisode, show: TraktShow) + case movie(TraktMovie) + } + } +} + +extension TraktWatching { + public init(from decoder: Decoder) throws { + // First, try to decode as a watching item + do { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let expiresAt = try container.decode(Date.self, forKey: .expiresAt) + let startedAt = try container.decode(Date.self, forKey: .startedAt) + let action = try container.decode(String.self, forKey: .action) + let typeString = try container.decode(String.self, forKey: .mediaType) + + guard let type = WatchingItem.MediaType(rawValue: typeString) else { + throw DecodingError.dataCorruptedError( + forKey: .mediaType, + in: container, + debugDescription: "Invalid media type: \(typeString)" + ) + } + + let item: WatchingItem.MediaItem + + switch type { + case .episode: + let episode = try container.decode(TraktEpisode.self, forKey: .episode) + let show = try container.decode(TraktShow.self, forKey: .show) + item = .episode(episode, show: show) + + case .movie: + let movie = try container.decode(TraktMovie.self, forKey: .movie) + item = .movie(movie) + } + + let watchingItem = WatchingItem( + expiresAt: expiresAt, + startedAt: startedAt, + action: action, + mediaType: type, + mediaItem: item + ) + + self = .watching(watchingItem) + } catch { + // If decode failed, check if this is due to a 204 response + // This will be handled at the network layer, so here we'll + // just create a placeholder for that case + self = .notWatchingAnything + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .watching(let watchingItem): + // Encode common properties + try container.encode(watchingItem.expiresAt, forKey: .expiresAt) + try container.encode(watchingItem.startedAt, forKey: .startedAt) + try container.encode(watchingItem.action, forKey: .action) + try container.encode(watchingItem.mediaType.rawValue, forKey: .mediaType) + + // Encode media-specific properties + switch watchingItem.mediaItem { + case .episode(let episode, let show): + try container.encode(episode, forKey: .episode) + try container.encode(show, forKey: .show) + + case .movie(let movie): + try container.encode(movie, forKey: .movie) + } + + case .notWatchingAnything: + // For not watching, we'll encode a special marker + // This isn't in the Trakt API but helps us identify this state when decoding our stored data + try container.encodeNil(forKey: .expiresAt) + try container.encodeNil(forKey: .startedAt) + try container.encodeNil(forKey: .action) + try container.encodeNil(forKey: .mediaType) + } + } + + private enum CodingKeys: String, CodingKey { case expiresAt = "expires_at" case startedAt = "started_at" case action - case type - + case mediaType = "type" case episode case show case movie } } + +// Extension to add convenience accessors +extension TraktWatching { + public var isWatching: Bool { + switch self { + case .watching: + return true + case .notWatchingAnything: + return false + } + } + + public var mediaType: WatchingItem.MediaType? { + switch self { + case .watching(let item): + return item.mediaType + case .notWatchingAnything: + return nil + } + } +} diff --git a/Common/Models/Users/UnhideItemResult.swift b/Common/Models/Users/UnhideItemResult.swift index 2398db8..5ad8965 100644 --- a/Common/Models/Users/UnhideItemResult.swift +++ b/Common/Models/Users/UnhideItemResult.swift @@ -8,20 +8,22 @@ import Foundation -public struct UnhideItemResult: Codable, Hashable { +public struct UnhideItemResult: TraktObject { public let deleted: Deleted public let notFound: NotFound - public struct Deleted: Codable, Hashable { + public struct Deleted: TraktObject { public let movies: Int public let shows: Int public let seasons: Int + public let users: Int } - public struct NotFound: Codable, Hashable { + public struct NotFound: TraktObject { public let movies: [NotFoundIds] public let shows: [NotFoundIds] public let seasons: [NotFoundIds] + public let users: [NotFoundIds] } enum CodingKeys: String, CodingKey { diff --git a/Common/Models/Users/UserStats.swift b/Common/Models/Users/UserStats.swift index f363a07..0c03081 100644 --- a/Common/Models/Users/UserStats.swift +++ b/Common/Models/Users/UserStats.swift @@ -8,7 +8,7 @@ import Foundation -public struct UserStats: Codable, Hashable { +public struct UserStats: TraktObject { public let movies: Movies public let shows: Shows public let seasons: Seasons @@ -16,7 +16,7 @@ public struct UserStats: Codable, Hashable { public let network: Network public let ratings: UserStatsRatingsDistribution - public struct Movies: Codable, Hashable { + public struct Movies: TraktObject { public let plays: Int public let watched: Int public let minutes: Int @@ -25,19 +25,19 @@ public struct UserStats: Codable, Hashable { public let comments: Int } - public struct Shows: Codable, Hashable { + public struct Shows: TraktObject { public let watched: Int public let collected: Int public let ratings: Int public let comments: Int } - public struct Seasons: Codable, Hashable { + public struct Seasons: TraktObject { public let ratings: Int public let comments: Int } - public struct Episodes: Codable, Hashable { + public struct Episodes: TraktObject { public let plays: Int public let watched: Int public let minutes: Int @@ -46,17 +46,17 @@ public struct UserStats: Codable, Hashable { public let comments: Int } - public struct Network: Codable, Hashable { + public struct Network: TraktObject { public let friends: Int public let followers: Int public let following: Int } - public struct UserStatsRatingsDistribution: Codable, Hashable { + public struct UserStatsRatingsDistribution: TraktObject { public let total: Int public let distribution: Distribution - public struct Distribution: Codable, Hashable { + public struct Distribution: TraktObject { public let one: Int public let two: Int public let three: Int diff --git a/Common/Models/Users/UsersComments.swift b/Common/Models/Users/UsersComments.swift index 9aba047..281c91d 100644 --- a/Common/Models/Users/UsersComments.swift +++ b/Common/Models/Users/UsersComments.swift @@ -8,7 +8,7 @@ import Foundation -public struct UsersComments: Codable, Hashable { +public struct UsersComments: TraktObject { public let type: String public let comment: Comment public let movie: TraktMovie? diff --git a/Common/Wrapper/AuthStorage.swift b/Common/Wrapper/AuthStorage.swift new file mode 100644 index 0000000..7046c29 --- /dev/null +++ b/Common/Wrapper/AuthStorage.swift @@ -0,0 +1,144 @@ +// +// AuthStorage.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/6/25. +// +import Foundation + +public struct AuthenticationState: Sendable { + public let accessToken: String + public let refreshToken: String + public let expirationDate: Date + + public init(accessToken: String, refreshToken: String, expirationDate: Date) { + self.accessToken = accessToken + self.refreshToken = refreshToken + self.expirationDate = expirationDate + } +} + +public enum AuthenticationError: Error, Equatable { + /// Token was found, but is past the expiration date. + case tokenExpired(refreshToken: String) + /// Thrown if credentials could not be retreived. + case noStoredCredentials +} + +public protocol TraktAuthentication: Sendable { + /// Returns the current access token, refresh token, and expiration date. + func getCurrentState() async throws(AuthenticationError) -> AuthenticationState + /// Store the latest state + func updateState(_ state: AuthenticationState) async + /// Delete the data + func clear() async +} + +public actor KeychainTraktAuthentication: TraktAuthentication { + private enum Constants { + static let tokenExpirationDefaultsKey = "accessTokenExpirationDate" + static let accessTokenKey = "accessToken" + static let refreshTokenKey = "refreshToken" + } + + private var accessToken: String? + private var refreshToken: String? + private var expirationDate: Date? + + public init() { + + } + + public func load() throws(AuthenticationError) -> AuthenticationState { + guard + let accessTokenData = MLKeychain.loadData(forKey: Constants.accessTokenKey), + let accessTokenString = String(data: accessTokenData, encoding: .utf8), + let refreshTokenData = MLKeychain.loadData(forKey: Constants.refreshTokenKey), + let refreshTokenString = String(data: refreshTokenData, encoding: .utf8) + else { throw .noStoredCredentials } + + accessToken = accessTokenString + refreshToken = refreshTokenString + + // Refresh auth if expiration is not found. + guard + let expiration = UserDefaults.standard.object(forKey: Constants.tokenExpirationDefaultsKey) as? Date + else { throw .tokenExpired(refreshToken: refreshTokenString) } + + expirationDate = expiration + + return AuthenticationState(accessToken: accessTokenString, refreshToken: refreshTokenString, expirationDate: expiration) + } + + public func getCurrentState() throws(AuthenticationError) -> AuthenticationState { + guard + let accessToken, + let refreshToken, + let expirationDate + else { return try load() } + + guard expirationDate > .now else { throw .tokenExpired(refreshToken: refreshToken) } + + return AuthenticationState(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate) + } + + public func updateState(_ state: AuthenticationState) { + // Keep in memory + accessToken = state.accessToken + refreshToken = state.refreshToken + + // Save to keychain + MLKeychain.setString(value: state.accessToken, forKey: Constants.accessTokenKey) + MLKeychain.setString(value: state.refreshToken, forKey: Constants.refreshTokenKey) + + UserDefaults.standard.set(state.expirationDate, forKey: Constants.tokenExpirationDefaultsKey) + } + + public func clear() { + accessToken = nil + refreshToken = nil + expirationDate = nil + + MLKeychain.deleteItem(forKey: Constants.accessTokenKey) + MLKeychain.deleteItem(forKey: Constants.refreshTokenKey) + + UserDefaults.standard.removeObject(forKey: Constants.tokenExpirationDefaultsKey) + } +} + +public actor TraktMockAuthStorage: TraktAuthentication { + + var accessToken: String? + var refreshToken: String? + var expirationDate: Date? + + public init(accessToken: String? = nil, refreshToken: String? = nil, expirationDate: Date? = nil) { + self.accessToken = accessToken + self.refreshToken = refreshToken + self.expirationDate = expirationDate + } + + public func getCurrentState() async throws(AuthenticationError) -> AuthenticationState { + guard + let accessToken, + let refreshToken, + let expirationDate + else { throw .noStoredCredentials } + + guard expirationDate > .now else { throw .tokenExpired(refreshToken: refreshToken) } + + return AuthenticationState(accessToken: accessToken, refreshToken: refreshToken, expirationDate: expirationDate) + } + + public func updateState(_ state: AuthenticationState) async { + accessToken = state.accessToken + refreshToken = state.refreshToken + expirationDate = state.expirationDate + } + + public func clear() async { + accessToken = nil + refreshToken = nil + expirationDate = nil + } +} diff --git a/Common/Wrapper/Comments.swift b/Common/Wrapper/Comments.swift deleted file mode 100644 index b40de44..0000000 --- a/Common/Wrapper/Comments.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// Comments.swift -// TraktKit -// -// Created by Maximilian Litteral on 11/15/15. -// Copyright © 2015 Maximilian Litteral. All rights reserved. -// - -import Foundation - -extension TraktManager { - - // MARK: - Comments - - /** - Add a new comment to a movie, show, season, episode, or list. Make sure to allow and encourage spoilers to be indicated in your app and follow the rules listed above. - - 🔒 OAuth: Required - */ - @discardableResult - public func postComment(movie: SyncId? = nil, show: SyncId? = nil, season: SyncId? = nil, episode: SyncId? = nil, list: SyncId? = nil, comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping SuccessCompletionHandler) throws -> URLSessionDataTaskProtocol? { - let body = TraktCommentBody(movie: movie, show: show, season: season, episode: episode, list: list, comment: comment, spoiler: spoiler) - guard let request = post("comments", body: body) else { return nil } - return performRequest(request: request, completion: completion) - } - - /** - Returns a single comment and indicates how many replies it has. Use **GET** `/comments/:id/replies` to get the actual replies. - */ - @discardableResult - public func getComment(commentID id: T, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)", - withQuery: [:], - isAuthorized: false, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } - - /** - Update a single comment created within the last hour. The OAuth user must match the author of the comment in order to update it. - - 🔒 OAuth: Required - */ - @discardableResult - public func updateComment(commentID id: T, newComment comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - let body = TraktCommentBody(comment: comment, spoiler: spoiler) - guard var request = mutableRequest(forPath: "comments/\(id)", - withQuery: [:], - isAuthorized: true, - withHTTPMethod: .PUT) else { return nil } - request.httpBody = try jsonEncoder.encode(body) - return performRequest(request: request, - completion: completion) - } - - /** - Delete a single comment created within the last hour. This also effectively removes any replies this comment has. The OAuth user must match the author of the comment in order to delete it. - - 🔒 OAuth: Required - */ - @discardableResult - public func deleteComment(commentID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard - let request = mutableRequest(forPath: "comments/\(id)", - withQuery: [:], - isAuthorized: true, - withHTTPMethod: .DELETE) else { return nil } - return performRequest(request: request, completion: completion) - } - - // MARK: - Replies - - /** - Returns all replies for a comment. It is possible these replies could have replies themselves, so in that case you would just call **GET** `/comments/:id/replies` again with the new comment `id`. - - 📄 Pagination - */ - @discardableResult - public func getReplies(commentID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)/replies", - withQuery: [:], - isAuthorized: false, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } - - /** - Add a new reply to an existing comment. Make sure to allow and encourage spoilers to be indicated in your app and follow the rules listed above. - - 🔒 OAuth: Required - */ - @discardableResult - public func postReply(commentID id: T, comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - let body = TraktCommentBody(comment: comment, spoiler: spoiler) - guard let request = post("comments/\(id)/replies", body: body) else { return nil } - return performRequest(request: request, completion: completion) - } - - // MARK: - Item - - /** - Returns all users who liked a comment. If you only need the `replies` count, the main `comment` object already has that, so no need to use this method. - - 📄 Pagination - */ - @discardableResult - public func getAttachedMediaItem(commentID id: T, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)/item", - withQuery: [:], - isAuthorized: true, - withHTTPMethod: .POST) else { return nil } - return performRequest(request: request, - completion: completion) - } - - // MARK: - Likes - - /** - Returns the media item this comment is attached to. The media type can be `movie`, `show`, `season`, `episode`, or `list` and it also returns the standard media object for that media type. - - ✨ Extended Info - */ - @discardableResult - public func getUsersWhoLikedComment(commentID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)/likes", - withQuery: [:], - isAuthorized: true, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } - - // MARK: - Like - - /** - Votes help determine popular comments. Only one like is allowed per comment per user. - - 🔒 OAuth: Required - */ - @discardableResult - public func likeComment(commentID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)/like", - withQuery: [:], - isAuthorized: false, - withHTTPMethod: .POST) else { return nil } - return performRequest(request: request, - completion: completion) - } - - /** - Remove a like on a comment. - - 🔒 OAuth: Required - */ - @discardableResult - public func removeLikeOnComment(commentID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/\(id)/like", - withQuery: [:], - isAuthorized: false, - withHTTPMethod: .DELETE) else { return nil } - return performRequest(request: request, - completion: completion) - } - - // MARK: - Trending - - /** - Returns all comments with the most likes and replies over the last 7 days. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. - - 📄 Pagination - ✨ Extended - */ - @discardableResult - public func getTrendingComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/trending/\(commentType.rawValue)/\(mediaType.rawValue)", - withQuery: ["include_replies": "\(includeReplies)"], - isAuthorized: false, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } - - // MARK: - Recent - - /** - Returns the most recently written comments across all of Trakt. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. - - 📄 Pagination - ✨ Extended - */ - @discardableResult - public func getRecentComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/recent/\(commentType.rawValue)/\(mediaType.rawValue)", - withQuery: ["include_replies": "\(includeReplies)"], - isAuthorized: false, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } - - // MARK: - Updates - - /** - Returns the most recently updated comments across all of Trakt. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. - - 📄 Pagination - ✨ Extended - */ - @discardableResult - public func getRecentlyUpdatedComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "comments/updates/\(commentType.rawValue)/\(mediaType.rawValue)", - withQuery: ["include_replies": "\(includeReplies)"], - isAuthorized: false, - withHTTPMethod: .GET) else { return nil } - return performRequest(request: request, - completion: completion) - } -} diff --git a/Common/Wrapper/Calendars.swift b/Common/Wrapper/CompletionHandlerEndpoints/Calendars.swift similarity index 77% rename from Common/Wrapper/Calendars.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Calendars.swift index b5d4f2f..b03e10d 100644 --- a/Common/Wrapper/Calendars.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Calendars.swift @@ -19,8 +19,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func myShows(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/my/shows/\(dateString)/\(days)", + public func myShows(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/my/shows/\(dateString)/\(days)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -37,8 +37,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func myNewShows(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/my/shows/new/\(dateString)/\(days)", + public func myNewShows(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/my/shows/new/\(dateString)/\(days)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -55,8 +55,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func mySeasonPremieres(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/my/shows/premieres/\(dateString)/\(days)", + public func mySeasonPremieres(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/my/shows/premieres/\(dateString)/\(days)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -73,8 +73,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func myMovies(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/my/movies/\(dateString)/\(days)", + public func myMovies(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarMovie]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/my/movies/\(dateString)/\(days)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -90,8 +90,8 @@ extension TraktManager { 🎚 Filters */ @discardableResult - public func myDVDReleases(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/my/dvd/\(dateString)/\(days)", + public func myDVDReleases(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarMovie]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/my/dvd/\(dateString)/\(days)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -106,8 +106,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func allShows(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/all/shows/\(dateString)/\(days)", + public func allShows(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/all/shows/\(dateString)/\(days)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -122,8 +122,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func allNewShows(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/all/shows/new/\(dateString)/\(days)", + public func allNewShows(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/all/shows/new/\(dateString)/\(days)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -138,8 +138,8 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func allSeasonPremieres(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/all/shows/premieres/\(dateString)/\(days)", + public func allSeasonPremieres(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarShow]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/all/shows/premieres/\(dateString)/\(days)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -154,7 +154,7 @@ extension TraktManager { - parameter days: Number of days to display. Example: `7`. */ @discardableResult - public func allMovies(startDateString dateString: String, days: Int, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func allMovies(startDateString dateString: String, days: Int, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping ObjectCompletionHandler<[CalendarMovie]>) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -165,7 +165,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "calendars/all/movies/\(dateString)/\(days)", + guard let request = try? mutableRequest(forPath: "calendars/all/movies/\(dateString)/\(days)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -176,8 +176,8 @@ extension TraktManager { /** */ @discardableResult - public func allDVD(startDateString dateString: String, days: Int, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "calendars/all/dvd/\(dateString)/\(days)", + public func allDVD(startDateString dateString: String, days: Int, completion: @escaping ObjectCompletionHandler<[CalendarMovie]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "calendars/all/dvd/\(dateString)/\(days)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Certifications.swift b/Common/Wrapper/CompletionHandlerEndpoints/Certifications.swift similarity index 85% rename from Common/Wrapper/Certifications.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Certifications.swift index cbde48a..7d94e69 100644 --- a/Common/Wrapper/Certifications.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Certifications.swift @@ -16,8 +16,8 @@ extension TraktManager { Note: Only `us` certifications are currently returned. */ @discardableResult - public func getCertifications(completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "certifications", + public func getCertifications(completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "certifications", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Checkin.swift b/Common/Wrapper/CompletionHandlerEndpoints/Checkin.swift similarity index 84% rename from Common/Wrapper/Checkin.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Checkin.swift index 2185827..a070e89 100644 --- a/Common/Wrapper/Checkin.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Checkin.swift @@ -15,8 +15,8 @@ extension TraktManager { **Note**: If a checkin is already in progress, a `409` HTTP status code will returned. The response will contain an `expires_at` timestamp which is when the user can check in again. */ @discardableResult - public func checkIn(_ body: TraktCheckinBody, completionHandler: @escaping checkinCompletionHandler) throws -> URLSessionDataTaskProtocol? { - guard let request = post("checkin", body: body) else { return nil } + public func checkIn(_ body: TraktCheckinBody, completionHandler: @escaping checkinCompletionHandler) -> URLSessionDataTask? { + guard let request = try? post("checkin", body: body) else { return nil } return performRequest(request: request, completion: completionHandler) } @@ -24,13 +24,13 @@ extension TraktManager { Removes any active checkins, no need to provide a specific item. */ @discardableResult - public func deleteActiveCheckins(completionHandler: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "checkin", + public func deleteActiveCheckins(completionHandler: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "checkin", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } - let dataTask = session._dataTask(with: request) { data, response, error in + let dataTask = session.dataTask(with: request) { data, response, error in guard error == nil else { completionHandler(.fail) return diff --git a/Common/Wrapper/CompletionHandlerEndpoints/Comments.swift b/Common/Wrapper/CompletionHandlerEndpoints/Comments.swift new file mode 100644 index 0000000..53a88f5 --- /dev/null +++ b/Common/Wrapper/CompletionHandlerEndpoints/Comments.swift @@ -0,0 +1,209 @@ +// +// Comments.swift +// TraktKit +// +// Created by Maximilian Litteral on 11/15/15. +// Copyright © 2015 Maximilian Litteral. All rights reserved. +// + +import Foundation + +extension TraktManager { + + // MARK: - Comments + + /** + Add a new comment to a movie, show, season, episode, or list. Make sure to allow and encourage spoilers to be indicated in your app and follow the rules listed above. + + 🔒 OAuth: Required + */ + @discardableResult + public func postComment(movie: SyncId? = nil, show: SyncId? = nil, season: SyncId? = nil, episode: SyncId? = nil, list: SyncId? = nil, comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping SuccessCompletionHandler) throws -> URLSessionDataTask? { + let body = TraktCommentBody(movie: movie, show: show, season: season, episode: episode, list: list, comment: comment, spoiler: spoiler) + let request = try post("comments", body: body) + return performRequest(request: request, completion: completion) + } + + /** + Returns a single comment and indicates how many replies it has. Use **GET** `/comments/:id/replies` to get the actual replies. + */ + @discardableResult + public func getComment(commentID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)", + withQuery: [:], + isAuthorized: false, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } + + /** + Update a single comment created within the last hour. The OAuth user must match the author of the comment in order to update it. + + 🔒 OAuth: Required + */ + @discardableResult + public func updateComment(commentID id: CustomStringConvertible, newComment comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + let body = TraktCommentBody(comment: comment, spoiler: spoiler) + var request = try mutableRequest(forPath: "comments/\(id)", + withQuery: [:], + isAuthorized: true, + withHTTPMethod: .PUT) + request.httpBody = try Self.jsonEncoder.encode(body) + return performRequest(request: request, completion: completion) + } + + /** + Delete a single comment created within the last hour. This also effectively removes any replies this comment has. The OAuth user must match the author of the comment in order to delete it. + + 🔒 OAuth: Required + */ + @discardableResult + public func deleteComment(commentID id: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)", + withQuery: [:], + isAuthorized: true, + withHTTPMethod: .DELETE) + return performRequest(request: request, completion: completion) + } + + // MARK: - Replies + + /** + Returns all replies for a comment. It is possible these replies could have replies themselves, so in that case you would just call **GET** `/comments/:id/replies` again with the new comment `id`. + + 📄 Pagination + */ + @discardableResult + public func getReplies(commentID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler<[Comment]>) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)/replies", + withQuery: [:], + isAuthorized: false, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } + + /** + Add a new reply to an existing comment. Make sure to allow and encourage spoilers to be indicated in your app and follow the rules listed above. + + 🔒 OAuth: Required + */ + @discardableResult + public func postReply(commentID id: CustomStringConvertible, comment: String, isSpoiler spoiler: Bool? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + let body = TraktCommentBody(comment: comment, spoiler: spoiler) + let request = try post("comments/\(id)/replies", body: body) + return performRequest(request: request, completion: completion) + } + + // MARK: - Item + + /** + Returns all users who liked a comment. If you only need the `replies` count, the main `comment` object already has that, so no need to use this method. + + 📄 Pagination + */ + @discardableResult + public func getAttachedMediaItem(commentID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)/item", + withQuery: [:], + isAuthorized: true, + withHTTPMethod: .POST) + return performRequest(request: request, completion: completion) + } + + // MARK: - Likes + + /** + Returns the media item this comment is attached to. The media type can be `movie`, `show`, `season`, `episode`, or `list` and it also returns the standard media object for that media type. + + ✨ Extended Info + */ + @discardableResult + public func getUsersWhoLikedComment(commentID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler<[TraktCommentLikedUser]>) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)/likes", + withQuery: [:], + isAuthorized: true, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } + + // MARK: - Like + + /** + Votes help determine popular comments. Only one like is allowed per comment per user. + + 🔒 OAuth: Required + */ + @discardableResult + public func likeComment(commentID id: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)/like", + withQuery: [:], + isAuthorized: false, + withHTTPMethod: .POST) + return performRequest(request: request, completion: completion) + } + + /** + Remove a like on a comment. + + 🔒 OAuth: Required + */ + @discardableResult + public func removeLikeOnComment(commentID id: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/\(id)/like", + withQuery: [:], + isAuthorized: false, + withHTTPMethod: .DELETE) + return performRequest(request: request, completion: completion) + } + + // MARK: - Trending + + /** + Returns all comments with the most likes and replies over the last 7 days. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. + + 📄 Pagination + ✨ Extended + */ + @discardableResult + public func getTrendingComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectCompletionHandler<[TraktTrendingComment]>) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/trending/\(commentType.rawValue)/\(mediaType.rawValue)", + withQuery: ["include_replies": "\(includeReplies)"], + isAuthorized: false, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } + + // MARK: - Recent + + /** + Returns the most recently written comments across all of Trakt. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. + + 📄 Pagination + ✨ Extended + */ + @discardableResult + public func getRecentComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectCompletionHandler<[TraktTrendingComment]>) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/recent/\(commentType.rawValue)/\(mediaType.rawValue)", + withQuery: ["include_replies": "\(includeReplies)"], + isAuthorized: false, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } + + // MARK: - Updates + + /** + Returns the most recently updated comments across all of Trakt. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. If you want to `include_replies` that will return replies in place alongside top level comments. + + 📄 Pagination + ✨ Extended + */ + @discardableResult + public func getRecentlyUpdatedComments(commentType: CommentType, mediaType: Type2, includeReplies: Bool, completion: @escaping ObjectCompletionHandler<[TraktTrendingComment]>) throws -> URLSessionDataTask? { + let request = try mutableRequest(forPath: "comments/updates/\(commentType.rawValue)/\(mediaType.rawValue)", + withQuery: ["include_replies": "\(includeReplies)"], + isAuthorized: false, + withHTTPMethod: .GET) + return performRequest(request: request, completion: completion) + } +} diff --git a/Common/Wrapper/Episodes.swift b/Common/Wrapper/CompletionHandlerEndpoints/Episodes.swift similarity index 65% rename from Common/Wrapper/Episodes.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Episodes.swift index 745aa98..a5acb14 100644 --- a/Common/Wrapper/Episodes.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Episodes.swift @@ -18,8 +18,8 @@ extension TraktManager { **Note**: If the `first_aired` is unknown, it will be set to `null`. */ @discardableResult - public func getEpisodeSummary(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)", + public func getEpisodeSummary(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -38,13 +38,13 @@ extension TraktManager { - parameter language: 2 character language code */ @discardableResult - public func getEpisodeTranslations(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, language: String? = nil, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getEpisodeTranslations(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, language: String? = nil, completion: @escaping ObjectCompletionHandler<[TraktEpisodeTranslation]>) -> URLSessionDataTask? { var path = "shows/\(id)/seasons/\(season)/episodes/\(episode)/translations" if let language = language { path += "/\(language)" } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -60,7 +60,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getEpisodeComments(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getEpisodeComments(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = [:] @@ -71,7 +71,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/comments", + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/comments", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -87,7 +87,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getListsContainingEpisode(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getListsContainingEpisode(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var path = "shows/\(id)/seasons/\(season)/episodes/\(episode)/lists" if let listType = listType { path += "/\(listType)" @@ -106,7 +106,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -120,8 +120,8 @@ extension TraktManager { Returns rating (between 0 and 10) and distribution for an episode. */ @discardableResult - public func getEpisodeRatings(showID id: T, seasonNumber: NSNumber, episodeNumber: NSNumber, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(seasonNumber)/episodes/\(episodeNumber)/ratings", + public func getEpisodeRatings(showID id: CustomStringConvertible, seasonNumber: NSNumber, episodeNumber: NSNumber, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(seasonNumber)/episodes/\(episodeNumber)/ratings", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -135,8 +135,8 @@ extension TraktManager { Returns lots of episode stats. */ @discardableResult - public func getEpisodeStatistics(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, completion: @escaping statsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/stats", + public func getEpisodeStatistics(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, completion: @escaping statsCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/stats", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -150,8 +150,8 @@ extension TraktManager { Returns all users watching this episode right now. */ @discardableResult - public func getUsersWatchingEpisode(showID id: T, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/watching", + public func getUsersWatchingEpisode(showID id: CustomStringConvertible, seasonNumber season: NSNumber, episodeNumber episode: NSNumber, completion: @escaping ObjectCompletionHandler<[User]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/watching", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -173,8 +173,8 @@ extension TraktManager { **Note**: This returns a lot of data, so please only use this extended parameter if you actually need it! */ @discardableResult - public func getPeopleInEpisode(showID id: T, season: NSNumber, episode: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/people", + public func getPeopleInEpisode(showID id: CustomStringConvertible, season: NSNumber, episode: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/episodes/\(episode)/people", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Genres.swift b/Common/Wrapper/CompletionHandlerEndpoints/Genres.swift similarity index 85% rename from Common/Wrapper/Genres.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Genres.swift index 2cde3bf..6ba8c72 100644 --- a/Common/Wrapper/Genres.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Genres.swift @@ -14,8 +14,8 @@ extension TraktManager { Get a list of all genres, including names and slugs. */ @discardableResult - public func listGenres(type: WatchedType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "genres/\(type)", + public func listGenres(type: WatchedType, completion: @escaping ObjectCompletionHandler<[Genres]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "genres/\(type)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { diff --git a/Common/Wrapper/Languages.swift b/Common/Wrapper/CompletionHandlerEndpoints/Languages.swift similarity index 85% rename from Common/Wrapper/Languages.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Languages.swift index 0a3424e..16b3b3d 100644 --- a/Common/Wrapper/Languages.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Languages.swift @@ -14,8 +14,8 @@ extension TraktManager { Get a list of all genres, including names and slugs. */ @discardableResult - public func listLanguages(type: WatchedType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "languages/\(type)", + public func listLanguages(type: WatchedType, completion: @escaping ObjectCompletionHandler<[Languages]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "languages/\(type)", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { diff --git a/Common/Wrapper/Lists.swift b/Common/Wrapper/CompletionHandlerEndpoints/Lists.swift similarity index 77% rename from Common/Wrapper/Lists.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Lists.swift index 0d3a1de..131ae24 100644 --- a/Common/Wrapper/Lists.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Lists.swift @@ -16,9 +16,9 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getTrendingLists(completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getTrendingLists(completion: @escaping ObjectCompletionHandler<[TraktTrendingList]>) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "lists/trending", + let request = try? mutableRequest(forPath: "lists/trending", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { @@ -35,9 +35,9 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getPopularLists(completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getPopularLists(completion: @escaping ObjectCompletionHandler<[TraktTrendingList]>) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "lists/popular", + let request = try? mutableRequest(forPath: "lists/popular", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { diff --git a/Common/Wrapper/Movies.swift b/Common/Wrapper/CompletionHandlerEndpoints/Movies.swift similarity index 87% rename from Common/Wrapper/Movies.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Movies.swift index 83e1881..de5bd16 100644 --- a/Common/Wrapper/Movies.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Movies.swift @@ -18,7 +18,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getTrendingMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping TrendingMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getTrendingMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping TrendingMoviesCompletionHandler) -> URLSessionDataTask? { return getTrending(.Movies, pagination: pagination, extended: extended, filters: filters, completion: completion) } @@ -30,7 +30,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getPopularMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getPopularMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { return getPopular(.Movies, pagination: pagination, extended: extended, filters: filters, completion: completion) } @@ -42,7 +42,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getPlayedMovies(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getPlayedMovies(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTask? { return getPlayed(.Movies, period: period, pagination: pagination, completion: completion) } @@ -54,7 +54,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getWatchedMovies(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getWatchedMovies(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTask? { return getWatched(.Movies, period: period, pagination: pagination, completion: completion) } @@ -66,7 +66,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getCollectedMovies(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getCollectedMovies(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostMoviesCompletionHandler) -> URLSessionDataTask? { return getCollected(.Movies, period: period, pagination: pagination, completion: completion) } @@ -78,7 +78,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getAnticipatedMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping AnticipatedMovieCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getAnticipatedMovies(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping AnticipatedMovieCompletionHandler) -> URLSessionDataTask? { return getAnticipated(.Movies, pagination: pagination, extended: extended, completion: completion) } @@ -88,8 +88,8 @@ extension TraktManager { Returns the top 10 grossing movies in the U.S. box office last weekend. Updated every Monday morning. */ @discardableResult - public func getWeekendBoxOffice(extended: [ExtendedType] = [.Min], completion: @escaping BoxOfficeMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "movies/boxoffice", + public func getWeekendBoxOffice(extended: [ExtendedType] = [.Min], completion: @escaping BoxOfficeMoviesCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "movies/boxoffice", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -105,7 +105,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getUpdatedMovies(startDate: Date?, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUpdatedMovies(startDate: Date?, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { return getUpdated(.Movies, startDate: startDate, pagination: pagination, extended: extended, completion: completion) } @@ -115,7 +115,7 @@ extension TraktManager { Returns a single movie's details. */ @discardableResult - public func getMovieSummary(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping MovieCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieSummary(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping MovieCompletionHandler) -> URLSessionDataTask? { return getSummary(.Movies, id: id, extended: extended, completion: completion) } @@ -125,7 +125,7 @@ extension TraktManager { Returns all title aliases for a movie. Includes country where name is different. */ @discardableResult - public func getMovieAliases(movieID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieAliases(movieID id: T, completion: @escaping ObjectCompletionHandler<[Alias]>) -> URLSessionDataTask? { return getAliases(.Movies, id: id, completion: completion) } @@ -138,7 +138,7 @@ extension TraktManager { - parameter country: 2 character country code. Example: `us`. */ @discardableResult - public func getMovieReleases(movieID id: T, country: String?, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieReleases(movieID id: T, country: String?, completion: @escaping ObjectCompletionHandler<[TraktMovieRelease]>) -> URLSessionDataTask? { var path = "movies/\(id)/releases" @@ -146,7 +146,7 @@ extension TraktManager { path += "/\(country)" } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -160,7 +160,7 @@ extension TraktManager { Returns all translations for a movie, including language and translated values for title, tagline and overview. */ @discardableResult - public func getMovieTranslations(movieID id: T, language: String?, completion: @escaping MovieTranslationsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieTranslations(movieID id: T, language: String?, completion: @escaping MovieTranslationsCompletionHandler) -> URLSessionDataTask? { return getTranslations(.Movies, id: id, language: language, completion: completion) } @@ -172,7 +172,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getMovieComments(movieID id: T, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieComments(movieID id: T, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { return getComments(.Movies, id: id, pagination: pagination, completion: completion) } @@ -188,7 +188,7 @@ extension TraktManager { listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, - completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var path = "movies/\(id)/lists" if let listType = listType { path += "/\(listType)" @@ -207,7 +207,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -223,7 +223,7 @@ extension TraktManager { The `crew` object will be broken up into `production`, `art`, `crew`, `costume & make-up`, `directing`, `writing`, `sound`, and `camera` (if there are people for those crew positions). Each of those members will have a `job` and a standard `person` object. */ @discardableResult - public func getPeopleInMovie(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { + public func getPeopleInMovie(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { return getPeople(.Movies, id: id, extended: extended, completion: completion) } @@ -233,7 +233,7 @@ extension TraktManager { Returns rating (between 0 and 10) and distribution for a movie. */ @discardableResult - public func getMovieRatings(movieID id: T, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieRatings(movieID id: T, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTask? { return getRatings(.Movies, id: id, completion: completion) } @@ -245,7 +245,7 @@ extension TraktManager { **Note**: We are continuing to improve this algorithm. */ @discardableResult - public func getRelatedMovies(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping MoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getRelatedMovies(movieID id: T, extended: [ExtendedType] = [.Min], completion: @escaping MoviesCompletionHandler) -> URLSessionDataTask? { return getRelated(.Movies, id: id, extended: extended, completion: completion) } @@ -255,7 +255,7 @@ extension TraktManager { Returns lots of movie stats. */ @discardableResult - public func getMovieStatistics(movieID id: T, completion: @escaping statsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getMovieStatistics(movieID id: T, completion: @escaping statsCompletionHandler) -> URLSessionDataTask? { return getStatistics(.Movies, id: id, completion: completion) } @@ -265,7 +265,7 @@ extension TraktManager { Returns all users watching this movie right now. */ @discardableResult - public func getUsersWatchingMovie(movieID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUsersWatchingMovie(movieID id: T, completion: @escaping ObjectCompletionHandler<[User]>) -> URLSessionDataTask? { return getUsersWatching(.Movies, id: id, completion: completion) } } diff --git a/Common/Wrapper/People.swift b/Common/Wrapper/CompletionHandlerEndpoints/People.swift similarity index 89% rename from Common/Wrapper/People.swift rename to Common/Wrapper/CompletionHandlerEndpoints/People.swift index 03c7079..3f74f58 100644 --- a/Common/Wrapper/People.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/People.swift @@ -18,8 +18,8 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getPersonDetails(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "people/\(id)", + public func getPersonDetails(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "people/\(id)", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { @@ -40,7 +40,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getMovieCredits(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { + public func getMovieCredits(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { return getCredits(type: WatchedType.Movies, id: id, extended: extended, completion: completion) } @@ -54,7 +54,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getShowCredits(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { + public func getShowCredits(personID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { return getCredits(type: WatchedType.Shows, id: id, extended: extended, completion: completion) } @@ -66,7 +66,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getListsContainingPerson(personId id: T, listType: ListType? = nil, sortBy: ListSortType? = nil, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getListsContainingPerson(personId id: T, listType: ListType? = nil, sortBy: ListSortType? = nil, completion: @escaping ObjectCompletionHandler<[TraktList]>) -> URLSessionDataTask? { var path = "people/\(id)/lists" if let listType = listType { path += "/\(listType)" @@ -76,7 +76,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -87,8 +87,8 @@ extension TraktManager { // MARK: - Private @discardableResult - private func getCredits(type: WatchedType, id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "people/\(id)/\(type)", + private func getCredits(type: WatchedType, id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "people/\(id)/\(type)", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { diff --git a/Common/Wrapper/Recommendations.swift b/Common/Wrapper/CompletionHandlerEndpoints/Recommendations.swift similarity index 84% rename from Common/Wrapper/Recommendations.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Recommendations.swift index a46848e..2831f41 100644 --- a/Common/Wrapper/Recommendations.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Recommendations.swift @@ -19,7 +19,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getRecommendedMovies(completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getRecommendedMovies(completion: @escaping ObjectCompletionHandler<[TraktMovie]>) -> URLSessionDataTask? { return getRecommendations(.Movies, completion: completion) } @@ -29,7 +29,7 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func hideRecommendedMovie(movieID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { + public func hideRecommendedMovie(movieID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { return hideRecommendation(type: .Movies, id: id, completion: completion) } @@ -39,7 +39,7 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func getRecommendedShows(completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getRecommendedShows(completion: @escaping ObjectCompletionHandler<[TraktShow]>) -> URLSessionDataTask? { return getRecommendations(.Shows, completion: completion) } @@ -49,15 +49,15 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func hideRecommendedShow(showID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { + public func hideRecommendedShow(showID id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { return hideRecommendation(type: .Shows, id: id, completion: completion) } // MARK: - Private @discardableResult - private func getRecommendations(_ type: WatchedType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "recommendations/\(type)", + private func getRecommendations(_ type: WatchedType, completion: @escaping ObjectCompletionHandler<[T]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "recommendations/\(type)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { @@ -70,8 +70,8 @@ extension TraktManager { } @discardableResult - private func hideRecommendation(type: WatchedType, id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "recommendations/\(type)/\(id)", + private func hideRecommendation(type: WatchedType, id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "recommendations/\(type)/\(id)", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { diff --git a/Common/Wrapper/Scrobble.swift b/Common/Wrapper/CompletionHandlerEndpoints/Scrobble.swift similarity index 82% rename from Common/Wrapper/Scrobble.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Scrobble.swift index 1ce1619..55b01e1 100644 --- a/Common/Wrapper/Scrobble.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Scrobble.swift @@ -20,8 +20,8 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func scrobbleStart(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - return try perform("start", scrobble: scrobble, completion: completion) + public func scrobbleStart(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + try perform("start", scrobble: scrobble, completion: completion) } // MARK: - Pause @@ -32,8 +32,8 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func scrobblePause(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - return try perform("pause", scrobble: scrobble, completion: completion) + public func scrobblePause(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + try perform("pause", scrobble: scrobble, completion: completion) } // MARK: - Stop @@ -48,16 +48,15 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func scrobbleStop(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - return try perform("stop", scrobble: scrobble, completion: completion) + public func scrobbleStop(_ scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + try perform("stop", scrobble: scrobble, completion: completion) } // MARK: - Private @discardableResult - func perform(_ scrobbleAction: String, scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { - // Request - guard let request = post("scrobble/\(scrobbleAction)", body: scrobble) else { return nil } + func perform(_ scrobbleAction: String, scrobble: TraktScrobble, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { + let request = try post("scrobble/\(scrobbleAction)", body: scrobble) return performRequest(request: request, completion: completion) } } diff --git a/Common/Wrapper/Search.swift b/Common/Wrapper/CompletionHandlerEndpoints/Search.swift similarity index 94% rename from Common/Wrapper/Search.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Search.swift index c5b08ce..c5005f4 100644 --- a/Common/Wrapper/Search.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Search.swift @@ -25,7 +25,7 @@ extension TraktManager { pagination: Pagination? = nil, filters: [Filter]? = nil, fields: [SearchType.Field]? = nil, - completion: @escaping SearchCompletionHandler) -> URLSessionDataTaskProtocol? { + completion: @escaping SearchCompletionHandler) -> URLSessionDataTask? { let typesString = types.map { $0.rawValue }.joined(separator: ",") // Search with multiple types var query: [String: String] = ["query": query, @@ -50,7 +50,7 @@ extension TraktManager { } // - guard let request = mutableRequest( + guard let request = try? mutableRequest( forPath: "search/\(typesString)", withQuery: query, isAuthorized: false, @@ -72,7 +72,7 @@ extension TraktManager { extended: [ExtendedType] = [.Min], type: SearchType, pagination: Pagination? = nil, - completion: @escaping SearchCompletionHandler) -> URLSessionDataTaskProtocol? { + completion: @escaping SearchCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString(), "type": type.rawValue] @@ -84,7 +84,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "search/\(id.name)/\(id.id)", + guard let request = try? mutableRequest(forPath: "search/\(id.name)/\(id.id)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Seasons.swift b/Common/Wrapper/CompletionHandlerEndpoints/Seasons.swift similarity index 87% rename from Common/Wrapper/Seasons.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Seasons.swift index c7ec285..70cc28f 100644 --- a/Common/Wrapper/Seasons.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Seasons.swift @@ -21,8 +21,8 @@ extension TraktManager { */ @discardableResult - public func getSeasons(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping SeasonsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons", + public func getSeasons(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping SeasonsCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -40,12 +40,12 @@ extension TraktManager { ✨ Extended */ @discardableResult - public func getEpisodesForSeason(showID id: T, season: NSNumber, translatedInto language: String? = nil, extended: [ExtendedType] = [.Min], completion: @escaping EpisodesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getEpisodesForSeason(showID id: T, season: NSNumber, translatedInto language: String? = nil, extended: [ExtendedType] = [.Min], completion: @escaping EpisodesCompletionHandler) -> URLSessionDataTask? { var query = ["extended": extended.queryString()] query["translations"] = language - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)", + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -61,7 +61,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getAllSeasonComments(showID id: T, season: NSNumber, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getAllSeasonComments(showID id: T, season: NSNumber, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = [:] // pagination @@ -71,7 +71,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/comments", + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/comments", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -87,7 +87,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getListsContainingSeason(showID id: T, season: NSNumber, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getListsContainingSeason(showID id: T, season: NSNumber, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var path = "shows/\(id)/seasons/\(season)/lists" if let listType = listType { path += "/\(listType)" @@ -106,7 +106,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -120,8 +120,8 @@ extension TraktManager { Returns rating (between 0 and 10) and distribution for a season. */ @discardableResult - public func getSeasonRatings(showID id: T, season: NSNumber, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/ratings", + public func getSeasonRatings(showID id: T, season: NSNumber, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/ratings", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -135,8 +135,8 @@ extension TraktManager { Returns lots of season stats. */ @discardableResult - public func getSeasonStatistics(showID id: T, season: NSNumber, completion: @escaping statsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/stats", + public func getSeasonStatistics(showID id: T, season: NSNumber, completion: @escaping statsCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/stats", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -150,8 +150,8 @@ extension TraktManager { Returns all users watching this season right now. */ @discardableResult - public func getUsersWatchingSeasons(showID id: T, season: NSNumber, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/watching", + public func getUsersWatchingSeasons(showID id: T, season: NSNumber, completion: @escaping ObjectCompletionHandler<[User]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/watching", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -172,8 +172,8 @@ extension TraktManager { **Note**: This returns a lot of data, so please only use this extended parameter if you actually need it! */ @discardableResult - public func getPeopleInSeason(showID id: T, season: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/seasons/\(season)/people", + public func getPeopleInSeason(showID id: T, season: NSNumber, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/seasons/\(season)/people", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/SharedFunctions.swift b/Common/Wrapper/CompletionHandlerEndpoints/SharedFunctions.swift similarity index 66% rename from Common/Wrapper/SharedFunctions.swift rename to Common/Wrapper/CompletionHandlerEndpoints/SharedFunctions.swift index bd8dcc5..cea36eb 100644 --- a/Common/Wrapper/SharedFunctions.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/SharedFunctions.swift @@ -12,7 +12,7 @@ internal extension TraktManager { // MARK: - Trending - func getTrending(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping ((_ result: ObjectsResultTypePagination) -> Void)) -> URLSessionDataTaskProtocol? { + func getTrending(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -29,7 +29,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/trending", + guard let request = try? mutableRequest(forPath: "\(type)/trending", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -39,7 +39,7 @@ internal extension TraktManager { // MARK: - Popular - func getPopular(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping ((_ result: ObjectsResultTypePagination) -> Void)) -> URLSessionDataTaskProtocol? { + func getPopular(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -56,7 +56,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/popular", + guard let request = try? mutableRequest(forPath: "\(type)/popular", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -66,7 +66,7 @@ internal extension TraktManager { // MARK: - Played - func getPlayed(_ type: WatchedType, period: Period = .Weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping ((_ result: ObjectsResultTypePagination) -> Void)) -> URLSessionDataTaskProtocol? { + func getPlayed(_ type: WatchedType, period: Period = .weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -77,7 +77,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/played/\(period.rawValue)", + guard let request = try? mutableRequest(forPath: "\(type)/played/\(period.rawValue)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -87,7 +87,7 @@ internal extension TraktManager { // MARK: - Watched - func getWatched(_ type: WatchedType, period: Period = .Weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + func getWatched(_ type: WatchedType, period: Period = .weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -98,7 +98,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/watched/\(period.rawValue)", + guard let request = try? mutableRequest(forPath: "\(type)/watched/\(period.rawValue)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -108,7 +108,7 @@ internal extension TraktManager { // MARK: - Collected - func getCollected(_ type: WatchedType, period: Period = .Weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + func getCollected(_ type: WatchedType, period: Period = .weekly, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -119,7 +119,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/collected/\(period.rawValue)", + guard let request = try? mutableRequest(forPath: "\(type)/collected/\(period.rawValue)", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -129,7 +129,7 @@ internal extension TraktManager { // MARK: - Anticipated - func getAnticipated(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + func getAnticipated(_ type: WatchedType, pagination: Pagination?, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -140,7 +140,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/anticipated", + guard let request = try? mutableRequest(forPath: "\(type)/anticipated", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -150,7 +150,7 @@ internal extension TraktManager { // MARK: - Updates - func getUpdated(_ type: WatchedType, startDate: Date?, pagination: Pagination?, extended: [ExtendedType], completion: @escaping UpdateCompletionHandler) -> URLSessionDataTaskProtocol? { + func getUpdated(_ type: WatchedType, startDate: Date?, pagination: Pagination?, extended: [ExtendedType], completion: @escaping UpdateCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -166,7 +166,7 @@ internal extension TraktManager { path.append(startDateString) } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -176,8 +176,8 @@ internal extension TraktManager { // MARK: - Summary - func getSummary(_ type: WatchedType, id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)", + func getSummary(_ type: WatchedType, id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -187,8 +187,8 @@ internal extension TraktManager { // MARK: - Aliases - func getAliases(_ type: WatchedType, id: T, completion: @escaping AliasCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/aliases", + func getAliases(_ type: WatchedType, id: CustomStringConvertible, completion: @escaping AliasCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/aliases", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -197,14 +197,14 @@ internal extension TraktManager { // MARK: - Translations - func getTranslations(_ type: WatchedType, id: T, language: String?, completion: @escaping ((_ result: ObjectsResultType) -> Void)) -> URLSessionDataTaskProtocol? { - + func getTranslations(_ type: WatchedType, id: CustomStringConvertible, language: String?, completion: @escaping ObjectCompletionHandler<[U]>) -> URLSessionDataTask? { + var path = "\(type)/\(id)/translations" if let language = language { path += "/\(language)" } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -213,7 +213,7 @@ internal extension TraktManager { // MARK: - Comments - func getComments(_ type: WatchedType, id: T, pagination: Pagination?, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + func getComments(_ type: WatchedType, id: CustomStringConvertible, pagination: Pagination?, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = [:] @@ -224,7 +224,7 @@ internal extension TraktManager { } } - guard let request = mutableRequest(forPath: "\(type)/\(id)/comments", + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/comments", withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -234,8 +234,8 @@ internal extension TraktManager { // MARK: - People - func getPeople(_ type: WatchedType, id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/people", + func getPeople(_ type: WatchedType, id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/people", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -245,8 +245,8 @@ internal extension TraktManager { // MARK: - Ratings - func getRatings(_ type: WatchedType, id: T, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/ratings", + func getRatings(_ type: WatchedType, id: CustomStringConvertible, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/ratings", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -256,8 +256,8 @@ internal extension TraktManager { // MARK: - Related - func getRelated(_ type: WatchedType, id: T, extended: [ExtendedType] = [.Min], completion: @escaping ((_ result: ObjectsResultType) -> Void)) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/related", + func getRelated(_ type: WatchedType, id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler<[U]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/related", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -267,8 +267,8 @@ internal extension TraktManager { // MARK: - Stats - func getStatistics(_ type: WatchedType, id: T, completion: @escaping statsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/stats", + func getStatistics(_ type: WatchedType, id: CustomStringConvertible, completion: @escaping statsCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/stats", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -278,8 +278,8 @@ internal extension TraktManager { // MARK: - Watching - func getUsersWatching(_ type: WatchedType, id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "\(type)/\(id)/watching", + func getUsersWatching(_ type: WatchedType, id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler<[User]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "\(type)/\(id)/watching", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Shows.swift b/Common/Wrapper/CompletionHandlerEndpoints/Shows.swift similarity index 76% rename from Common/Wrapper/Shows.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Shows.swift index 6f6b7de..319be45 100644 --- a/Common/Wrapper/Shows.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Shows.swift @@ -18,7 +18,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getTrendingShows(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping TrendingShowsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getTrendingShows(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping TrendingShowsCompletionHandler) -> URLSessionDataTask? { return getTrending(.Shows, pagination: pagination, extended: extended, filters: filters, completion: completion) } @@ -30,7 +30,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getPopularShows(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getPopularShows(pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], filters: [Filter]? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { return getPopular(.Shows, pagination: pagination, extended: extended, filters: filters, completion: completion) } @@ -42,7 +42,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getPlayedShows(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getPlayedShows(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTask? { return getPlayed(.Shows, period: period, pagination: pagination, completion: completion) } @@ -54,7 +54,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getWatchedShows(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getWatchedShows(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTask? { return getWatched(.Shows, period: period, pagination: pagination, completion: completion) } @@ -66,7 +66,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getCollectedShows(period: Period = .Weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getCollectedShows(period: Period = .weekly, pagination: Pagination? = nil, completion: @escaping MostShowsCompletionHandler) -> URLSessionDataTask? { return getCollected(.Shows, pagination: pagination, completion: completion) } @@ -78,7 +78,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getAnticipatedShows(period: Period = .Weekly, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping AnticipatedShowCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getAnticipatedShows(period: Period = .weekly, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping AnticipatedShowCompletionHandler) -> URLSessionDataTask? { return getAnticipated(.Shows, pagination: pagination, extended: extended, completion: completion) } @@ -90,7 +90,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getUpdatedShows(startDate: Date?, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUpdatedShows(startDate: Date?, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { return getUpdated(.Shows, startDate: startDate, pagination: pagination, extended: extended, completion: completion) } @@ -101,7 +101,7 @@ extension TraktManager { */ @discardableResult - public func getUpdatedShowTraktIds(from startDate: Date, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUpdatedShowTraktIds(from startDate: Date, pagination: Pagination? = nil, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { var query = [String: String]() // pagination @@ -111,7 +111,7 @@ extension TraktManager { } } let path = "shows/updates/id/\(startDate.dateString(withFormat: "yyyy-MM-dd'T'HH:mm:ss").addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? startDate.dateString(withFormat: "yyyy-MM-dd"))" - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -127,7 +127,7 @@ extension TraktManager { **Note**: When getting `full` extended info, the `status` field can have a value of `returning series` (airing right now), `in production` (airing soon), `planned` (in development), `canceled`, or `ended`. */ @discardableResult - public func getShowSummary(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowSummary(showID id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { return getSummary(.Shows, id: id, extended: extended, completion: completion) } @@ -139,7 +139,7 @@ extension TraktManager { - parameter id: Trakt.tv ID, Trakt.tv slug, or IMDB ID */ @discardableResult - public func getShowAliases(showID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowAliases(showID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler<[Alias]>) -> URLSessionDataTask? { return getAliases(.Shows, id: id, completion: completion) } @@ -152,7 +152,7 @@ extension TraktManager { - parameter language: 2 character language code. Example: `es` */ @discardableResult - public func getShowTranslations(showID id: T, language: String?, completion: @escaping ShowTranslationsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowTranslations(showID id: CustomStringConvertible, language: String?, completion: @escaping ShowTranslationsCompletionHandler) -> URLSessionDataTask? { return getTranslations(.Shows, id: id, language: language, completion: completion) } @@ -164,7 +164,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getShowComments(showID id: T, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowComments(showID id: CustomStringConvertible, pagination: Pagination? = nil, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { return getComments(.Shows, id: id, pagination: pagination, completion: completion) } @@ -176,7 +176,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getListsContainingShow(showID id: T, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getListsContainingShow(showID id: CustomStringConvertible, listType: ListType? = nil, sortBy: ListSortType? = nil, pagination: Pagination? = nil, completion: @escaping ObjectCompletionHandler<[TraktList]>) -> URLSessionDataTask? { var path = "shows/\(id)/lists" if let listType = listType { path += "/\(listType)" @@ -195,7 +195,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -211,9 +211,9 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func getShowCollectionProgress(showID id: T, hidden: Bool = false, specials: Bool = false, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowCollectionProgress(showID id: CustomStringConvertible, hidden: Bool = false, specials: Bool = false, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "shows/\(id)/progress/collection", + let request = try? mutableRequest(forPath: "shows/\(id)/progress/collection", withQuery: ["hidden": "\(hidden)", "specials": "\(specials)"], isAuthorized: true, @@ -230,9 +230,9 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func getShowWatchedProgress(showID id: T, hidden: Bool = false, specials: Bool = false, completion: @escaping ShowWatchedProgressCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowWatchedProgress(showID id: CustomStringConvertible, hidden: Bool = false, specials: Bool = false, completion: @escaping ShowWatchedProgressCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "shows/\(id)/progress/watched", + let request = try? mutableRequest(forPath: "shows/\(id)/progress/watched", withQuery: ["hidden": "\(hidden)", "specials": "\(specials)"], isAuthorized: true, @@ -255,7 +255,7 @@ extension TraktManager { **Note**: This returns a lot of data, so please only use this extended parameter if you actually need it! */ @discardableResult - public func getPeopleInShow(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTaskProtocol? { + public func getPeopleInShow(showID id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler>) -> URLSessionDataTask? { return getPeople(.Shows, id: id, extended: extended, completion: completion) } @@ -265,7 +265,7 @@ extension TraktManager { Returns rating (between 0 and 10) and distribution for a show. */ @discardableResult - public func getShowRatings(showID id: T, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowRatings(showID id: CustomStringConvertible, completion: @escaping RatingDistributionCompletionHandler) -> URLSessionDataTask? { return getRatings(.Shows, id: id, completion: completion) } @@ -277,7 +277,7 @@ extension TraktManager { **Note**: We are continuing to improve this algorithm. */ @discardableResult - public func getRelatedShows(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getRelatedShows(showID id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler<[TraktShow]>) -> URLSessionDataTask? { return getRelated(.Shows, id: id, extended: extended, completion: completion) } @@ -287,7 +287,7 @@ extension TraktManager { Returns lots of show stats. */ @discardableResult - public func getShowStatistics(showID id: T, completion: @escaping statsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getShowStatistics(showID id: CustomStringConvertible, completion: @escaping statsCompletionHandler) -> URLSessionDataTask? { return getStatistics(.Shows, id: id, completion: completion) } @@ -297,7 +297,7 @@ extension TraktManager { Returns all users watching this show right now. */ @discardableResult - public func getUsersWatchingShow(showID id: T, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUsersWatchingShow(showID id: CustomStringConvertible, completion: @escaping ObjectCompletionHandler<[User]>) -> URLSessionDataTask? { return getUsersWatching(.Shows, id: id, completion: completion) } @@ -309,8 +309,8 @@ extension TraktManager { **Note**: If no episode is found, a 204 HTTP status code will be returned. */ @discardableResult - public func getNextEpisode(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/next_episode", + public func getNextEpisode(showID id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/next_episode", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -326,8 +326,8 @@ extension TraktManager { **Note**: If no episode is found, a 204 HTTP status code will be returned. */ @discardableResult - public func getLastEpisode(showID id: T, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "shows/\(id)/last_episode", + public func getLastEpisode(showID id: CustomStringConvertible, extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "shows/\(id)/last_episode", withQuery: ["extended": extended.queryString()], isAuthorized: false, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/Sync.swift b/Common/Wrapper/CompletionHandlerEndpoints/Sync.swift similarity index 87% rename from Common/Wrapper/Sync.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Sync.swift index b1fc45b..f448002 100644 --- a/Common/Wrapper/Sync.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Sync.swift @@ -19,11 +19,11 @@ extension TraktManager { - parameter completion: completion block - - returns: URLSessionDataTaskProtocol? + - returns: URLSessionDataTask? */ @discardableResult - public func lastActivities(completion: @escaping LastActivitiesCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "sync/last_activities", + public func lastActivities(completion: @escaping LastActivitiesCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "sync/last_activities", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -46,8 +46,8 @@ extension TraktManager { - parameter type: Possible Values: .Movies, .Episodes */ @discardableResult - public func getPlaybackProgress(type: WatchedType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "sync/playback/\(type)", + public func getPlaybackProgress(type: WatchedType, completion: @escaping ObjectCompletionHandler<[PlaybackProgress]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "sync/playback/\(type)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -62,8 +62,8 @@ extension TraktManager { 🔒 OAuth: Required */ @discardableResult - public func removePlaybackItem(id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "sync/playback/\(id)", + public func removePlaybackItem(id: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "sync/playback/\(id)", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } @@ -87,8 +87,8 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getCollection(type: WatchedType, extended: [ExtendedType] = [.Min], completion: @escaping CollectionCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "sync/collection/\(type)", + public func getCollection(type: WatchedType, extended: [ExtendedType] = [.Min], completion: @escaping CollectionCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "sync/collection/\(type)", withQuery: ["extended": extended.queryString()], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -97,24 +97,24 @@ extension TraktManager { /** Add items to a user's collection. Accepts shows, seasons, episodes and movies. If only a show is passed, all episodes for the show will be collected. If seasons are specified, all episodes in those seasons will be collected. - + Send a `collected_at` UTC datetime to mark items as collected in the past. You can also send additional metadata about the media itself to have a very accurate collection. Showcase what is available to watch from your epic HD DVD collection down to your downloaded iTunes movies. - - **Note**: You can resend items already in your collection and they will be updated with any new values. This includes the `collected_at` and any other metadata. - + + > Note: You can resend items already in your collection and they will be updated with any new values. This includes the `collected_at` and any other metadata. + Status Code: 201 - + 🔒 OAuth: Required - + - parameter movies: Array of movie Trakt ids - parameter shows: Array of show Trakt ids - parameter seasons: Array of season Trakt ids - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func addToCollection(movies: [CollectionId]? = nil, shows: [CollectionId]? = nil, seasons: [CollectionId]? = nil, episodes: [CollectionId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func addToCollection(movies: [CollectionId]? = nil, shows: [CollectionId]? = nil, seasons: [CollectionId]? = nil, episodes: [CollectionId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/collection", body: body) else { return nil } + let request = try post("sync/collection", body: body) return performRequest(request: request, completion: completion) } @@ -131,9 +131,9 @@ extension TraktManager { - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func removeFromCollection(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func removeFromCollection(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/collection/remove", body: body) else { return nil } + let request = try post("sync/collection/remove", body: body) return performRequest(request: request, completion: completion) } @@ -155,10 +155,10 @@ extension TraktManager { - parameter completion: completion handler */ @discardableResult - public func getWatchedShows(extended: [ExtendedType] = [.Min], completion: @escaping WatchedShowsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getWatchedShows(extended: [ExtendedType] = [.Min], completion: @escaping WatchedShowsCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "sync/watched/shows", + let request = try? mutableRequest(forPath: "sync/watched/shows", withQuery: ["extended": extended.queryString()], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -181,9 +181,9 @@ extension TraktManager { - parameter completion: completion handler */ @discardableResult - public func getWatchedMovies(extended: [ExtendedType] = [.Min], completion: @escaping WatchedMoviesCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getWatchedMovies(extended: [ExtendedType] = [.Min], completion: @escaping WatchedMoviesCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "sync/watched/movies", + let request = try? mutableRequest(forPath: "sync/watched/movies", withQuery: ["extended": extended.queryString()], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -202,7 +202,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getHistory(type: WatchedType? = nil, traktID: Int? = nil, startAt: Date? = nil, endAt: Date? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HistoryCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getHistory(type: WatchedType? = nil, traktID: Int? = nil, startAt: Date? = nil, endAt: Date? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HistoryCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -231,7 +231,7 @@ extension TraktManager { } guard - let request = mutableRequest(forPath: path, + let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -248,15 +248,16 @@ extension TraktManager { 🔒 OAuth: Required - - parameter movies: array of movie objects - - parameter shows: array of show objects - - parameter episodes: array of episode objects - - parameter completion: completion handler + - parameters: + - movies: array of `movie` objects + - shows: array of `show` objects + - seasons: array of `season` objects + - episodes: array of `episode` objects */ @discardableResult - public func addToHistory(movies: [AddToHistoryId]? = nil, shows: [AddToHistoryId]? = nil, seasons: [AddToHistoryId]? = nil, episodes: [AddToHistoryId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func addToHistory(movies: [AddToHistoryId]? = nil, shows: [AddToHistoryId]? = nil, seasons: [AddToHistoryId]? = nil, episodes: [AddToHistoryId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/history", body: body) else { return nil } + let request = try post("sync/history", body: body) return performRequest(request: request, completion: completion) } @@ -276,9 +277,9 @@ extension TraktManager { - parameter completion: completion handler */ @discardableResult - public func removeFromHistory(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, historyIDs: [Int]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func removeFromHistory(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, historyIDs: [Int]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, ids: historyIDs) - guard let request = post("sync/history/remove", body: body) else { return nil } + let request = try post("sync/history/remove", body: body) return performRequest(request: request, completion: completion) } @@ -293,14 +294,14 @@ extension TraktManager { - parameter rating: Filter for a specific rating */ @discardableResult - public func getRatings(type: WatchedType, rating: NSInteger?, completion: @escaping RatingsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getRatings(type: WatchedType, rating: NSInteger?, completion: @escaping RatingsCompletionHandler) -> URLSessionDataTask? { var path = "sync/ratings/\(type)" if let rating = rating { path += "/\(rating)" } guard - let request = mutableRequest(forPath: path, + let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -320,9 +321,9 @@ extension TraktManager { - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func addRatings(movies: [RatingId]? = nil, shows: [RatingId]? = nil, seasons: [RatingId]? = nil, episodes: [RatingId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func addRatings(movies: [RatingId]? = nil, shows: [RatingId]? = nil, seasons: [RatingId]? = nil, episodes: [RatingId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/ratings", body: body) else { return nil } + let request = try post("sync/ratings", body: body) return performRequest(request: request, completion: completion) } @@ -337,9 +338,9 @@ extension TraktManager { - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func removeRatings(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func removeRatings(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/ratings/remove", body: body) else { return nil } + let request = try post("sync/ratings/remove", body: body) return performRequest(request: request, completion: completion) } @@ -358,7 +359,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getWatchlist(watchType: WatchedType, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping WatchlistCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getWatchlist(watchType: WatchedType, pagination: Pagination? = nil, extended: [ExtendedType] = [.Min], completion: @escaping WatchlistCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] @@ -369,7 +370,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "sync/watchlist/\(watchType.rawValue)", + guard let request = try? mutableRequest(forPath: "sync/watchlist/\(watchType.rawValue)", withQuery: query, isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -389,9 +390,9 @@ extension TraktManager { - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func addToWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func addToWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/watchlist", body: body) else { completion(.error(error: nil)); return nil } + let request = try post("sync/watchlist", body: body) return performRequest(request: request, completion: completion) } @@ -408,9 +409,9 @@ extension TraktManager { - parameter episodes: Array of episode Trakt ids */ @discardableResult - public func removeFromWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func removeFromWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes) - guard let request = post("sync/watchlist/remove", body: body) else { completion(.error(error: nil)); return nil } + let request = try post("sync/watchlist/remove", body: body) return performRequest(request: request, completion: completion) } } diff --git a/Common/Wrapper/Users.swift b/Common/Wrapper/CompletionHandlerEndpoints/Users.swift similarity index 78% rename from Common/Wrapper/Users.swift rename to Common/Wrapper/CompletionHandlerEndpoints/Users.swift index 8119c11..b748647 100644 --- a/Common/Wrapper/Users.swift +++ b/Common/Wrapper/CompletionHandlerEndpoints/Users.swift @@ -29,8 +29,8 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func getSettings(completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/settings", + public func getSettings(completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/settings", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -45,8 +45,8 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func getFollowRequests(completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/requests", + public func getFollowRequests(completion: @escaping ObjectCompletionHandler<[FollowRequest]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/requests", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -63,8 +63,8 @@ extension TraktManager { - parameter id: ID of the follower request. Example: `123`. */ @discardableResult - public func approveFollowRequest(requestID id: NSNumber, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/requests/\(id)", + public func approveFollowRequest(requestID id: NSNumber, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/requests/\(id)", withQuery: [:], isAuthorized: true, withHTTPMethod: .POST) else { return nil } @@ -79,8 +79,8 @@ extension TraktManager { - parameter id: ID of the follower request. Example: `123`. */ @discardableResult - public func denyFollowRequest(requestID id: NSNumber, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/requests/\(id)", + public func denyFollowRequest(requestID id: NSNumber, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/requests/\(id)", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } @@ -97,7 +97,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func hiddenItems(section: SectionType, type: HiddenItemsType? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HiddenItemsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func hiddenItems(section: String, type: HiddenItemsType? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HiddenItemsCompletionHandler) -> URLSessionDataTask? { var query: [String: String] = ["extended": extended.queryString()] if let type = type { query["type"] = type.rawValue @@ -110,7 +110,7 @@ extension TraktManager { } } - guard let request = mutableRequest(forPath: "users/hidden/\(section.rawValue)", + guard let request = try? mutableRequest(forPath: "users/hidden/\(section)", withQuery: query, isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -123,9 +123,9 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func hide(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, from section: SectionType, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func hide(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, from section: String, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons) - guard let request = post("users/hidden/\(section.rawValue)", body: body) else { return nil } + let request = try post("users/hidden/\(section)", body: body) return performRequest(request: request, completion: completion) } @@ -135,9 +135,9 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func unhide(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, from section: SectionType, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func unhide(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, from section: String, completion: @escaping ObjectCompletionHandler) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons) - guard let request = post("users/hidden/\(section.rawValue)/remove", body: body) else { return nil } + let request = try post("users/hidden/\(section)/remove", body: body) return performRequest(request: request, completion: completion) } @@ -152,8 +152,8 @@ extension TraktManager { - Parameter type: Possible values: comments, lists. */ @discardableResult - public func getLikes(type: LikeType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/likes/\(type.rawValue)", + public func getLikes(type: LikeType, completion: @escaping ObjectCompletionHandler<[Like]>) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/likes/\(type.rawValue)", withQuery: [:], isAuthorized: true, withHTTPMethod: .GET) else { return nil } @@ -168,9 +168,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserProfile(username: String = "me", extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserProfile(username: String = "me", extended: [ExtendedType] = [.Min], completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)", + guard let request = try? mutableRequest(forPath: "users/\(username)", withQuery: ["extended": extended.queryString()], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -187,9 +187,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserCollection(username: String = "me", type: MediaType, completion: @escaping ObjectsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserCollection(username: String = "me", type: MediaType, completion: @escaping ObjectCompletionHandler<[TraktCollectedItem]>) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/collection/\(type.rawValue)", + guard let request = try? mutableRequest(forPath: "users/\(username)/collection/\(type.rawValue)", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -205,7 +205,7 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getUserComments(username: String = "me", commentType: CommentType? = nil, type: Type2? = nil, pagination: Pagination? = nil, completion: @escaping UserCommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserComments(username: String = "me", commentType: CommentType? = nil, type: Type2? = nil, pagination: Pagination? = nil, completion: @escaping UserCommentsCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false var path = "users/\(username)/comments" if let commentType = commentType { @@ -226,7 +226,7 @@ extension TraktManager { } guard - let request = mutableRequest(forPath: path, + let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -241,10 +241,10 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getCustomLists(username: String = "me", completion: @escaping ListsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getCustomLists(username: String = "me", completion: @escaping ListsCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false guard - let request = mutableRequest(forPath: "users/\(username)/lists", + let request = try? mutableRequest(forPath: "users/\(username)/lists", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -263,7 +263,7 @@ extension TraktManager { - parameter allowComments: Are comments allowed? */ @discardableResult - public func createCustomList(listName: String, listDescription: String, privacy: String = "private", displayNumbers: Bool = false, allowComments: Bool = true, completion: @escaping ListCompletionHandler) throws -> URLSessionDataTaskProtocol? { + public func createCustomList(listName: String, listDescription: String, privacy: String = "private", displayNumbers: Bool = false, allowComments: Bool = true, completion: @escaping ListCompletionHandler) throws -> URLSessionDataTask? { // JSON let json: [String: Any] = [ @@ -275,7 +275,7 @@ extension TraktManager { ] // Request - guard var request = mutableRequest(forPath: "users/me/lists", withQuery: [:], isAuthorized: true, withHTTPMethod: .POST) else { return nil } + guard var request = try? mutableRequest(forPath: "users/me/lists", withQuery: [:], isAuthorized: true, withHTTPMethod: .POST) else { return nil } request.httpBody = try JSONSerialization.data(withJSONObject: json, options: []) return performRequest(request: request, completion: completion) @@ -289,10 +289,10 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getCustomList(username: String = "me", listID: T, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getCustomList(username: String = "me", listID: CustomStringConvertible, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false guard - let request = mutableRequest(forPath: "users/\(username)/lists/\(listID)", + let request = try? mutableRequest(forPath: "users/\(username)/lists/\(listID)", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -305,8 +305,8 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func updateCustomList(listID: T, listName: String? = nil, listDescription: String? = nil, privacy: String? = nil, displayNumbers: Bool? = nil, allowComments: Bool? = nil, completion: @escaping ListCompletionHandler) throws -> URLSessionDataTaskProtocol? { - + public func updateCustomList(listID: CustomStringConvertible, listName: String? = nil, listDescription: String? = nil, privacy: String? = nil, displayNumbers: Bool? = nil, allowComments: Bool? = nil, completion: @escaping ListCompletionHandler) throws -> URLSessionDataTask? { + // JSON var json = [String: Any]() json["name"] = listName @@ -316,7 +316,7 @@ extension TraktManager { json["allow_comments"] = allowComments // Request - guard var request = mutableRequest(forPath: "users/me/lists/\(listID)", withQuery: [:], isAuthorized: true, withHTTPMethod: .PUT) else { return nil } + guard var request = try? mutableRequest(forPath: "users/me/lists/\(listID)", withQuery: [:], isAuthorized: true, withHTTPMethod: .PUT) else { return nil } request.httpBody = try JSONSerialization.data(withJSONObject: json, options: []) return performRequest(request: request, completion: completion) @@ -328,9 +328,9 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func deleteCustomList(username: String = "me", listID: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { + public func deleteCustomList(username: String = "me", listID: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "users/\(username)/lists/\(listID)", + let request = try? mutableRequest(forPath: "users/\(username)/lists/\(listID)", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } @@ -345,9 +345,9 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func likeList(username: String = "me", listID: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { + public func likeList(username: String = "me", listID: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "users/\(username)/lists/\(listID)/like", + let request = try? mutableRequest(forPath: "users/\(username)/lists/\(listID)/like", withQuery: [:], isAuthorized: true, withHTTPMethod: .POST) else { return nil } @@ -360,9 +360,9 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func removeListLike(username: String = "me", listID: T, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { + public func removeListLike(username: String = "me", listID: CustomStringConvertible, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "users/\(username)/lists/\(listID)/like", + let request = try? mutableRequest(forPath: "users/\(username)/lists/\(listID)/like", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } @@ -377,7 +377,7 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getItemsForCustomList(username: String = "me", listID: T, type: [ListItemType]? = nil, extended: [ExtendedType] = [.Min], completion: @escaping ListItemCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getItemsForCustomList(username: String = "me", listID: CustomStringConvertible, type: [ListItemType]? = nil, extended: [ExtendedType] = [.Min], completion: @escaping ListItemCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false var path = "users/\(username)/lists/\(listID)/items" @@ -386,7 +386,7 @@ extension TraktManager { path += "/\(value)" } - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: ["extended": extended.queryString()], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -405,9 +405,9 @@ extension TraktManager { - parameter people: Array of people Trakt ids */ @discardableResult - public func addItemToCustomList(username: String = "me", listID: T, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil, completion: @escaping AddListItemCompletion) throws -> URLSessionDataTaskProtocol? { + public func addItemToCustomList(username: String = "me", listID: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil, completion: @escaping AddListItemCompletion) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) - guard let request = post("users/\(username)/lists/\(listID)/items", body: body) else { return nil } + let request = try post("users/\(username)/lists/\(listID)/items", body: body) return performRequest(request: request, completion: completion) } @@ -425,9 +425,9 @@ extension TraktManager { - parameter people: Array of people Trakt ids */ @discardableResult - public func removeItemFromCustomList(username: String = "me", listID: T, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil, completion: @escaping RemoveListItemCompletion) throws -> URLSessionDataTaskProtocol? { + public func removeItemFromCustomList(username: String = "me", listID: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil, completion: @escaping RemoveListItemCompletion) throws -> URLSessionDataTask? { let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) - guard let request = post("users/\(username)/lists/\(listID)/items/remove", body: body) else { return nil } + let request = try post("users/\(username)/lists/\(listID)/items/remove", body: body) return performRequest(request: request, completion: completion) } @@ -439,9 +439,9 @@ extension TraktManager { 📄 Pagination */ @discardableResult - public func getUserAllListComments(username: String = "me", listID: String, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserAllListComments(username: String = "me", listID: String, completion: @escaping CommentsCompletionHandler) -> URLSessionDataTask? { guard - let request = mutableRequest(forPath: "users/\(username)/lists/\(listID)/comments", + let request = try? mutableRequest(forPath: "users/\(username)/lists/\(listID)/comments", withQuery: [:], isAuthorized: false, withHTTPMethod: .GET) else { return nil } @@ -458,8 +458,8 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func followUser(username: String, completion: @escaping FollowUserCompletion) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/\(username)/follow", + public func followUser(username: String, completion: @escaping FollowUserCompletion) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/\(username)/follow", withQuery: [:], isAuthorized: true, withHTTPMethod: .POST) else { return nil } @@ -472,8 +472,8 @@ extension TraktManager { 🔒 OAuth Required */ @discardableResult - public func unfollowUser(username: String, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - guard let request = mutableRequest(forPath: "users/\(username)/follow", + public func unfollowUser(username: String, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + guard let request = try? mutableRequest(forPath: "users/\(username)/follow", withQuery: [:], isAuthorized: true, withHTTPMethod: .DELETE) else { return nil } @@ -488,9 +488,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserFollowers(username: String = "me", completion: @escaping FollowersCompletion) -> URLSessionDataTaskProtocol? { + public func getUserFollowers(username: String = "me", completion: @escaping FollowersCompletion) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/followers", + guard let request = try? mutableRequest(forPath: "users/\(username)/followers", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -505,10 +505,10 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserFollowing(username: String = "me", completion: @escaping FollowersCompletion) -> URLSessionDataTaskProtocol? { + public func getUserFollowing(username: String = "me", completion: @escaping FollowersCompletion) -> URLSessionDataTask? { let authorization = username == "me" ? true : false guard - let request = mutableRequest(forPath: "users/\(username)/following", + let request = try? mutableRequest(forPath: "users/\(username)/following", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -523,9 +523,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserFriends(username: String = "me", completion: @escaping FriendsCompletion) -> URLSessionDataTaskProtocol? { + public func getUserFriends(username: String = "me", completion: @escaping FriendsCompletion) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/friends", + guard let request = try? mutableRequest(forPath: "users/\(username)/friends", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -544,7 +544,7 @@ extension TraktManager { ✨ Extended Info */ @discardableResult - public func getUserWatchedHistory(username: String = "me", type: WatchedType? = nil, traktId: Int? = nil, startAt: Date? = nil, endAt: Date? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HistoryCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserWatchedHistory(username: String = "me", type: WatchedType? = nil, traktId: Int? = nil, startAt: Date? = nil, endAt: Date? = nil, extended: [ExtendedType] = [.Min], pagination: Pagination? = nil, completion: @escaping HistoryCompletionHandler) -> URLSessionDataTask? { var path = "users/\(username)/history" if let type = type { @@ -573,7 +573,7 @@ extension TraktManager { } let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: path, + guard let request = try? mutableRequest(forPath: path, withQuery: query, isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -588,7 +588,7 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserRatings(username: String = "me", type: MediaType? = nil, rating: NSNumber? = nil, completion: @escaping RatingsCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserRatings(username: String = "me", type: MediaType? = nil, rating: NSNumber? = nil, completion: @escaping RatingsCompletionHandler) -> URLSessionDataTask? { var path = "users/\(username)/ratings" @@ -602,7 +602,7 @@ extension TraktManager { let authorization = username == "me" ? true : false guard - let request = mutableRequest(forPath: path, + let request = try? mutableRequest(forPath: path, withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -617,9 +617,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserWatchlist(username: String = "me", type: WatchedType, extended: [ExtendedType] = [.Min], completion: @escaping ListItemCompletionHandler) -> URLSessionDataTaskProtocol? { + public func getUserWatchlist(username: String = "me", type: WatchedType, extended: [ExtendedType] = [.Min], completion: @escaping ListItemCompletionHandler) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/watchlist/\(type.rawValue)", + guard let request = try? mutableRequest(forPath: "users/\(username)/watchlist/\(type.rawValue)", withQuery: ["extended": extended.queryString()], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -634,11 +634,11 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserWatching(username: String = "me", completion: @escaping WatchingCompletion) -> URLSessionDataTaskProtocol? { + public func getUserWatching(username: String = "me", completion: @escaping WatchingCompletion) -> URLSessionDataTask? { // Should this function have a special completion handler? If it returns no data it is obvious that the user // is not watching anything, but checking a boolean in the completion block is also nice let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/watching", + guard let request = try? mutableRequest(forPath: "users/\(username)/watching", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -653,9 +653,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserWatched(username: String = "me", type: MediaType, extended: [ExtendedType] = [.Min], completion: @escaping UserWatchedCompletion) -> URLSessionDataTaskProtocol? { + public func getUserWatched(username: String = "me", type: MediaType, extended: [ExtendedType] = [.Min], completion: @escaping UserWatchedCompletion) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard var request = mutableRequest(forPath: "users/\(username)/watched/\(type.rawValue)", + guard var request = try? mutableRequest(forPath: "users/\(username)/watched/\(type.rawValue)", withQuery: ["extended": extended.queryString()], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } @@ -671,9 +671,9 @@ extension TraktManager { 🔓 OAuth Optional */ @discardableResult - public func getUserStats(username: String = "me", completion: @escaping UserStatsCompletion) -> URLSessionDataTaskProtocol? { + public func getUserStats(username: String = "me", completion: @escaping UserStatsCompletion) -> URLSessionDataTask? { let authorization = username == "me" ? true : false - guard let request = mutableRequest(forPath: "users/\(username)/stats", + guard let request = try? mutableRequest(forPath: "users/\(username)/stats", withQuery: [:], isAuthorized: authorization, withHTTPMethod: .GET) else { return nil } diff --git a/Common/Wrapper/CompletionHandlers.swift b/Common/Wrapper/CompletionHandlers.swift index 00e7138..21f282b 100644 --- a/Common/Wrapper/CompletionHandlers.swift +++ b/Common/Wrapper/CompletionHandlers.swift @@ -9,19 +9,13 @@ import Foundation /// Generic result type -public enum ObjectResultType { +public enum ObjectResultType: Sendable { case success(object: T) case error(error: Error?) } -/// Generic results type -public enum ObjectsResultType { - case success(objects: [T]) - case error(error: Error?) -} - /// Generic results type + Pagination -public enum ObjectsResultTypePagination { +public enum ObjectsResultTypePagination: Sendable { case success(objects: [T], currentPage: Int, limit: Int) case error(error: Error?) } @@ -30,215 +24,226 @@ extension TraktManager { // MARK: - Result Types - public enum DataResultType { + public enum DataResultType: Sendable { case success(data: Data) case error(error: Error?) } - public enum SuccessResultType { + public enum SuccessResultType: Sendable { case success case fail } - - public enum ProgressResultType { - case success - case fail(Int) - } - - public enum WatchingResultType { - case checkedIn(watching: TraktWatching) - case notCheckedIn - case error(error: Error?) - } - - public enum CheckinResultType { - case success(checkin: TraktCheckinResponse) - case checkedIn(expiration: Date) - case error(error: Error?) - } - - public enum TraktKitError: Error { - case couldNotParseData - case handlingRetry - } - - public enum TraktError: Error { - /// Bad Request - request couldn't be parsed + + public enum TraktError: LocalizedError, Equatable { + /// 204. Some methods will succeed but not return any content. The network manager doesn't handle this well at the moment as it wants to decode the data when it is empty. Instead I'll throw this error so that it can be ignored for now. + case noContent + + /// Bad Request (400) - request couldn't be parsed case badRequest - /// Oauth must be provided + /// Oauth must be provided (401) case unauthorized - /// Forbidden - invalid API key or unapproved app + /// Forbidden - invalid API key or unapproved app (403) case forbidden - /// Not Found - method exists, but no record found + /// Not Found - method exists, but no record found (404) case noRecordFound - /// Method Not Found - method doesn't exist + /// Method Not Found - method doesn't exist (405) case noMethodFound - /// Conflict - resource already created + /// Conflict - resource already created (409) case resourceAlreadyCreated - /// Account Limit Exceeded - list count, item count, etc + /// Precondition Failed - use application/json content type (412) + case preconditionFailed + /// Account Limit Exceeded - list count, item count, etc (420) case accountLimitExceeded - /// Locked User Account - have the user contact support + /// Unprocessable Entity - validation errors (422) + case unprocessableEntity + /// Locked User Account - have the user contact support (423) case accountLocked - /// VIP Only - user must upgrade to VIP + /// VIP Only - user must upgrade to VIP (426) case vipOnly - /// Rate Limit Exceeded + /// Rate Limit Exceede (429) + case retry(after: TimeInterval) + /// Rate Limit Exceeded, retry interval not available (429) case rateLimitExceeded(HTTPURLResponse) - /// Service Unavailable - server overloaded (try again in 30s) + /// Server Error - please open a support ticket (500) + case serverError + /// Service Unavailable - server overloaded (try again in 30s) (502 / 503 / 504) case serverOverloaded - /// Service Unavailable - Cloudflare error + /// Service Unavailable - Cloudflare error (520 / 521 / 522) case cloudflareError /// Full url response - case unhandled(HTTPURLResponse) + case unhandled(URLResponse) + + public var errorDescription: String? { + switch self { + case .noContent: + nil + case .badRequest: + "Request could not be parsed." + case .unauthorized: + "Unauthorized. Please sign in with Trakt." + case .forbidden: + "Forbidden. Invalid API key or unapproved app." + case .noRecordFound: + "No record found." + case .noMethodFound: + "Method not found." + case .resourceAlreadyCreated: + "Resource has already been created." + case .preconditionFailed: + "Invalid content type." + case .accountLimitExceeded: + "The number of Trakt lists or list items has been exceeded. Please see Trakt.tv for account limits and support." + case .unprocessableEntity: + "Invalid entity." + case .accountLocked: + "Trakt.tv has indicated that this account is locked. Please contact Trakt support to unlock your account." + case .vipOnly: + "This feature is VIP only with Trakt. Please see Trakt.tv for more information." + case .retry: + nil + case .rateLimitExceeded: + "Rate Limit Exceeded. Please try again in a minute." + case .serverError, .serverOverloaded, .cloudflareError: + "Trakt.tv is down. Please try again later." + case .unhandled(let urlResponse): + if let httpResponse = urlResponse as? HTTPURLResponse { + "Unhandled response. Status code \(httpResponse.statusCode)" + } else { + "Unhandled response. \(urlResponse.description)" + } + } + } } // MARK: - Completion handlers // MARK: Common - public typealias ObjectCompletionHandler = (_ result: ObjectResultType) -> Void - public typealias ObjectsCompletionHandler = (_ result: ObjectsResultType) -> Void - public typealias paginatedCompletionHandler = (_ result: ObjectsResultTypePagination) -> Void - - public typealias DataResultCompletionHandler = (_ result: DataResultType) -> Void - public typealias SuccessCompletionHandler = (_ result: SuccessResultType) -> Void - public typealias ProgressCompletionHandler = (_ result: ProgressResultType) -> Void + public typealias ObjectCompletionHandler = @Sendable (_ result: ObjectResultType) -> Void + public typealias paginatedCompletionHandler = @Sendable (_ result: ObjectsResultTypePagination) -> Void + + public typealias DataResultCompletionHandler = @Sendable (_ result: DataResultType) -> Void + public typealias SuccessCompletionHandler = @Sendable (_ result: SuccessResultType) -> Void public typealias CommentsCompletionHandler = paginatedCompletionHandler -// public typealias CastCrewCompletionHandler = ObjectCompletionHandler - - public typealias SearchCompletionHandler = ObjectsCompletionHandler + + public typealias SearchCompletionHandler = ObjectCompletionHandler<[TraktSearchResult]> public typealias statsCompletionHandler = ObjectCompletionHandler // MARK: Shared public typealias UpdateCompletionHandler = paginatedCompletionHandler - public typealias AliasCompletionHandler = ObjectsCompletionHandler + public typealias AliasCompletionHandler = ObjectCompletionHandler<[Alias]> public typealias RatingDistributionCompletionHandler = ObjectCompletionHandler // MARK: Calendar - public typealias dvdReleaseCompletionHandler = ObjectsCompletionHandler + public typealias dvdReleaseCompletionHandler = ObjectCompletionHandler<[TraktDVDReleaseMovie]> // MARK: Checkin - public typealias checkinCompletionHandler = (_ result: CheckinResultType) -> Void - + public typealias checkinCompletionHandler = ObjectCompletionHandler + // MARK: Shows public typealias TrendingShowsCompletionHandler = paginatedCompletionHandler public typealias MostShowsCompletionHandler = paginatedCompletionHandler public typealias AnticipatedShowCompletionHandler = paginatedCompletionHandler - public typealias ShowTranslationsCompletionHandler = ObjectsCompletionHandler - public typealias SeasonsCompletionHandler = ObjectsCompletionHandler + public typealias ShowTranslationsCompletionHandler = ObjectCompletionHandler<[TraktShowTranslation]> + public typealias SeasonsCompletionHandler = ObjectCompletionHandler<[TraktSeason]> - public typealias WatchedShowsCompletionHandler = ObjectsCompletionHandler + public typealias WatchedShowsCompletionHandler = ObjectCompletionHandler<[TraktWatchedShow]> public typealias ShowWatchedProgressCompletionHandler = ObjectCompletionHandler // MARK: Episodes public typealias EpisodeCompletionHandler = ObjectCompletionHandler - public typealias EpisodesCompletionHandler = ObjectsCompletionHandler + public typealias EpisodesCompletionHandler = ObjectCompletionHandler<[TraktEpisode]> // MARK: Movies public typealias MovieCompletionHandler = ObjectCompletionHandler - public typealias MoviesCompletionHandler = ObjectsCompletionHandler + public typealias MoviesCompletionHandler = ObjectCompletionHandler<[TraktMovie]> public typealias TrendingMoviesCompletionHandler = paginatedCompletionHandler public typealias MostMoviesCompletionHandler = paginatedCompletionHandler public typealias AnticipatedMovieCompletionHandler = paginatedCompletionHandler - public typealias MovieTranslationsCompletionHandler = ObjectsCompletionHandler + public typealias MovieTranslationsCompletionHandler = ObjectCompletionHandler<[TraktMovieTranslation]> public typealias WatchedMoviesCompletionHandler = paginatedCompletionHandler - public typealias BoxOfficeMoviesCompletionHandler = ObjectsCompletionHandler + public typealias BoxOfficeMoviesCompletionHandler = ObjectCompletionHandler<[TraktBoxOfficeMovie]> // MARK: Sync public typealias LastActivitiesCompletionHandler = ObjectCompletionHandler - public typealias RatingsCompletionHandler = ObjectsCompletionHandler + public typealias RatingsCompletionHandler = ObjectCompletionHandler<[TraktRating]> public typealias HistoryCompletionHandler = paginatedCompletionHandler - public typealias CollectionCompletionHandler = ObjectsCompletionHandler + public typealias CollectionCompletionHandler = ObjectCompletionHandler<[TraktCollectedItem]> // MARK: Users public typealias ListCompletionHandler = ObjectCompletionHandler - public typealias ListsCompletionHandler = ObjectsCompletionHandler - public typealias ListItemCompletionHandler = ObjectsCompletionHandler + public typealias ListsCompletionHandler = ObjectCompletionHandler<[TraktList]> + public typealias ListItemCompletionHandler = ObjectCompletionHandler<[TraktListItem]> public typealias WatchlistCompletionHandler = paginatedCompletionHandler public typealias HiddenItemsCompletionHandler = paginatedCompletionHandler - public typealias UserCommentsCompletionHandler = ObjectsCompletionHandler + public typealias UserCommentsCompletionHandler = ObjectCompletionHandler<[UsersComments]> public typealias AddListItemCompletion = ObjectCompletionHandler public typealias RemoveListItemCompletion = ObjectCompletionHandler public typealias FollowUserCompletion = ObjectCompletionHandler - public typealias FollowersCompletion = ObjectsCompletionHandler - public typealias FriendsCompletion = ObjectsCompletionHandler - public typealias WatchingCompletion = (_ result: WatchingResultType) -> Void + public typealias FollowersCompletion = ObjectCompletionHandler<[FollowResult]> + public typealias FriendsCompletion = ObjectCompletionHandler<[Friend]> + public typealias WatchingCompletion = ObjectCompletionHandler public typealias UserStatsCompletion = ObjectCompletionHandler - public typealias UserWatchedCompletion = ObjectsCompletionHandler + public typealias UserWatchedCompletion = ObjectCompletionHandler<[TraktWatchedItem]> // MARK: - Error handling - private func handleResponse(response: URLResponse?, retry: @escaping (() -> Void)) throws { - guard let httpResponse = response as? HTTPURLResponse else { throw TraktKitError.couldNotParseData } - + private func handleResponse(response: URLResponse?) throws(TraktError) { + guard let response else { return } + guard let httpResponse = response as? HTTPURLResponse else { throw .unhandled(response) } + guard 200...299 ~= httpResponse.statusCode else { switch httpResponse.statusCode { - case StatusCodes.BadRequest: - throw TraktError.badRequest - case StatusCodes.Unauthorized: - throw TraktError.unauthorized - case StatusCodes.Forbidden: - throw TraktError.forbidden - case StatusCodes.NotFound: - throw TraktError.noRecordFound - case StatusCodes.MethodNotFound: - throw TraktError.noMethodFound - case StatusCodes.Conflict: - throw TraktError.resourceAlreadyCreated - case StatusCodes.AccountLimitExceeded: - throw TraktError.accountLimitExceeded - case StatusCodes.acountLocked: - throw TraktError.accountLocked - case StatusCodes.vipOnly: - throw TraktError.vipOnly - case StatusCodes.RateLimitExceeded: - if let retryAfter = httpResponse.allHeaderFields["retry-after"] as? String, - let retryInterval = TimeInterval(retryAfter) { - DispatchQueue.main.asyncAfter(deadline: .now() + retryInterval) { - retry() - } - /// To ensure completionHandler isn't called when retrying. - throw TraktKitError.handlingRetry + case 400: throw .badRequest + case 401: throw .unauthorized + case 403: throw .forbidden + case 404: throw .noRecordFound + case 405: throw .noMethodFound + case 409: throw .resourceAlreadyCreated + case 412: throw .preconditionFailed + case 420: throw .accountLimitExceeded + case 422: throw .unprocessableEntity + case 423: throw .accountLocked + case 426: throw .vipOnly + case 429: + let rawRetryAfter = httpResponse.allHeaderFields["retry-after"] + if let retryAfterString = rawRetryAfter as? String, + let retryAfter = TimeInterval(retryAfterString) { + throw .retry(after: retryAfter) + } else if let retryAfter = rawRetryAfter as? TimeInterval { + throw .retry(after: retryAfter) } else { - throw TraktError.rateLimitExceeded(httpResponse) + throw .rateLimitExceeded(httpResponse) } - - case 503, 504: - throw TraktError.serverOverloaded - case 500...600: - throw TraktError.cloudflareError + case 500: throw .serverError + // Try again in 30 seconds throw + case 502, 503, 504: throw .serverOverloaded + case 500...600: throw .cloudflareError default: - throw TraktError.unhandled(httpResponse) + throw .unhandled(httpResponse) } } } - - // MARK: - Perform Requests - func perform(request: URLRequest) async throws -> T { - let (data, _) = try await session.data(for: request) - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) - let object = try decoder.decode(T.self, from: data) - return object - } + // MARK: - Perform Requests /// Data - func performRequest(request: URLRequest, completion: @escaping DataResultCompletionHandler) -> URLSessionDataTaskProtocol? { - let datatask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { + func performRequest(request: URLRequest, completion: @escaping DataResultCompletionHandler) -> URLSessionDataTask? { + let datatask = session.dataTask(with: request) { [weak self] data, response, error in + guard let self else { return } + if let error { completion(.error(error: error)) return } // Check response - do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) + do throws(TraktError) { + try self.handleResponse(response: response) } catch { switch error { - case TraktKitError.handlingRetry: - break + case .retry(let after): + DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self, completion] in + _ = self?.performRequest(request: request, completion: completion) + } default: completion(.error(error: error)) } @@ -257,23 +262,23 @@ extension TraktManager { } /// Success / Failure - func performRequest(request: URLRequest, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTaskProtocol? { - let datatask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } + func performRequest(request: URLRequest, completion: @escaping SuccessCompletionHandler) -> URLSessionDataTask? { + let datatask = session.dataTask(with: request) { [weak self] data, response, error in + guard let self else { return } guard error == nil else { completion(.fail) return } // Check response - do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) + do throws(TraktError) { + try self.handleResponse(response: response) } catch { switch error { - case TraktKitError.handlingRetry: - break + case .retry(let after): + DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self, completion] in + _ = self?.performRequest(request: request, completion: completion) + } default: completion(.fail) } @@ -285,62 +290,16 @@ extension TraktManager { datatask.resume() return datatask } - - /// Checkin - func performRequest(request: URLRequest, completion: @escaping checkinCompletionHandler) -> URLSessionDataTaskProtocol? { - let datatask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { - completion(.error(error: error)) - return - } - - // Check data - guard let data = data else { - completion(.error(error: TraktKitError.couldNotParseData)) - return - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) - - if let checkin = try? decoder.decode(TraktCheckinResponse.self, from: data) { - completion(.success(checkin: checkin)) - return - } else if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), - let jsonDictionary = jsonObject as? RawJSON, - let expirationDateString = jsonDictionary["expires_at"] as? String, - let expirationDate = try? Date.dateFromString(expirationDateString) { - completion(.checkedIn(expiration: expirationDate)) - return - } - - // Check response - do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) - } catch { - switch error { - case TraktKitError.handlingRetry: - break - default: - completion(.error(error: error)) - } - return - } - completion(.error(error: nil)) - } - datatask.resume() - return datatask - } - // Generic array of Trakt objects - func performRequest(request: URLRequest, completion: @escaping ((_ result: ObjectResultType) -> Void)) -> URLSessionDataTaskProtocol? { + func performRequest(request: URLRequest, completion: @escaping ObjectCompletionHandler) -> URLSessionDataTask? { let aCompletion: DataResultCompletionHandler = { (result) -> Void in switch result { case .success(let data): + guard !data.isEmpty else { + completion(.error(error: TraktError.noContent)) + return + } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) do { @@ -357,71 +316,27 @@ extension TraktManager { let dataTask = performRequest(request: request, completion: aCompletion) return dataTask } - - /// Array of TraktProtocol objects - func performRequest(request: URLRequest, completion: @escaping ((_ result: ObjectsResultType) -> Void)) -> URLSessionDataTaskProtocol? { - let dataTask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { - completion(.error(error: error)) - return - } - - // Check response - do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) - } catch { - switch error { - case TraktKitError.handlingRetry: - break - default: - completion(.error(error: error)) - } - return - } - - // Check data - guard let data = data else { - completion(.error(error: TraktKitError.couldNotParseData)) - return - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) - do { - let array = try decoder.decode([T].self, from: data) - completion(.success(objects: array)) - } catch { - completion(.error(error: error)) - } - } - - dataTask.resume() - return dataTask - } - + /// Array of ObjectsResultTypePagination objects - func performRequest(request: URLRequest, completion: @escaping ((_ result: ObjectsResultTypePagination) -> Void)) -> URLSessionDataTaskProtocol? { - let dataTask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { + func performRequest(request: URLRequest, completion: @escaping paginatedCompletionHandler) -> URLSessionDataTask? { + let dataTask = session.dataTask(with: request) { [weak self] data, response, error in + guard let self else { return } + if let error { completion(.error(error: error)) return } - + guard let httpResponse = response as? HTTPURLResponse else { return completion(.error(error: nil)) } // Check response - do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) + do throws(TraktError) { + try self.handleResponse(response: response) } catch { switch error { - case TraktKitError.handlingRetry: - break + case .retry(let after): + DispatchQueue.global().asyncAfter(deadline: .now() + after) { [weak self, completion] in + _ = self?.performRequest(request: request, completion: completion) + } default: completion(.error(error: error)) } @@ -459,54 +374,63 @@ extension TraktManager { dataTask.resume() return dataTask } - - // Watching - func performRequest(request: URLRequest, completion: @escaping WatchingCompletion) -> URLSessionDataTaskProtocol? { - let dataTask = session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { - completion(.error(error: error)) - return - } - - guard let httpResponse = response as? HTTPURLResponse else { return completion(.error(error: nil)) } - - // Check response + + // MARK: - Async await + + /** + Downloads the contents of a URL based on the specified URL request. Handles ``TraktError/retry(after:)`` up to the specified `retryLimit` + */ + func fetchData(request: URLRequest, retryLimit: Int = 3) async throws -> (Data, URLResponse) { + var retryCount = 0 + + while true { do { - try self.handleResponse(response: response, retry: { - _ = self.performRequest(request: request, completion: completion) - }) - } catch { + let (data, response) = try await session.data(for: request) + try handleResponse(response: response) + return (data, response) + } catch let error as TraktError { switch error { - case TraktKitError.handlingRetry: - break + case .retry(let retryDelay): + retryCount += 1 + if retryCount >= retryLimit { + throw error + } + print("Retrying after delay: \(retryDelay)") + try await Task.sleep(for: .seconds(retryDelay)) + try Task.checkCancellation() default: - completion(.error(error: error)) + throw error } - return - } - - if httpResponse.statusCode == StatusCodes.SuccessNoContentToReturn { - completion(.notCheckedIn) - return - } - - // Check data - guard let data = data else { - completion(.error(error: TraktKitError.couldNotParseData)) - return - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) - do { - let watching = try decoder.decode(TraktWatching.self, from: data) - completion(.checkedIn(watching: watching)) } catch { - completion(.error(error: error)) + throw error } } - dataTask.resume() - return dataTask + } + + /** + Downloads the contents of a URL based on the specified URL request, and decodes the data into a `TraktObject` + */ + func perform(request: URLRequest, retryLimit: Int = 3) async throws -> T { + let (data, response) = try await fetchData(request: request, retryLimit: retryLimit) + return try decodeTraktObject(from: data, response: response) + } + + /// Decodes data into a TraktObject. If the `TraktObject` type is `PagedObject` the headers will be extracted from the response. + private func decodeTraktObject(from data: Data, response: URLResponse) throws -> T { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy) + + if let pagedType = T.self as? PagedObjectProtocol.Type { + let decodedItems = try decoder.decode(pagedType.objectType, from: data) + var currentPage = 0 + var pageCount = 0 + if let r = response as? HTTPURLResponse { + currentPage = Int(r.value(forHTTPHeaderField: "x-pagination-page") ?? "0") ?? 0 + pageCount = Int(r.value(forHTTPHeaderField: "x-pagination-page-count") ?? "0") ?? 0 + } + return pagedType.createPagedObject(with: decodedItems, currentPage: currentPage, pageCount: pageCount) as! T + } + + return try decoder.decode(T.self, from: data) } } diff --git a/Common/Wrapper/Enums.swift b/Common/Wrapper/Enums.swift index 880c298..8e4cee4 100644 --- a/Common/Wrapper/Enums.swift +++ b/Common/Wrapper/Enums.swift @@ -8,7 +8,7 @@ import Foundation -public enum Method: String { +public enum Method: String, Sendable { /// Select one or more items. Success returns 200 status code. case GET /// Create a new item. Success returns 201 status code. @@ -17,9 +17,22 @@ public enum Method: String { case PUT /// Delete an item. Success returns 200 or 204 status code. case DELETE + + public var expectedResult: Int { + switch self { + case .GET: + 200 + case .POST: + 201 + case .PUT: + 200 + case .DELETE: + 204 + } + } } -public struct StatusCodes { +public struct StatusCodes: Sendable { /// Success public static let Success = 200 /// Success - new resource created (POST) @@ -58,42 +71,17 @@ public struct StatusCodes { public static let CloudflareError2 = 521 /// Service Unavailable - Cloudflare error public static let CloudflareError3 = 522 - - static func message(for status: Int) -> String? { - switch status { - case Unauthorized: - return "App not authorized. Please sign in again." - case Forbidden: - return "Invalid API Key" - case NotFound: - return "API not found" - case AccountLimitExceeded: - return "The number of Trakt lists or list items has been exceeded. Please see Trakt.tv for account limits and support." - case acountLocked: - return "Trakt.tv has indicated that this account is locked. Please contact Trakt support to unlock your account." - case vipOnly: - return "This feature is VIP only with Trakt. Please see Trakt.tv for more information." - case RateLimitExceeded: - return "Rate Limit Exceeded. Please try again in a minute." - case ServerError.. Route { + let body = OAuthBody( + code: code, + clientId: traktManager.clientId, + clientSecret: traktManager.clientSecret, + redirectURI: traktManager.redirectURI, + grantType: "authorization_code" + ) + return Route( + paths: [path, "token"], + body: body, + method: .POST, + requiresAuthentication: false, + traktManager: traktManager + ) + } + + /** + Use the `refresh_token` to get a new `access_token` without asking the user to re-authenticate. The `access_token` is valid for **24 hours** before it needs to be refreshed again. + */ + public func getAccessToken(from refreshToken: String) -> Route { + let body = OAuthBody( + refreshToken: refreshToken, + clientId: traktManager.clientId, + clientSecret: traktManager.clientSecret, + redirectURI: traktManager.redirectURI, + grantType: "refresh_token" + ) + return Route( + paths: [path, "token"], + body: body, + method: .POST, + requiresAuthentication: false, + traktManager: traktManager + ) + } + + /** + An `access_token` can be revoked when a user signs out of their Trakt account in your app. This is not required, but might improve the user experience so the user doesn't have an unused app connection hanging around. + */ + public func revokeToken(_ accessToken: String) -> EmptyRoute { + let body = OAuthBody( + accessToken: accessToken, + clientId: traktManager.clientId, + clientSecret: traktManager.clientSecret + ) + return EmptyRoute( + paths: [path, "revoke"], + body: body, + method: .POST, + requiresAuthentication: false, + traktManager: traktManager + ) + } + + // MARK: - Authentication - Devices + + /** + Generate new codes to start the device authentication process. The `device_code` and interval will be used later to poll for the `access_token`. The `user_code` and `verification_url` should be presented to the user as mentioned in the flow steps above. + + **QR Code** + + You might consider generating a QR code for the user to easily scan on their mobile device. The QR code should be a URL that redirects to the `verification_url` and appends the `user_code`. For example, `https://trakt.tv/activate/5055CC52` would load the Trakt hosted `verification_url` and pre-fill in the `user_code`. + */ + public func generateDeviceCode() -> Route { + let body = OAuthBody(clientId: traktManager.clientId) + return Route( + paths: [path, "device", "code"], + body: body, + method: .POST, + requiresAuthentication: false, + traktManager: traktManager + ) + } + + /** + Use the `device_code` and poll at the `interval` (in seconds) to check if the user has authorized you app. Use `expires_in` to stop polling after that many seconds, and gracefully instruct the user to restart the process. **It is important to poll at the correct interval and also stop polling when expired.** + + When you receive a `200` success response, save the `access_token` so your app can authenticate the user in methods that require it. The `access_token` is valid for 24 hours. Save and use the `refresh_token` to get a new `access_token` without asking the user to re-authenticate. Check below for all the error codes that you should handle. + + > important: This function does **NOT** poll for the access token. This makes a single request to the endpoint and returns authentication info if available, and the status code used for polling. Use ``../TraktManager/pollForAccessToken(deviceCode:)`` to poll for the access token. + */ + public func requestAccessToken(code: String) async throws -> (AuthenticationInfo?, Int) { + let body = OAuthBody( + code: code, + clientId: traktManager.clientId, + clientSecret: traktManager.clientSecret + ) + let request = try traktManager.mutableRequest(forPath: "oauth/device/token", isAuthorized: false, withHTTPMethod: .POST, body: body) + + // Make response + let (data, response) = try await traktManager.session.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + + // Don't throw, we want to check the `responseCode` even if the token response cannot be decoded. + let tokenResponse = try? JSONDecoder().decode(AuthenticationInfo.self, from: data) + + return (tokenResponse, httpResponse.statusCode) + } + } +} diff --git a/Common/Wrapper/Resources/CheckinResource.swift b/Common/Wrapper/Resources/CheckinResource.swift new file mode 100644 index 0000000..d24e123 --- /dev/null +++ b/Common/Wrapper/Resources/CheckinResource.swift @@ -0,0 +1,63 @@ +// +// CheckinResource.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/8/25. +// + +extension TraktManager { + /** + Checking in is a manual action used by mobile apps allowing the user to indicate what they are watching right now. While not as effortless as scrobbling, checkins help fill in the gaps. You might be watching live tv, at a friend's house, or watching a movie in theaters. You can simply checkin from your phone or tablet in those situations. The item will display as watching on the site, then automatically switch to watched status once the duration has elapsed. + */ + public struct CheckinResource { + private let traktManager: TraktManager + private let path: String = "checkin" + + internal init(traktManager: TraktManager) { + self.traktManager = traktManager + } + + // MARK: - Routes + + /** + Check into a movie or episode. This should be tied to a user action to manually indicate they are watching something. The item will display as watching on the site, then automatically switch to watched status once the duration has elapsed. A unique history `id` (64-bit integer) will be returned and can be used to reference this checkin directly. + + --- + **Sharing** + + The sharing object is optional and will apply the user's settings if not sent. If sharing is sent, each key will override the user's setting for that social network. Send true to post or false to not post on the indicated social network. You can see which social networks a user has connected with the ``TraktManager/CurrentUserResource/settings()`` method. + + > note: If a checkin is already in progress, a ``TraktManager/TraktError/resourceAlreadyCreated`` error will thrown. Use ``TraktManager/UsersResource/watching()`` to get the timestamp of when the check-in is completed. + + 🔒 OAuth Required + */ + public func checkInto( + movie: SyncId? = nil, + episode: SyncId? = nil, + sharing: ShareSettings? = nil, + message: String? = nil + ) async throws -> Route { + Route( + path: path, + body: TraktCheckinBody( + movie: movie, + episode: episode, + sharing: sharing, + message: message + ), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Removes any active checkins, no need to provide a specific item. + + 🔒 OAuth Required + */ + public func deleteActiveCheckin() async throws -> EmptyRoute { + EmptyRoute(path: path, method: .DELETE, requiresAuthentication: true, traktManager: traktManager) + } + } +} diff --git a/Common/Wrapper/Resources/EpisodeResource.swift b/Common/Wrapper/Resources/EpisodeResource.swift index 64288fe..0e48430 100644 --- a/Common/Wrapper/Resources/EpisodeResource.swift +++ b/Common/Wrapper/Resources/EpisodeResource.swift @@ -12,26 +12,132 @@ public struct EpisodeResource { public let showId: CustomStringConvertible public let seasonNumber: Int public let episodeNumber: Int - public let traktManager: TraktManager private let path: String - - init(showId: CustomStringConvertible, seasonNumber: Int, episodeNumber: Int, traktManager: TraktManager = .sharedManager) { + private let traktManager: TraktManager + + internal init(showId: CustomStringConvertible, seasonNumber: Int, episodeNumber: Int, traktManager: TraktManager) { self.showId = showId self.seasonNumber = seasonNumber self.episodeNumber = episodeNumber - self.traktManager = traktManager self.path = "shows/\(showId)/seasons/\(seasonNumber)/episodes/\(episodeNumber)" + self.traktManager = traktManager } - - public func summary() async throws -> Route { - try await traktManager.get(path) + + /** + Returns a single episode's details. All date and times are in UTC and were calculated using the episode's `air_date` and show's `country` and `air_time`. + + > note: If the `first_aired` is unknown, it will be set to `null`. + + > note: When getting `full` extended info, the `episode_type` field can have a value of `standard`, `series_premiere` (season 1, episode 1), `season_premiere` (episode 1), `mid_season_finale`, `mid_season_premiere` (the next episode after the mid season finale), `season_finale`, or `series_finale` (last episode to air for an ended show). + */ + public func summary() -> Route { + Route(path: path, method: .GET, traktManager: traktManager) } - - public func comments() async throws -> Route<[Comment]> { - try await traktManager.get(path + "/comments") + + /** + Returns all translations for an episode, including language and translated values for title and overview. + + - parameter language: 2 character language code + */ + public func translations(language: String? = nil) -> Route<[TraktEpisodeTranslation]> { + Route(paths: [path, "translations", language], method: .GET, traktManager: traktManager) } - - public func people() async throws -> Route> { - try await traktManager.get(path + "/people") + + /** + Returns all top level comments for an episode. By default, the `newest` comments are returned first. Other sorting options include `oldest`, most `likes`, most `replies`, `highest` rated, `lowest` rated, and most `plays`. + + > note: If you send OAuth, comments from blocked users will be automatically filtered out. + + 🔓 OAuth Optional 📄 Pagination 😁 Emojis + + - parameter sort: how to sort Example: `newest`. + - parameter authenticate: comments from blocked users will be automatically filtered out if `true`. + */ + public func comments(sort: String? = nil, authenticate: Bool = false) -> Route<[Comment]> { + Route(paths: [path, "comments", sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + /** + Returns all lists that contain this episode. By default, `personal` lists are returned sorted by the most `popular`. + + 📄 Pagination + */ + public func containingLists() -> Route<[TraktList]> { + Route(path: path + "/lists", method: .GET, traktManager: traktManager) + } + + /** + Returns all lists that contain this episode. By default, `personal` lists are returned sorted by the most `popular`. + + 📄 Pagination 😁 Emojis + + - parameter type: Filter for a specific list type. Possible values: `all` , `personal` , `official` , `watchlists` , `favorites` . + - parameter sort: How to sort . Possible values: `popular` , `likes` , `comments` , `items` , `added` , `updated` . + */ + public func containingLists(type: String? = nil, sort: String? = nil) -> Route> { + Route(paths: [path, "lists", type, sort], method: .GET, traktManager: traktManager) + } + + /** + Returns all `cast` and `crew` for an episode. Each `cast` member will have a `characters` array and a standard person object. + + The `crew` object will be broken up by department into `production, art, crew, costume & make-up, directing, writing, sound, camera, visual effects, lighting, and editing` (if there are people for those crew positions). Each of those members will have a `jobs` array and a standard `person` object. + + **Guest Stars** + + If you add `?extended=guest_stars` to the URL, it will return all guest stars that appeared in the episode. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + ✨ Extended Info + */ + public func people() -> Route> { + Route(paths: [path, "people"], method: .GET, traktManager: traktManager) + } + + /** + Returns rating (between 0 and 10) and distribution for an episode. + */ + public func ratings() -> Route { + Route(paths: [path, "ratings"], method: .GET, traktManager: traktManager) + } + + /** + Returns lots of episode stats. + */ + public func stats() -> Route { + Route(paths: [path, "stats"], method: .GET, traktManager: traktManager) + } + + /** + Returns all users watching this episode right now. + + ✨ Extended Info + */ + public func usersWatching() -> Route<[User]> { + Route(paths: [path, "watching"], method: .GET, traktManager: traktManager) + } + + /** + Returns all videos including trailers, teasers, clips, and featurettes. + + ✨ Extended Info + */ + public func videos() -> Route<[TraktVideo]> { + Route(paths: [path, "videos"], method: .GET, traktManager: traktManager) + } +} + +extension Route where T == [TraktList] { + func listType(_ listType: ListType) -> Self { + var copy = self + copy.path = "\(path)/\(listType.rawValue)" + return copy + } + + func sort(by sortType: ListSortType) -> Self { + var copy = self + copy.path = "\(path)/\(sortType.rawValue)" + return copy } } diff --git a/Common/Wrapper/Resources/ExploreResource.swift b/Common/Wrapper/Resources/ExploreResource.swift deleted file mode 100644 index a2fd6f1..0000000 --- a/Common/Wrapper/Resources/ExploreResource.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// ExploreResource.swift -// TraktKit -// -// Created by Maxamilian Litteral on 6/14/21. -// Copyright © 2021 Maximilian Litteral. All rights reserved. -// - -import Foundation - -public struct ExploreResource { - - // MARK: - Properties - - public let traktManager: TraktManager - - public lazy var trending = Trending(traktManager: traktManager) - public lazy var popular = Popular(traktManager: traktManager) - public lazy var recommended = Recommended(traktManager: traktManager) - public lazy var played = Played(traktManager: traktManager) - public lazy var watched = Watched(traktManager: traktManager) - public lazy var collected = Collected(traktManager: traktManager) - public lazy var anticipated = Anticipated(traktManager: traktManager) - - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - // MARK: - Routes - - public struct Trending { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktTrendingShow]> { - Route(path: "shows/trending", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktTrendingMovie]> { - Route(path: "movies/trending", method: .GET, traktManager: traktManager) - } - } - - public struct Popular { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktShow]> { - Route(path: "shows/popular", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktMovie]> { - Route(path: "movies/popular", method: .GET, traktManager: traktManager) - } - } - - public struct Recommended { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktTrendingShow]> { - Route(path: "shows/recommended", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktTrendingMovie]> { - Route(path: "movies/recommended", method: .GET, traktManager: traktManager) - } - } - - public struct Played { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktMostShow]> { - Route(path: "shows/played", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktMostMovie]> { - Route(path: "movies/played", method: .GET, traktManager: traktManager) - } - } - - public struct Watched { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktMostShow]> { - Route(path: "shows/watched", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktMostMovie]> { - Route(path: "movies/watched", method: .GET, traktManager: traktManager) - } - } - - public struct Collected { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktTrendingShow]> { - Route(path: "shows/collected", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktTrendingMovie]> { - Route(path: "movies/collected", method: .GET, traktManager: traktManager) - } - } - - public struct Anticipated { - public let traktManager: TraktManager - public init(traktManager: TraktManager = .sharedManager) { - self.traktManager = traktManager - } - - public func shows() -> Route<[TraktAnticipatedShow]> { - Route(path: "shows/anticipated", method: .GET, traktManager: traktManager) - } - - public func movies() -> Route<[TraktAnticipatedMovie]> { - Route(path: "movies/anticipated", method: .GET, traktManager: traktManager) - } - } -} diff --git a/Common/Wrapper/Resources/MovieResource.swift b/Common/Wrapper/Resources/MovieResource.swift index e57d75d..b0faff6 100644 --- a/Common/Wrapper/Resources/MovieResource.swift +++ b/Common/Wrapper/Resources/MovieResource.swift @@ -9,8 +9,245 @@ import Foundation extension TraktManager { + /// Endpoints for movies in general + public struct MoviesResource { + private let traktManager: TraktManager + internal init(traktManager: TraktManager) { + self.traktManager = traktManager + } + + /** + Returns all movies being watched right now. Movies with the most users are returned first. + */ + public func trending() -> Route> { + Route(path: "movies/trending", method: .GET, traktManager: traktManager) + } + + /** + Returns the most popular movies. Popularity is calculated using the rating percentage and the number of ratings. + */ + public func popular() -> Route> { + Route(path: "movies/popular", method: .GET, traktManager: traktManager) + } + + /** + Returns the most favorited movies in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func favorited(period: Period? = nil) -> Route> { + Route(paths: ["movies/favorited", period], method: .GET, traktManager: traktManager) + } + + /** + Returns the most played (a single user can watch multiple times) movies in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func played(period: Period? = nil) -> Route> { + Route(paths: ["movies/played", period], method: .GET, traktManager: traktManager) + } + + /** + Returns the most watched (unique users) movies in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func watched(period: Period? = nil) -> Route> { + Route(paths: ["movies/watched", period], method: .GET, traktManager: traktManager) + } + + /** + Returns the most collected (unique users) movies in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func collected(period: Period? = nil) -> Route> { + Route(paths: ["movies/collected", period], method: .GET, traktManager: traktManager) + } + + /** + Returns the most anticipated movies based on the number of lists a movie appears on. + */ + public func anticipated() -> Route> { + Route(path: "movies/anticipated", method: .GET, traktManager: traktManager) + } + + /** + Returns the top 10 grossing movies in the U.S. box office last weekend. Updated every Monday morning. + */ + public func boxOffice() -> Route<[TraktBoxOfficeMovie]> { + Route(path: "movies/boxoffice", method: .GET, traktManager: traktManager) + } + + /** + Returns all movies updated since the specified date. We recommended storing the date you can be efficient using this method moving forward. + + 📄 Pagination + + > important: The `startDate` is only accurate to the hour, for caching purposes. Please drop the minutes and seconds from your timestamp to help optimize our cached data. For example, use `2021-07-17T12:00:00Z` and not `2021-07-17T12:23:34Z` + + > note: .The `startDate` can only be a maximum of 30 days in the past. + */ + public func recentlyUpdated(since startDate: Date) async throws -> Route> { + let formattedDate = startDate.dateString(withFormat: "yyyy-MM-dd'T'HH:mm:ss") + return Route(path: "movies/updates/\(formattedDate)", method: .GET, traktManager: traktManager) + } + + /** + Returns all movie Trakt IDs updated since the specified UTC date and time. We recommended storing the X-Start-Date header you can be efficient using this method moving forward. By default, 10 results are returned. You can send a limit to get up to 100 results per page. + + 📄 Pagination + + > important: The `startDate` is only accurate to the hour, for caching purposes. Please drop the minutes and seconds from your timestamp to help optimize our cached data. For example, use `2021-07-17T12:00:00Z` and not `2021-07-17T12:23:34Z` + + > note: .The `startDate` can only be a maximum of 30 days in the past. + */ + public func recentlyUpdatedIds(since startDate: Date) async throws -> Route> { + let formattedDate = startDate.dateString(withFormat: "yyyy-MM-dd'T'HH:mm:ss") + return Route(path: "movies/updates/id/\(formattedDate)", method: .GET, traktManager: traktManager) + } + } + + /// Endpoints for a specific movie public struct MovieResource { - + // MARK: - Properties + + /// Trakt ID, Trakt slug, or IMDB ID + internal let id: String + internal let path: String + + private let traktManager: TraktManager + + // MARK: - Lifecycle + + internal init(id: CustomStringConvertible, traktManager: TraktManager) { + self.id = id.description + self.traktManager = traktManager + self.path = "movies/\(id)" + } + + // MARK: - Methods + + /** + Returns a single movie's details. + + > note: When getting `full` extended info, the `status` field can have a value of `released`, `in production`, `post production`, `planned`, `rumored`, or `canceled`. + */ + public func summary() -> Route { + Route(path: path, method: .GET, traktManager: traktManager) + } + + /** + Returns all title aliases for a movie. Includes country where name is different. + */ + public func aliases() -> Route<[Alias]> { + Route(paths: [path, "aliases"], method: .GET, traktManager: traktManager) + } + + /** + Returns all releases for a movie including country, certification, release date, release type, and note. The release type can be set to unknown, premiere, limited, theatrical, digital, physical, or tv. The note might have optional info such as the film festival name for a premiere release or Blu-ray specs for a physical release. We pull this info from TMDB.' + + - parameter country: 2 character country code Example: `us` + */ + public func releases(country: String? = nil) -> Route<[TraktMovieRelease]> { + Route(paths: [path, "releases", country], method: .GET, traktManager: traktManager) + } + + /** + Returns all translations for a movie, including language and translated values for title, tagline and overview. + + - parameter language: 2 character language code Example: `es` + */ + public func translations(language: String? = nil) -> Route<[TraktMovieTranslation]> { + Route(paths: [path, "translations", language], method: .GET, traktManager: traktManager) + } + + /** + Returns all top level comments for a movie. By default, the `newest` comments are returned first. Other sorting options include `oldest`, most `likes`, most `replies`, `highest` rated, `lowest` rated, most `plays`, and highest `watched` percentage. + + 🔓 OAuth Optional 📄 Pagination 😁 Emojis + + > note: If you send OAuth, comments from blocked users will be automatically filtered out. + + - parameter sort: how to sort Example: `newest`. + - parameter authenticate: comments from blocked users will be automatically filtered out if `true`. + */ + public func comments(sort: String? = nil, authenticate: Bool = false) -> Route> { + Route(paths: [path, "comments", sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + /** + Returns all lists that contain this movie. By default, `personal` lists are returned sorted by the most `popular`. + + 📄 Pagination 😁 Emojis + + - parameter type: Filter for a specific list type. Possible values: `all` , `personal` , `official` , `watchlists` , `favorites` . + - parameter sort: How to sort . Possible values: `popular` , `likes` , `comments` , `items` , `added` , `updated` . + */ + public func containingLists(type: String? = nil, sort: String? = nil) -> Route> { + Route(paths: [path, "lists", type, sort], method: .GET, traktManager: traktManager) + } + + /** + Returns all `cast` and `crew` for a movie. Each `cast` member will have a `characters` array and a standard `person` object.The `crew` object will be broken up by department into production, `art`, `crew`, `costume & make-up`, `directing`, `writing`, `sound`, `camera`, `visual effects`, `lighting`, and `editing` (if there are people for those crew positions). Each of those members will have a `jobs` array and a standard `person` object. + + ✨ Extended Info + */ + public func people() -> Route> { + Route(paths: [path, "people"], method: .GET, traktManager: traktManager) + } + + /** + Returns rating (between 0 and 10) and distribution for a movie. + */ + public func ratings() -> Route { + Route(paths: [path, "ratings"], method: .GET, traktManager: traktManager) + } + + /** + Returns related and similar movies. + + 📄 Pagination ✨ Extended Info + */ + public func relatedMovies() -> Route> { + Route(paths: [path, "related"], method: .GET, traktManager: traktManager) + } + + /** + Returns lots of movie stats. + */ + public func stats() -> Route { + Route(paths: [path, "stats"], method: .GET, traktManager: traktManager) + } + + /** + Returns all studios for a movie. + */ + public func studios() -> Route<[TraktStudio]> { + Route(paths: [path, "studios"], method: .GET, traktManager: traktManager) + } + + /** + Returns all users watching this movie right now. + + ✨ Extended Info + */ + public func usersWatching() -> Route<[User]> { + Route(paths: [path, "watching"], method: .GET, traktManager: traktManager) + } + + /** + Returns all videos including trailers, teasers, clips, and featurettes. + + ✨ Extended Info + */ + public func videos() -> Route<[TraktVideo]> { + Route(paths: [path, "videos"], method: .GET, traktManager: traktManager) + } + + /** + Queue this movie for a full metadata and image refresh. It might take up to 8 hours for the updated metadata to be availabe through the API. + + > note: If this movie is already queued, a 409 HTTP status code will returned. + + 🔥 VIP Only 🔒 OAuth Required + */ + public func refreshMetadata() -> EmptyRoute { + EmptyRoute(paths: [path, "refresh"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } } } diff --git a/Common/Wrapper/Resources/SearchResource.swift b/Common/Wrapper/Resources/SearchResource.swift index a2244a8..59943e9 100644 --- a/Common/Wrapper/Resources/SearchResource.swift +++ b/Common/Wrapper/Resources/SearchResource.swift @@ -1,5 +1,5 @@ // -// File.swift +// SearchResource.swift // // // Created by Maximilian Litteral on 10/2/22. @@ -8,28 +8,24 @@ import Foundation public struct SearchResource { - - // MARK: - Properties - - public let traktManager: TraktManager - - // MARK: - Lifecycle - - public init(traktManager: TraktManager = .sharedManager) { + + private let traktManager: TraktManager + + internal init(traktManager: TraktManager) { self.traktManager = traktManager } - + // MARK: - Actions public func search( _ query: String, types: [SearchType]// = [.movie, .show, .episode, .person, .list] - ) async throws -> Route<[TraktSearchResult]> { + ) -> Route<[TraktSearchResult]> { let searchTypes = types.map { $0.rawValue }.joined(separator: ",") - return try await traktManager.get("search/\(searchTypes)", resultType: [TraktSearchResult].self).query(query) + return Route(path: "search/\(searchTypes)", method: .GET, traktManager: traktManager).query(query) } - public func lookup(_ id: LookupType) async throws -> Route<[TraktSearchResult]> { - try await traktManager.get("search/\(id.name)/\(id.id)") + public func lookup(_ id: LookupType) -> Route<[TraktSearchResult]> { + Route(path: "search/\(id.name)/\(id.id)", method: .GET, traktManager: traktManager) } } diff --git a/Common/Wrapper/Resources/SeasonResource.swift b/Common/Wrapper/Resources/SeasonResource.swift index 53fffad..be17d72 100644 --- a/Common/Wrapper/Resources/SeasonResource.swift +++ b/Common/Wrapper/Resources/SeasonResource.swift @@ -11,24 +11,131 @@ import Foundation public struct SeasonResource { public let showId: CustomStringConvertible public let seasonNumber: Int - public let traktManager: TraktManager + private let path: String + private let traktManager: TraktManager - init(showId: CustomStringConvertible, seasonNumber: Int, traktManager: TraktManager = .sharedManager) { + internal init(showId: CustomStringConvertible, seasonNumber: Int, traktManager: TraktManager) { self.showId = showId self.seasonNumber = seasonNumber + self.path = "shows/\(showId)/seasons/\(seasonNumber)" self.traktManager = traktManager } // MARK: - Methods - public func summary() async throws -> Route { - try await traktManager.get("shows/\(showId)/seasons/\(seasonNumber)") + // MARK: Season + + /** + Returns a single seasons for a show. + + ✨ Extended Info + */ + public func info() -> Route { + Route(path: "\(path)/info", method: .GET, traktManager: traktManager) } - public func comments() async throws -> Route<[Comment]> { - try await traktManager.get("shows/\(showId)/seasons/\(seasonNumber)/comments") + // MARK: Episodes + + /** + Returns all episodes for a specific season of a show. + + **Translations** + + If you'd like to included translated episode titles and overviews in the response, include the translations parameter in the URL. Include all languages by setting the parameter to `all` or use a specific 2 digit country language code to further limit it. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + ✨ Extended Info + + - parameter translations: Include episode translations. Example: `es` + */ + public func episodes(translations: String? = nil) -> Route<[TraktEpisode]> { + Route(path: path, + queryItems: ["translations": translations].compactMapValues { $0 }, + method: .GET, + traktManager: traktManager) } - + + /** + Returns all translations for an season, including language and translated values for title and overview. + */ + public func translations(language: String? = nil) -> Route<[TraktSeasonTranslation]> { + Route(paths: [path, "translations", language], method: .GET, traktManager: traktManager) + } + + /** + Returns all top level comments for a season. By default, the `newest` comments are returned first. Other sorting options include `oldest`, most `likes`, most `replies`, `highest` rated, `lowest` rated, most `plays`, and highest `watched` percentage. + + > note: If you send OAuth, comments from blocked users will be automatically filtered out. + + 🔓 OAuth Optional 📄 Pagination 😁 Emojis + + - parameter sort: how to sort Example: `newest`. + - parameter authenticate: comments from blocked users will be automatically filtered out if `true`. + */ + public func comments(sort: String? = nil, authenticate: Bool = false) -> Route<[Comment]> { + Route(paths: [path, "comments", sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + /** + Returns all lists that contain this season. By default, `personal` lists are returned sorted by the most `popular`. + + 📄 Pagination 😁 Emojis + + - parameter type: Filter for a specific list type. Possible values: `all` , `personal` , `official` , `watchlists` , `favorites` . + - parameter sort: How to sort . Possible values: `popular` , `likes` , `comments` , `items` , `added` , `updated` . + */ + public func containingLists(type: String? = nil, sort: String? = nil) -> Route> { + Route(paths: [path, "lists", type, sort], method: .GET, traktManager: traktManager) + } + + /** + Returns all `cast` and `crew` for a season. Each `cast` member will have a `characters` array and a standard `person` object.The `crew` object will be broken up by department into production, `art`, `crew`, `costume & make-up`, `directing`, `writing`, `sound`, `camera`, `visual effects`, `lighting`, and `editing` (if there are people for those crew positions). Each of those members will have a `jobs` array and a standard `person` object. + + **Guest Stars** + + If you add `?extended=guest_stars` to the URL, it will return all guest stars that appeared in at least 1 episode of the season. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + ✨ Extended Info + */ + public func people() -> Route> { + Route(paths: [path, "people"], method: .GET, traktManager: traktManager) + } + + /** + Returns rating (between 0 and 10) and distribution for a season. + */ + public func ratings() -> Route { + Route(paths: [path, "ratings"], method: .GET, traktManager: traktManager) + } + + /** + Returns lots of season stats. + */ + public func stats() -> Route { + Route(paths: [path, "stats"], method: .GET, traktManager: traktManager) + } + + /** + Returns all users watching this season right now. + + ✨ Extended Info + */ + public func usersWatching() -> Route<[User]> { + Route(paths: [path, "watching"], method: .GET, traktManager: traktManager) + } + + /** + Returns all videos including trailers, teasers, clips, and featurettes. + + ✨ Extended Info + */ + public func videos() -> Route<[TraktVideo]> { + Route(paths: [path, "videos"], method: .GET, traktManager: traktManager) + } + // MARK: - Resources public func episode(_ number: Int) -> EpisodeResource { diff --git a/Common/Wrapper/Resources/ShowResource.swift b/Common/Wrapper/Resources/ShowResource.swift index 2957b3d..f00a0ba 100644 --- a/Common/Wrapper/Resources/ShowResource.swift +++ b/Common/Wrapper/Resources/ShowResource.swift @@ -8,37 +8,350 @@ import Foundation -public struct ShowResource { +extension TraktManager { + /// Endpoints for shows in general + public struct ShowsResource { + private let traktManager: TraktManager - // MARK: - Properties + internal init(traktManager: TraktManager) { + self.traktManager = traktManager + } - public let id: CustomStringConvertible - public let traktManager: TraktManager + /** + Returns all shows being watched right now. Shows with the most users are returned first. + */ + public func trending() -> Route> { + Route(path: "shows/trending", method: .GET, traktManager: traktManager) + } - // MARK: - Lifecycle + /** + Returns the most popular shows. Popularity is calculated using the rating percentage and the number of ratings. + */ + public func popular() -> Route> { + Route(path: "shows/popular", method: .GET, traktManager: traktManager) + } - public init(id: CustomStringConvertible, traktManager: TraktManager = .sharedManager) { - self.id = id - self.traktManager = traktManager - } + /** + Returns the most favorited shows in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func favorited(period: Period? = nil) -> Route> { + Route(paths: ["shows/favorited", period], method: .GET, traktManager: traktManager) + } - // MARK: - Methods + /** + Returns the most played (a single user can watch multiple episodes multiple times) shows in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func played(period: Period? = nil) -> Route> { + Route(paths: ["shows/played", period], method: .GET, traktManager: traktManager) + } - public func summary() async throws -> Route { - try await traktManager.get("shows/\(id)") - } + /** + Returns the most watched (unique users) shows in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func watched(period: Period? = nil) -> Route> { + Route(paths: ["shows/watched", period], method: .GET, traktManager: traktManager) + } - public func aliases() async throws -> Route<[Alias]> { - Route(path: "shows/\(id)/aliases", method: .GET, traktManager: traktManager) - } + /** + Returns the most collected (unique users) shows in the specified time `period`, defaulting to `weekly`. All stats are relative to the specific time `period`. + */ + public func collected(period: Period? = nil) -> Route> { + Route(paths: ["shows/collected", period], method: .GET, traktManager: traktManager) + } + + /** + Returns the most anticipated shows based on the number of lists a show appears on. + */ + public func anticipated() -> Route> { + Route(path: "shows/anticipated", method: .GET, traktManager: traktManager) + } + + /** + Returns all shows updated since the specified date. We recommended storing the date you can be efficient using this method moving forward. + + 📄 Pagination + + > important: The `startDate` is only accurate to the hour, for caching purposes. Please drop the minutes and seconds from your timestamp to help optimize our cached data. For example, use `2021-07-17T12:00:00Z` and not `2021-07-17T12:23:34Z` + + > note: .The `startDate` can only be a maximum of 30 days in the past. + */ + public func recentlyUpdated(since startDate: Date) async throws -> Route> { + let formattedDate = startDate.dateString(withFormat: "yyyy-MM-dd'T'HH:mm:ss") + return Route(path: "shows/updates/\(formattedDate)", method: .GET, traktManager: traktManager) + } + + /** + Returns all show Trakt IDs updated since the specified UTC date and time. We recommended storing the X-Start-Date header you can be efficient using this method moving forward. By default, 10 results are returned. You can send a limit to get up to 100 results per page. + + 📄 Pagination - public func certifications() async throws -> Route { - Route(path: "shows/\(id)/certifications", method: .GET, traktManager: traktManager) + > important: The `startDate` is only accurate to the hour, for caching purposes. Please drop the minutes and seconds from your timestamp to help optimize our cached data. For example, use `2021-07-17T12:00:00Z` and not `2021-07-17T12:23:34Z` + + > note: .The `startDate` can only be a maximum of 30 days in the past. + */ + public func recentlyUpdatedIds(since startDate: Date) async throws -> Route> { + let formattedDate = startDate.dateString(withFormat: "yyyy-MM-dd'T'HH:mm:ss") + return Route(path: "shows/updates/id/\(formattedDate)", method: .GET, traktManager: traktManager) + } } - // MARK: - Resources + /// Endpoints for a specific series + public struct ShowResource { + + // MARK: - Properties + + /// Trakt ID, Trakt slug, or IMDB ID + internal let id: CustomStringConvertible + internal let path: String + + private let traktManager: TraktManager + + // MARK: - Lifecycle + + internal init(id: CustomStringConvertible, traktManager: TraktManager) { + self.id = id + self.traktManager = traktManager + self.path = "shows/\(id)" + } + + // MARK: - Methods + + /** + Returns a single shows's details. If you request extended info, the `airs` object is relative to the show's country. You can use the `day`, `time`, and `timezone` to construct your own date then convert it to whatever timezone your user is in. + */ + public func summary() -> Route { + Route(path: path, method: .GET, traktManager: traktManager) + } + + /** + Returns all title aliases for a show. Includes country where name is different. + */ + public func aliases() -> Route<[Alias]> { + Route(paths: [path, "aliases"], method: .GET, traktManager: traktManager) + } + + /** + Returns all content certifications for a show, including the country. + */ + public func certifications() -> Route { + Route(paths: [path, "certifications"], method: .GET, traktManager: traktManager) + } + + /** + Returns all translations for a show, including language and translated values for title and overview. + + - parameter language: 2 character language code Example: `es` + */ + public func translations(language: String? = nil) -> Route<[TraktShowTranslation]> { + Route(paths: [path, "translations", language], method: .GET, traktManager: traktManager) + } + + /** + Returns all top level comments for a show. By default, the `newest` comments are returned first. Other sorting options include `oldest`, most `likes`, most `replies`, `highest` rated, `lowest` rated, most `plays`, and highest `watched` percentage. + + 🔓 OAuth Optional 📄 Pagination 😁 Emojis + + > note: If you send OAuth, comments from blocked users will be automatically filtered out. + + - parameter sort: how to sort Example: `newest`. + - parameter authenticate: comments from blocked users will be automatically filtered out if `true`. + */ + public func comments(sort: String? = nil, authenticate: Bool = false) -> Route> { + Route(paths: [path, "comments", sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + /** + Returns all lists that contain this show. By default, `personal` lists are returned sorted by the most `popular`. + + 📄 Pagination 😁 Emojis + + - parameter type: Filter for a specific list type. Possible values: `all` , `personal` , `official` , `watchlists` , `favorites` . + - parameter sort: How to sort . Possible values: `popular` , `likes` , `comments` , `items` , `added` , `updated` . + */ + public func containingLists(type: String? = nil, sort: String? = nil) -> Route> { + Route(paths: [path, "lists", type, sort], method: .GET, traktManager: traktManager) + } + + // MARK: - Progress + + /** + Returns collection progress for a show including details on all aired seasons and episodes. The `next_episode` will be the next episode the user should collect, if there are no upcoming episodes it will be set to `null`. + + By default, any hidden seasons will be removed from the response and stats. To include these and adjust the completion stats, set the `hidden` flag to `true`. + + By default, specials will be excluded from the response. Set the `specials` flag to `true` to include season 0 and adjust the stats accordingly. If you'd like to include specials, but not adjust the stats, set `count_specials` to `false`. + + By default, the `last_episode` and `next_episode` are calculated using the last `aired` episode the user has collected, even if they've collected older episodes more recently. To use their last collected episode for these calculations, set the `last_activity` flag to `collected`. + + > note: Only aired episodes are used to calculate progress. Episodes in the future or without an air date are ignored. + + 🔒 OAuth Required + + - parameter includeHiddenSeasons: Include any hidden seasons + - parameter includeSpecials: Include specials as season 0 + - parameter progressCountsSpecials: Count specials in the overall stats (only applies if specials are included) + */ + public func collectedProgress(includeHiddenSeasons: Bool? = nil, includeSpecials: Bool? = nil, progressCountsSpecials: Bool? = nil) -> Route { + Route( + paths: [path, "progress/collection"], + queryItems: [ + "hidden": includeHiddenSeasons?.description, + "specials": includeSpecials?.description, + "count_specials": progressCountsSpecials?.description + ].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } - public func season(_ number: Int) -> SeasonResource { - SeasonResource(showId: id, seasonNumber: number, traktManager: traktManager) + /** + Returns watched progress for a show including details on all aired seasons and episodes. The `next_episode` will be the next episode the user should watch, if there are no upcoming episodes it will be set to `null`. If not `null`, the `reset_at` date is when the user started re-watching the show. Your app can adjust the progress by ignoring episodes with a `last_watched_at` prior to the `reset_at`. + + By default, any hidden seasons will be removed from the response and stats. To include these and adjust the completion stats, set the `hidden` flag to `true`. + + By default, specials will be excluded from the response. Set the `specials` flag to `true` to include season 0 and adjust the stats accordingly. If you'd like to include specials, but not adjust the stats, set `count_specials` to `false`. + + By default, the `last_episode` and `next_episode` are calculated using the last `aired` episode the user has watched, even if they've watched older episodes more recently. To use their last watched episode for these calculations, set the `last_activity` flag to watched. + + > note: Only aired episodes are used to calculate progress. Episodes in the future or without an air date are ignored. + + 🔒 OAuth Required + + - parameter includeHiddenSeasons: Include any hidden seasons + - parameter includeSpecials: Include specials as season 0 + - parameter progressCountsSpecials: Count specials in the overall stats (only applies if specials are included) + */ + public func watchedProgress(includeHiddenSeasons: Bool? = nil, includeSpecials: Bool? = nil, progressCountsSpecials: Bool? = nil) -> Route { + Route( + paths: [path, "progress/watched"], + queryItems: [ + "hidden": includeHiddenSeasons?.description, + "specials": includeSpecials?.description, + "count_specials": progressCountsSpecials?.description + ].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: Reset - VIP + + // MARK: - + + /** + Returns all `cast` and `crew` for a show. Each `cast` member will have a `characters` array and a standard `person` object.The `crew` object will be broken up by department into production, `art`, `crew`, `costume & make-up`, `directing`, `writing`, `sound`, `camera`, `visual effects`, `lighting`, and `editing` (if there are people for those crew positions). Each of those members will have a `jobs` array and a standard `person` object. + + **Guest Stars** + + If you add `?extended=guest_stars` to the URL, it will return all guest stars that appeared in at least 1 episode of the show. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + ✨ Extended Info + */ + public func people() -> Route> { + Route(paths: [path, "people"], method: .GET, traktManager: traktManager) + } + + /** + Returns rating (between 0 and 10) and distribution for a show. + */ + public func ratings() -> Route { + Route(paths: [path, "ratings"], method: .GET, traktManager: traktManager) + } + + /** + Returns related and similar shows. + + 📄 Pagination ✨ Extended Info + */ + public func relatedShows() -> Route> { + Route(paths: [path, "related"], method: .GET, traktManager: traktManager) + } + + /** + Returns lots of show stats. + */ + public func stats() -> Route { + Route(paths: [path, "stats"], method: .GET, traktManager: traktManager) + } + + /** + Returns all studios for a show. + */ + public func studios() -> Route<[TraktStudio]> { + Route(paths: [path, "studios"], method: .GET, traktManager: traktManager) + } + + /** + Returns all users watching this show right now. + + ✨ Extended Info + */ + public func usersWatching() -> Route<[User]> { + Route(paths: [path, "watching"], method: .GET, traktManager: traktManager) + } + + /** + Returns the next scheduled to air episode. If no episode is found, a `204` HTTP status code will be returned. + + ✨ Extended Info + */ + public func nextEpisode() -> Route { + Route(paths: [path, "next_episode"], method: .GET, traktManager: traktManager) + } + + /** + Returns the most recently aired episode. If no episode is found, a `204` HTTP status code will be returned. + + ✨ Extended Info + */ + public func lastEpisode() -> Route { + Route(paths: [path, "last_episode"], method: .GET, traktManager: traktManager) + } + + /** + Returns all videos including trailers, teasers, clips, and featurettes. + + ✨ Extended Info + */ + public func videos() -> Route<[TraktVideo]> { + Route(paths: [path, "videos"], method: .GET, traktManager: traktManager) + } + + /** + Queue this show for a full metadata and image refresh. It might take up to 8 hours for the updated metadata to be availabe through the API. + + > note: If this show is already queued, a `409` HTTP status code will returned. + + 🔥 VIP Only 🔒 OAuth Required + */ + public func refreshMetadata() -> EmptyRoute { + EmptyRoute(paths: [path, "refresh"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Returns all seasons for a show including the number of episodes in each season. + + **Episodes** + + If you add `?extended=episodes` to the URL, it will return all episodes for all seasons. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + ✨ Extended Info + */ + public func seasons() -> Route<[TraktSeason]> { + Route(paths: [path, "seasons"], method: .GET, traktManager: traktManager) + } + + // MARK: - Resources + + public func season(_ number: Int) -> SeasonResource { + SeasonResource(showId: id, seasonNumber: number, traktManager: traktManager) + } } + } diff --git a/Common/Wrapper/Resources/SyncResource.swift b/Common/Wrapper/Resources/SyncResource.swift new file mode 100644 index 0000000..203fe38 --- /dev/null +++ b/Common/Wrapper/Resources/SyncResource.swift @@ -0,0 +1,435 @@ +// +// SyncResource.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/1/25. +// + +import Foundation + +extension TraktManager { + /** + Syncing with trakt opens up quite a few cool features. Most importantly, trakt can serve as a cloud based backup for the data in your app. This is especially useful when rebuilding a media center or installing a mobile app on your new phone. It can also be nice to sync up multiple media centers with a central trakt account. If everything is in sync, your media can be managed from trakt and be reflected in your apps. + + **Media objects for syncing** + + As a baseline, all add and remove sync methods accept arrays of `movies`, `shows`, and `episodes`. Each of these top level array elements should themselves be an array of standard `movie`, `show`, or `episode` objects. Full examples are in the intro section called **Standard Media Objects**. Keep in mind that `episode` objects really only need the `ids` so it can find an exact match. This is useful for absolute ordered shows. Some methods also have optional metadata you can attach, so check the docs for each specific method. + + Media objects will be matched by ID first, then fall back to title and year. IDs will be matched in this order `trakt`, `imdb`, `tmdb`, `tvdb`, and `slug`. If nothing is found, it will match on the `title` and `year`. If still nothing, it would use just the `title` (or `name` for people) and find the most current object that exists. + + --- + **Watched History Sync** + + This is a 2 way sync that will get items from trakt to sync locally, plus find anything new and sync back to trakt. Perform this sync on startup or at set intervals (i.e. once every day) to keep everything in sync. This will only send data to trakt and not remove it. + + --- + **Collection Sync** + + It's very handy to have a snapshot on trakt of everything you have available to watch locally. Syncing your local connection will do just that. This will only send data to trakt and not remove it. + + --- + **Clean Collection** + + Cleaning a collection involves comparing the trakt collection to what exists locally. This will remove items from the trakt collection if they don't exist locally anymore. You should make this clear to the user that data might be removed from trakt. + */ + public struct SyncResource { + + private let path = "sync" + private let traktManager: TraktManager + + internal init(traktManager: TraktManager) { + self.traktManager = traktManager + } + + // MARK: - Routes + + /** + This method is a useful first step in the syncing process. We recommended caching these dates locally, then you can compare to know exactly what data has changed recently. This can greatly optimize your syncs so you don't pull down a ton of data only to see nothing has actually changed. + + **Account** + + `settings_at` is set when the OAuth user updates any of their Trakt settings on the website. `followed_at` is set when another Trakt user follows or unfollows the OAuth user. `following_at` is set when the OAuth user follows or unfollows another Trakt user. `pending_at` is set when the OAuth user follows a private account, which requires their approval. `requested_at` is set when the OAuth user has a private account and someone requests to follow them. + + 🔒 OAuth Required + */ + public func lastActivities() -> Route { + Route(paths: [path, "last_activities"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + // MARK: - Playback + + public func playback(type: String) -> Route<[PlaybackProgress]> { + Route(paths: [path, "playback", type], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Remove a playback item from a user's playback progress list. A `404` will be returned if the `id` is invalid. + */ + public func removePlayback(id: CustomStringConvertible) -> EmptyRoute { + EmptyRoute(paths: [path, "playback", id], method: .DELETE, requiresAuthentication: true, traktManager: traktManager) + } + + // MARK: - Collections + + /** + Get all collected items in a user's collection. A collected item indicates availability to watch digitally or on physical media. + + Each `movie` object contains `collected_at` and `updated_at` timestamps. Since users can set custom dates when they collected movies, it is possible for `collected_at` to be in the past. We also include `updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movie if you see a newer timestamp. + + Each `show` object contains `last_collected_at` and `last_updated_at` timestamps. Since users can set custom dates when they collected episodes, it is possible for `last_collected_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the show if you see a newer timestamp. + + If you add `?extended=metadata` to the URL, it will return the additional `media_type`, `resolution`, `hdr`, `audio`, `audio_channels` and `3d` metadata. It will use `null` if the metadata isn't set for an item. + + 🔒 OAuth Required ✨ Extended Info + + - parameter type: Possible values: `movies` , `shows` . + */ + public func collection(type: String) -> Route<[TraktCollectedItem]> { + Route(paths: [path, "collection", type], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Add items to a user's collection. Accepts shows, seasons, episodes and movies. If only a show is passed, all episodes for the show will be collected. If seasons are specified, all episodes in those seasons will be collected. + + Send a `collected_at` UTC datetime to mark items as collected in the past. You can also send additional metadata about the media itself to have a very accurate collection. Showcase what is available to watch from your epic HD DVD collection down to your downloaded iTunes movies. + + **Limits** + + If the user's collection limit is exceeded, a `420` HTTP error code is returned. Use the `/users/settings` method to get all limits for a user account. In most cases, upgrading to Trakt VIP will increase the limits. + + > note: You can resend items already in your collection and they will be updated with any new values. This includes the `collected_at` and any other metadata. + */ + public func addToCollection(movies: [CollectionId]? = nil, shows: [CollectionId]? = nil, seasons: [CollectionId]? = nil, episodes: [CollectionId]? = nil) -> Route { + Route( + paths: [path, "collection"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove one or more items from a user's collection. + + - parameter movies: Array of movie Trakt ids + - parameter shows: Array of show Trakt ids + - parameter seasons: Array of season Trakt ids + - parameter episodes: Array of episode Trakt ids + + 🔒 OAuth Required + */ + public func removeFromCollection(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil) -> Route { + Route( + paths: [path, "collection", "remove"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Watched + + /** + Returns all movies or shows a user has watched sorted by most plays. + + If type is set to `shows` and you add `?extended=noseasons` to the URL, it won't return season or episode info. + + Each `movie` and `show` object contains `last_watched_at` and `last_updated_at` timestamps. Since users can set custom dates when they watched movies and episodes, it is possible for `last_watched_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movies and shows if you see a newer timestamp. + + Each show object contains a `reset_at` timestamp. If not `null`, this is when the user started re-watching the show. Your app can adjust the progress by ignoring episodes with a `last_watched_at` prior to the `reset_at`. + + 🔒 OAuth Required ✨ Extended Info + + - parameter type: Possible values: `movies` , `shows` . + */ + private func watched(type: String) -> Route<[T]> { + Route(paths: [path, "watched", type], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Returns all shows a user has watched sorted by most plays. + + You add `?extended=noseasons` to the URL, it won't return season or episode info. + + Each `show` object contains `last_watched_at` and `last_updated_at` timestamps. Since users can set custom dates when they watched episodes, it is possible for `last_watched_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movies and shows if you see a newer timestamp. + + Each `show` object contains a `reset_at` timestamp. If not `null`, this is when the user started re-watching the show. Your app can adjust the progress by ignoring episodes with a `last_watched_at` prior to the `reset_at`. + + 🔒 OAuth Required ✨ Extended Info + */ + public func watchedShows() -> Route<[TraktWatchedShow]> { + watched(type: "shows") + } + + /** + Returns all movies a user has watched sorted by most plays. + + Each `movie` object contains `last_watched_at` and `last_updated_at` timestamps. Since users can set custom dates when they watched movies, it is possible for `last_watched_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movies and shows if you see a newer timestamp. + + 🔒 OAuth Required ✨ Extended Info + */ + public func watchedMovies() -> Route<[TraktWatchedMovie]> { + watched(type: "movies") + } + + // MARK: - History + + /** + Returns movies and episodes that a user has watched, sorted by most recent. You can optionally limit the `type` to `movies` or `episodes`. The `id` (64-bit integer) in each history item uniquely identifies the event and can be used to remove individual events by using the `/sync/history/remove` method. The `action` will be set to `scrobble`, `checkin`, or `watch`. + + Specify a `type` and trakt `id` to limit the history for just that item. If the `id` is valid, but there is no history, an empty array will be returned. + + 🔒 OAuth Required 📄 Pagination ✨ Extended Info + + - Parameters: + - type: Possible values: `movies` , `shows` , `seasons` , `episodes`. + - mediaId: Trakt ID for a specific item. + - startingAt: Starting date. + - endingAt: Ending date. + */ + public func history( + type: String? = nil, + mediaId: CustomStringConvertible? = nil, + startingAt: Date? = nil, + endingAt: Date? = nil + ) -> Route> { + Route( + paths: [path, "history", type, mediaId], + queryItems: [ + "start_at": startingAt?.UTCDateString(), + "end_at": endingAt?.UTCDateString() + ].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Add items to a user's watch history. Accepts shows, seasons, episodes and movies. If only a show is passed, all episodes for the show will be added. If seasons are specified, only episodes in those seasons will be added. + + Send a `watched_at` UTC datetime to mark items as watched in the past. This is useful for syncing past watches from a media center. + + > important: Please be careful with sending duplicate data. We don't verify the `item` + `watched_at` to ensure it's unique, it is up to your app to veify this and not send duplicate plays. + + 🔒 OAuth Required + + - parameters: + - movies: array of `movie` objects + - shows: array of `show` objects + - seasons: array of `season` objects + - episodes: array of `episode` objects + */ + public func addToHistory( + movies: [AddToHistoryId]? = nil, + shows: [AddToHistoryId]? = nil, + seasons: [AddToHistoryId]? = nil, + episodes: [AddToHistoryId]? = nil + ) -> Route { + Route( + paths: [path, "history"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove items from a user's watch history including all watches, scrobbles, and checkins. Accepts shows, seasons, episodes and movies. If only a show is passed, all episodes for the show will be removed. If seasons are specified, only episodes in those seasons will be removed. + + You can also send a list of raw history `ids` (64-bit integers) to delete single plays from the watched history. The ``TraktManager/SyncResource/history(type:mediaId:startingAt:endingAt:)`` method will return an individual `id` (64-bit integer) for each history item. + + 🔒 OAuth Required + */ + public func removeFromHistory( + movies: [SyncId]? = nil, + shows: [SyncId]? = nil, + seasons: [SyncId]? = nil, + episodes: [SyncId]? = nil, + historyIDs: [Int]? = nil + ) -> Route { + Route( + paths: [path, "history", "remove"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, ids: historyIDs), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Ratings + + /** + Get a user's ratings filtered by `type`. You can optionally filter for a specific `rating` between 1 and 10. Send a comma separated string for `rating` if you need multiple ratings. + + 🔒 OAuth Required 📄 Pagination Optional ✨ Extended Info + */ + public func ratings(type: String? = nil, rating: CustomStringConvertible? = nil) -> Route> { + Route( + paths: [path, "ratings", type, rating], + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Rate one or more items. Accepts shows, seasons, episodes and movies. If only a show is passed, only the show itself will be rated. If seasons are specified, all of those seasons will be rated. + + Send a ``RatingId/ratedAt`` datetime to mark items as rated in the past. This is useful for syncing ratings from a media center. + + 🔒 OAuth Required + + - parameters: + - movies: Array of movie Trakt ids + - shows: Array of show Trakt ids + - seasons: Array of season Trakt ids + - episodes: Array of episode Trakt ids + */ + public func addRatings( + movies: [RatingId]? = nil, + shows: [RatingId]? = nil, + seasons: [RatingId]? = nil, + episodes: [RatingId]? = nil + ) -> Route { + Route( + paths: [path, "ratings"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove ratings for one or more items. + + 🔒 OAuth Required + */ + public func removeRatings( + movies: [SyncId]? = nil, + shows: [SyncId]? = nil, + seasons: [SyncId]? = nil, + episodes: [SyncId]? = nil + ) -> Route { + Route( + paths: [path, "ratings", "remove"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Watchlist + + /** + Returns all items in a user's watchlist filtered by type. + + > Important: The watchlist should not be used as a list of what the user is actively watching. Use a combination of the ``TraktManager/SyncResource/watchedShows()`` and ``TraktManager/ShowResource/watchedProgress(includeHiddenSeasons:includeSpecials:progressCountsSpecials:)`` methods to get what the user is actively watching. + + --- + **Notes** + + Each watchlist item contains a `notes` field with text entered by the user. + + --- + **Sorting Headers** + + By default, all list items are sorted by `rank` `asc`. We send `X-Applied-Sort-By` and `X-Applied-Sort-How` headers to indicate how the results are actually being sorted. + + We also send `X-Sort-By` and `X-Sort-How` headers which indicate the user's sort preference. Use these to custom sort the watchlist **in your app** for more advanced sort abilities we can't do in the API. Values for `X-Sort-By` include `rank`, `added`, `title`, `released`, `runtime`, `popularity`, `percentage`, and `votes`. Values for `X-Sort-How` include `asc` and `desc`. + + --- + **Auto Removal** + + When an item is watched, it will be automatically removed from the watchlist. For shows and seasons, watching 1 episode will remove the entire show or season. + + 🔒 OAuth Required 📄 Pagination Optional ✨ Extended Info 😁 Emojis + + - parameters: + - type: Filter for a specific item type. Possible values: `movies` , `shows` , `seasons` , `episodes` . + - sort: How to sort (only if type is also sent). Possible values: `rank` , `added` , `released` , `title` . + */ + public func watchlist(type: String? = nil, sort: String? = nil) -> Route> { + Route( + paths: [path, "watchlist", type, sort], + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Update the watchlist by sending 1 or more parameters. + + 🔒 OAuth Required + */ + public func updateWatchlist(description: String? = nil, sortBy: String? = nil, sortHow: String? = nil) -> Route { + Route( + paths: [path, "watchlist"], + body: WatchlistUpdate(description: description, sortBy: sortBy, sortHow: sortHow), + method: .PUT, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Add one of more items to a user's watchlist. Accepts shows, seasons, episodes and movies. If only a show is passed, only the show itself will be added. If seasons are specified, all of those seasons will be added. + + --- + **Notes** + + Each watchlist item can optionally accept a `notes` (500 maximum characters) field with custom text. The user must be a Trakt VIP to send `notes`. + + --- + **Limits** + + If the user's watchlist limit is exceeded, a `420` HTTP error code is returned. Use the ``TraktManager/CurrentUserResource/settings()`` method to get all limits for a user account. In most cases, upgrading to Trakt VIP will increase the limits. + + 🔥 VIP Enhanced 🔒 OAuth Required 😁 Emojis + + - parameters: + - movies: Array of movie Trakt ids + - shows: Array of show Trakt ids + - seasons: Array of season Trakt ids + - episodes: Array of episode Trakt ids + */ + public func addToWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil) -> Route { + Route( + paths: [path, "watchlist"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove one or more items from a user's watchlist. + + 🔒 OAuth Required + + - parameters: + - movies: Array of movie Trakt ids + - shows: Array of show Trakt ids + - seasons: Array of season Trakt ids + - episodes: Array of episode Trakt ids + */ + public func removeFromWatchlist(movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil) -> Route { + Route( + paths: [path, "watchlist", "remove"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Favorites + } +} diff --git a/Common/Wrapper/Resources/TraktManager+AsyncAwait.swift b/Common/Wrapper/Resources/TraktManager+AsyncAwait.swift deleted file mode 100644 index 706e2fc..0000000 --- a/Common/Wrapper/Resources/TraktManager+AsyncAwait.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// TraktManager+AsyncAwait.swift -// TraktKit -// -// Created by Maxamilian Litteral on 6/14/21. -// Copyright © 2021 Maximilian Litteral. All rights reserved. -// - -import Foundation - -extension TraktManager { - func `get`(_ path: String, authorized: Bool = false, resultType: T.Type = T.self) async throws -> Route { - Route(path: path, method: .GET, requiresAuthentication: authorized, traktManager: self) - } -} diff --git a/Common/Wrapper/Resources/TraktManager+Resources.swift b/Common/Wrapper/Resources/TraktManager+Resources.swift index 238596f..d3bae53 100644 --- a/Common/Wrapper/Resources/TraktManager+Resources.swift +++ b/Common/Wrapper/Resources/TraktManager+Resources.swift @@ -10,6 +10,42 @@ import Foundation extension TraktManager { + // MARK: - Authentication + + public func auth() -> AuthenticationResource { + AuthenticationResource(traktManager: self) + } + + // MARK: - Checkin + + public func checkin() -> CheckinResource { + CheckinResource(traktManager: self) + } + + // MARK: - Search + + public func search() -> SearchResource { + SearchResource(traktManager: self) + } + + // MARK: - Movies + + public var movies: MoviesResource { + MoviesResource(traktManager: self) + } + + /// - parameter id: Trakt ID, Trakt slug, or IMDB ID + public func movie(id: CustomStringConvertible) -> MovieResource { + MovieResource(id: id, traktManager: self) + } + + // MARK: - TV + + public var shows: ShowsResource { + ShowsResource(traktManager: self) + } + + /// - parameter id: Trakt ID, Trakt slug, or IMDB ID public func show(id: CustomStringConvertible) -> ShowResource { ShowResource(id: id, traktManager: self) } @@ -21,16 +57,18 @@ extension TraktManager { public func episode(showId: CustomStringConvertible, season: Int, episode: Int) -> EpisodeResource { EpisodeResource(showId: showId, seasonNumber: season, episodeNumber: episode, traktManager: self) } - - public func currentUser() -> CurrentUserResource { - CurrentUserResource() + + public func sync() -> SyncResource { + SyncResource(traktManager: self) } - - public func user(_ username: String) -> UsersResource { - UsersResource(username: username, traktManager: self) + + // MARK: - User + + public func currentUser() -> CurrentUserResource { + CurrentUserResource(traktManager: self) } - public func search() -> SearchResource { - SearchResource(traktManager: self) + public func user(_ slug: String) -> UsersResource { + UsersResource(slug: slug, traktManager: self) } } diff --git a/Common/Wrapper/Resources/UserResource.swift b/Common/Wrapper/Resources/UserResource.swift index 8991d37..101111f 100644 --- a/Common/Wrapper/Resources/UserResource.swift +++ b/Common/Wrapper/Resources/UserResource.swift @@ -8,44 +8,597 @@ import Foundation extension TraktManager { - /// Resource for authenticated user + /// Resource containing all of the `/user` endpoints that **require** authentication. These requests will always be for the current authenticated user, and cannot be performed for another user. public struct CurrentUserResource { - public let traktManager: TraktManager - - init(traktManager: TraktManager = .sharedManager) { + + static let currentUserSlug = "me" + + private let traktManager: TraktManager + private let path: String = "users" + + internal init(traktManager: TraktManager) { self.traktManager = traktManager } - + // MARK: - Methods - public func settings() async throws -> Route { - try await traktManager.get("users/settings", authorized: true) + public func settings() -> Route { + Route(paths: [path, "settings"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + // MARK: Following Requests + + /// List a user's pending following requests that they're waiting for the other user's to approve. + public func getPendingFollowingRequests() -> Route<[FollowRequest]> { + Route(paths: [path, "requests", "following"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /// List a user's pending follow requests so they can either approve or deny them. + public func getFollowerRequests() -> Route<[FollowRequest]> { + Route(paths: [path, "requests"], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Approve a follower using the id of the request. If the id is not found, was already approved, or was already denied, a 404 error will be returned. + + 🔒 OAuth Required + */ + public func approveFollowRequest(id: Int) -> Route { + Route(paths: [path, "requests", id], method: .POST, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Deny a follower using the id of the request. If the id is not found, was already approved, or was already denied, a 404 error will be returned. + + 🔒 OAuth Required + */ + public func denyFollowRequest(id: Int) -> EmptyRoute { + EmptyRoute(paths: [path, "requests", id], method: .DELETE, requiresAuthentication: true, traktManager: traktManager) + } + + /** + Get all saved filters a user has created. The path and query can be used to construct an API path to retrieve the saved data. Think of this like a dynamically updated list. + + 🔥 VIP Only + 🔒 OAuth Required + 📄 Pagination + */ + public func savedFilters(for section: String? = nil) -> Route> { + Route(paths: [path, "saved_filters", section], method: .GET, requiresAuthentication: true, traktManager: traktManager) + } + + // MARK: - Hidden + + /** + Get hidden items for a section. This will return an array of standard media objects. You can optionally limit the type of results to return. + + 🔒 OAuth Required 📄 Pagination ✨ Extended Info + */ + public func hiddenItems(for section: String, type: String? = nil) -> Route> { + Route( + paths: [path, "hidden", section], + queryItems: ["type": type].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Hide items for a specific section. Here's what type of items can hidden for each section. + + 🔒 OAuth Required + */ + public func hide(movies: [SyncId] = [], shows: [SyncId] = [], seasons: [SyncId] = [], users: [SyncId] = [], in section: String) -> Route { + Route( + paths: [path, "hidden", section], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, users: users), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Unhide items for a specific section. Here's what type of items can unhidden for each section. + + 🔒 OAuth Required + */ + public func unhide(movies: [SyncId] = [], shows: [SyncId] = [], seasons: [SyncId] = [], users: [SyncId] = [], in section: String) -> Route { + Route( + paths: [path, "hidden", section, "remove"], + body: TraktMediaBody(movies: movies, shows: shows, seasons: seasons, users: users), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Profile + + /** + Get a user's profile information. If the user is private, info will only be returned if you send OAuth and are either that user or an approved follower. Adding `?extended=vip` will return some additional VIP related fields so you can display the user's Trakt VIP status and year count. + + 🔓 OAuth Required ✨ Extended Info + */ + public func profile() -> Route { + UsersResource(slug: Self.currentUserSlug, traktManager: traktManager).profile() + } + + // MARK: - Lists + + /** + Create a new personal list. The `name` is the only required field, but the other info is recommended to ask for. + + --- + **Limits** + + If the user's list limit is exceeded, a ``TraktManager/TraktError/accountLimitExceeded`` error is thrown. Use the ``TraktManager/CurrentUserResource/settings()`` method to get all limits for a user account. In most cases, upgrading to Trakt VIP will increase the limits. + + 🔥 VIP Enhanced 🔒 OAuth Required + */ + public func createPersonalList(_ body: TraktNewList) -> Route { + Route( + paths: [path, Self.currentUserSlug, "lists"], + body: body, + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Reorder all lists by sending the updated rank of list ids. Use the /users/:id/lists method to get all list ids. + + 🔒 OAuth Required + */ + public func reorderLists(_ rank: [Int]) -> Route { + struct ReorderBody: TraktObject { + let rank: [Int] + } + + return Route( + paths: [path, Self.currentUserSlug, "lists", "reorder"], + body: ReorderBody(rank: rank), + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - List + + /** + Returns a single personal list. Use the ``TraktManager/UsersResource/itemsOnList(_:type:)`` method to get the actual items this list contains. + + 🔓 OAuth Optional 😁 Emojis + + - parameter listId: Trakt ID or Trakt slug + */ + public func personalList(_ listId: CustomStringConvertible) -> Route { + UsersResource(slug: Self.currentUserSlug, traktManager: traktManager).personalList(listId) + } + + /** + Update a personal list by sending 1 or more parameters. If you update the list name, the original slug will still be retained so existing references to this list won't break. + + 🔒 OAuth Required + */ + public func updatePersonalList(_ listId: CustomStringConvertible, changes: TraktUpdateList) -> Route { + Route( + paths: [path, Self.currentUserSlug, "lists", listId], + body: changes, + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove a personal list and all items it contains. + + 🔒 OAuth Required + */ + public func deletePersonalList(_ listId: CustomStringConvertible) -> EmptyRoute { + EmptyRoute( + paths: [path, Self.currentUserSlug, "lists", listId], + method: .DELETE, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - List Items + + /** + Get all items on a personal list. Items can be a `movie`, `show`, `season`, `episode`, or `person`. You can optionally specify the type parameter with a single value or comma delimited string for multiple item types. + + **Notes** + + Each list item contains a `notes` field with text entered by the user. + + **Sorting** + + Default sorting is based on the list defaults and sent in the `X-Sort-By` and `X-Sort-How` headers. If you specify the `sort_by` and `sort_how` parameters, the response will be sorted based on those values and sent in the `X-Applied-Sort-By` and` X-Applied-Sort-How` headers. + + Some sort_by options are 🔥 **VIP Only** including `imdb_rating`, `tmdb_rating`, `rt_tomatometer`, `rt_audience`, `metascore`, `votes`, `imdb_votes`, and `tmdb_votes`. If sent for a non VIP, the items will fall back to `rank`. + + 🔥 VIP Enhanced 🔓 OAuth Optional 📄 Pagination Optional ✨ Extended Info 😁 Emojis + + - parameters: + - listId: Trakt ID or Trakt slug + - type: Filter for a specific item type. Possible values: `movie` , `show` , `season` , `episode` , `person` . + - sortBy: Sort by a specific property. Possible values: `rank` , `added` , `title` , `released` , `runtime` , `popularity` , `random` , `percentage` , `imdb_rating` , `tmdb_rating` , `rt_tomatometer` , `rt_audience` , `metascore` , `votes` , `imdb_votes` , `tmdb_votes` , `my_rating` , `watched` , `collected` . + - sortHow: Sort direction. Possible values: `asc` , `desc` . + */ + public func itemsOnList(_ listId: CustomStringConvertible, type: ListItemType? = nil, sortBy: String? = nil, sortHow: String? = nil) -> Route> { + UsersResource(slug: Self.currentUserSlug, traktManager: traktManager).itemsOnList(listId, type: type, sortBy: sortBy, sortHow: sortHow) + } + + /** + Add one or more items to a personal list. Items can be movies, shows, seasons, episodes, or people. + + **Notes** + + Each list item can optionally accept a `notes` (500 maximum characters) field with custom text. The user must be a Trakt VIP to send notes. + + **Limits** + + If the user's list item limit is exceeded, a `420` HTTP error code is returned. Use the /users/settings method to get all limits for a user account. In most cases, upgrading to Trakt VIP will increase the limits. + + 🔥 VIP Enhanced 🔒 OAuth Required 😁 Emojis + */ + public func addItemsToList(_ listId: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil) -> Route { + UsersResource(slug: Self.currentUserSlug, traktManager: traktManager).addItemsToList(listId, movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) + } + + /** + Remove one or more items from a personal list. + + 🔒 OAuth Required + */ + public func removeItemsFromList(_ listId: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil) -> Route { + UsersResource(slug: Self.currentUserSlug, traktManager: traktManager).removeItemsFromList(listId, movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) } + } - - /// Resource for /Users/id + + /// Resource containing all of the `/user/*` endpoints where authentication is **optional** or **not** required. public struct UsersResource { - - public let username: String - public let traktManager: TraktManager - - init(username: String, traktManager: TraktManager = .sharedManager) { - self.username = username + public let slug: String + private let path: String + private let authenticate: Bool + private let traktManager: TraktManager + + internal init(slug: String, authenticate: Bool = false, traktManager: TraktManager) { + self.slug = slug + self.path = "users/\(slug)" + self.authenticate = slug == "me" ? true : authenticate self.traktManager = traktManager } // MARK: - Methods - - public func lists() async throws -> Route<[TraktList]> { - try await traktManager.get("users/\(username)/lists") + + /** + Get a user's profile information. If the user is private, info will only be returned if you send OAuth and are either that user or an approved follower. Adding `?extended=vip` will return some additional VIP related fields so you can display the user's Trakt VIP status and year count. + + 🔓 OAuth Optional ✨ Extended Info + */ + public func profile() -> Route { + Route(paths: [path], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) } - - public func itemsOnList(_ listId: String, type: ListItemType? = nil) async throws -> Route<[TraktListItem]> { - if let type = type { - return try await traktManager.get("users/\(username)/lists/\(listId)/items/\(type.rawValue)") - } else { - return try await traktManager.get("users/\(username)/lists/\(listId)/items") - } + + // MARK: - Likes + + /** + Get items a user likes. This will return an array of standard media objects. You can optionally limit the `type` of results to return. + + **Comment Media Objects** + + If you add `?extended=comments` to the URL, it will return media objects for each comment like. + + > note: This returns a lot of data, so please only use this extended parameter if you actually need it! + + 🔒 OAuth Optional 📄 Pagination + */ + public func likes(type: String? = nil) -> Route> { + Route(paths: [path, "likes", type], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + // MARK: - Collection + + /** + Get all collected items in a user's collection. A collected item indicates availability to watch digitally or on physical media. + + Each `movie` object contains `collected_at` and `updated_at` timestamps. Since users can set custom dates when they collected movies, it is possible for `collected_at` to be in the past. We also include `updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movie if you see a newer timestamp. + + Each show object contains `last_collected_at` and `last_updated_at` timestamps. Since users can set custom dates when they collected episodes, it is possible for `last_collected_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the show if you see a newer timestamp. + + If you add `?extended=metadata` to the URL, it will return the additional `media_type`, `resolution`, `hdr`, `audio`, `audio_channels` and `3d` metadata. It will use `null` if the metadata isn't set for an item. + + 🔓 OAuth Optional ✨ Extended Info + + - parameter type: `movies` or `shows` + */ + public func collection(type: String) -> Route<[TraktCollectedItem]> { + Route(paths: [path, "collection", type], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + // MARK: - Comments + + /** + Returns the most recently written comments for the user. You can optionally filter by the `comment_type` and media `type` to limit what gets returned. + + By default, only top level comments are returned. Set `?include_replies=true` to return replies in addition to top level comments. Set `?include_replies=only` to return only replies and no top level comments. + + 🔓 OAuth Optional 📄 Pagination ✨ Extended Info + */ + public func comments(commentType: String? = nil, mediaType: String? = nil, includeReplies: String? = nil) -> Route> { + Route( + paths: [path, "comments", commentType, mediaType], + queryItems: ["include_replies": includeReplies].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + // MARK: - Notes + + // MARK: - Lists + + /** + Returns all personal lists for a user. Use the /users/:id/lists/:list_id/items method to get the actual items a specific list contains. + + 🔓 OAuth Optional 😁 Emojis + */ + public func lists() -> Route<[TraktList]> { + Route(paths: [path, "lists"], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + // MARK: - Collaborations + + // MARK: - List + + /** + Returns a single personal list. Use the ``TraktManager/UsersResource/itemsOnList(_:type:)`` method to get the actual items this list contains. + + 🔓 OAuth Optional 😁 Emojis + + - parameter listId: Trakt ID or Trakt slug + */ + public func personalList(_ listId: CustomStringConvertible) -> Route { + Route( + paths: [path, "lists", listId], + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + /** + Get all items on a personal list. Items can be a `movie`, `show`, `season`, `episode`, or `person`. You can optionally specify the type parameter with a single value or comma delimited string for multiple item types. + + **Notes** + + Each list item contains a `notes` field with text entered by the user. + + **Sorting** + + Default sorting is based on the list defaults and sent in the `X-Sort-By` and `X-Sort-How` headers. If you specify the `sort_by` and `sort_how` parameters, the response will be sorted based on those values and sent in the `X-Applied-Sort-By` and` X-Applied-Sort-How` headers. + + Some sort_by options are 🔥 **VIP Only** including `imdb_rating`, `tmdb_rating`, `rt_tomatometer`, `rt_audience`, `metascore`, `votes`, `imdb_votes`, and `tmdb_votes`. If sent for a non VIP, the items will fall back to `rank`. + + 🔥 VIP Enhanced 🔓 OAuth Optional 📄 Pagination Optional ✨ Extended Info 😁 Emojis + + - parameters: + - listId: Trakt ID or Trakt slug + - type: Filter for a specific item type. Possible values: `movie` , `show` , `season` , `episode` , `person` . + - sortBy: Sort by a specific property. Possible values: `rank` , `added` , `title` , `released` , `runtime` , `popularity` , `random` , `percentage` , `imdb_rating` , `tmdb_rating` , `rt_tomatometer` , `rt_audience` , `metascore` , `votes` , `imdb_votes` , `tmdb_votes` , `my_rating` , `watched` , `collected` . + - sortHow: Sort direction. Possible values: `asc` , `desc` . + */ + public func itemsOnList(_ listId: CustomStringConvertible, type: ListItemType? = nil, sortBy: String? = nil, sortHow: String? = nil) -> Route> { + Route( + paths: [path, "lists", listId, "items", type?.rawValue, sortBy, sortHow], + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + /** + Add one or more items to a personal list. Items can be movies, shows, seasons, episodes, or people. + + **Notes** + + Each list item can optionally accept a `notes` (500 maximum characters) field with custom text. The user must be a Trakt VIP to send notes. + + **Limits** + + If the user's list item limit is exceeded, a `420` HTTP error code is returned. Use the /users/settings method to get all limits for a user account. In most cases, upgrading to Trakt VIP will increase the limits. + + 🔥 VIP Enhanced 🔒 OAuth Required 😁 Emojis + */ + public func addItemsToList(_ listId: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil) -> Route { + let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) + return Route( + paths: [path, "lists", listId, "items"], + body: body, + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + /** + Remove one or more items from a personal list. + + 🔒 OAuth Required + */ + public func removeItemsFromList(_ listId: CustomStringConvertible, movies: [SyncId]? = nil, shows: [SyncId]? = nil, seasons: [SyncId]? = nil, episodes: [SyncId]? = nil, people: [SyncId]? = nil) -> Route { + let body = TraktMediaBody(movies: movies, shows: shows, seasons: seasons, episodes: episodes, people: people) + return Route( + paths: [path, "lists", listId, "items", "remove"], + body: body, + method: .POST, + requiresAuthentication: true, + traktManager: traktManager + ) + } + + // MARK: - Follow + + // MARK: - Followers + + // MARK: - Following + + // MARK: - Friends + + // MARK: - History + + /** + Returns movies and episodes that a user has watched, sorted by most recent. You can optionally limit the `type` to `movies` or `episodes`. The `id` (64-bit integer) in each history item uniquely identifies the event and can be used to remove individual events by using the `/sync/history/remove` method. The `action` will be set to `scrobble`, `checkin`, or `watch`. + + Specify a `type` and trakt `item_id` to limit the history for just that item. If the `item_id` is valid, but there is no history, an empty array will be returned. + + - parameters: + - type: Possible values: `movies` , `shows` , `seasons` , `episodes`. + - mediaId: Trakt ID for a specific item. + - startingAt: Starting date. + - endingAt: Ending date. + */ + public func watchedHistory( + type: String? = nil, + mediaId: CustomStringConvertible? = nil, + startingAt: Date? = nil, + endingAt: Date? = nil + ) -> Route> { + Route( + paths: [path, "history", type, mediaId], + queryItems: [ + "start_at": startingAt?.UTCDateString(), + "end_at": endingAt?.UTCDateString() + ].compactMapValues { $0 }, + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + // MARK: - Ratings + + /** + Get a user's ratings filtered by `type`. You can optionally filter for a specific `rating` between 1 and 10. Send a comma separated string for `rating` if you need multiple ratings. + + 🔓 OAuth Optional 📄 Pagination Optional ✨ Extended Info + + - parameters: + - type: Possible values: `movies` , `shows` , `seasons` , `episodes` , `all` . + - rating: Filter for a specific rating. Possible values: 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 . + */ + public func ratings(type: String? = nil, rating: CustomStringConvertible? = nil) -> Route> { + Route( + paths: [path, "ratings", type, rating], + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + // MARK: - Watchlist + + /** + Returns all items in a user's watchlist filtered by type. + + --- + **Notes** + + Each watchlist item contains a `notes` field with text entered by the user. + + --- + **Sorting Headers** + + By default, all list items are sorted by `rank` `asc`. We send `X-Applied-Sort-By` and `X-Applied-Sort-How` headers to indicate how the results are actually being sorted. + + We also send `X-Sort-By` and `X-Sort-How` headers which indicate the user's sort preference. Use these to to custom sort the watchlist in your app for more advanced sort abilities we can't do in the API. Values for `X-Sort-By` include `rank`, `added`, `title`, `released`, `runtime`, `popularity`, `percentage`, and `votes`. Values for `X-Sort-How` include `asc` and `desc`. + + --- + **Auto Removal** + + When an item is watched, it will be automatically removed from the watchlist. For shows and seasons, watching 1 episode will remove the entire show or season. + + --- + **The watchlist should not be used as a list of what the user is actively watching.** + + Use a combination of the ``TraktManager/SyncResource/watchedShows()``, ``TraktManager/SyncResource/watchedMovies()`` and ``TraktManager/ShowResource/watchedProgress(includeHiddenSeasons:includeSpecials:progressCountsSpecials:)`` methods to get what the user is actively watching. + + 🔓 OAuth Optional 📄 Pagination Optional ✨ Extended Info 😁 Emojis + + - parameters: + - type: Filter for a specific item type. Possible values: `movies` , `shows` , `seasons` , `episodes` . + - sort: How to sort (only if type is also sent). Possible values: `rank` , `added` , `released` , `title` . + */ + public func watchlist(type: String? = nil, sort: String? = nil) -> Route> { + Route(paths: [path, "watchlist", type, sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + /** + Returns all top level comments for the watchlist. By default, the `newest` comments are returned first. Other sorting options include `oldest`, most `likes`, and most `replies`. + + > note: If you send OAuth, comments from blocked users will be automatically filtered out. + + 🔓 OAuth Optional 📄 Pagination 😁 Emojis + + - parameter sort: How to sort. Possible values: `newest` , `oldest` , `likes` , `replies` . + */ + public func watchlistComments(sort: String? = nil) -> Route> { + Route(paths: [path, "watchlist", "comments", sort], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + // MARK: - Favorites + + // MARK: - Watching + + /** + Returns a movie or episode if the user is currently watching something. If they are not, it returns no data and a `204` HTTP status code. + + 🔓 OAuth Optional ✨ Extended Info + */ + public func watching() -> Route { + Route(paths: [path, "watching"], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) + } + + // MARK: - Watched + + /** + Returns all movies or shows a user has watched sorted by most plays. + + If type is set to `shows` and you add `?extended=noseasons` to the URL, it won't return season or episode info. + + Each `movie` and `show` object contains `last_watched_at` and `last_updated_at` timestamps. Since users can set custom dates when they watched movies and episodes, it is possible for `last_watched_at` to be in the past. We also include `last_updated_at` to help sync Trakt data with your app. Cache this timestamp locally and only re-process the movies and shows if you see a newer timestamp. + + Each `show` object contains a `reset_at` timestamp. If not null, this is when the user started re-watching the show. Your app can adjust the progress by ignoring episodes with a `last_watched_at` prior to the `reset_at`. + + 🔓 OAuth Optional ✨ Extended Info + */ + public func watched(type: String) -> Route<[TraktWatchedItem]> { + Route( + paths: [path, "watched", type], + method: .GET, + requiresAuthentication: authenticate, + traktManager: traktManager + ) + } + + // MARK: - Stats + + /** + Returns stats about the movies, shows, and episodes a user has watched, collected, and rated. + + 🔓 OAuth Optional + */ + public func stats() -> Route { + Route(paths: [path, "stats"], method: .GET, requiresAuthentication: authenticate, traktManager: traktManager) } } } diff --git a/Common/Wrapper/Route.swift b/Common/Wrapper/Route.swift index 6acfebb..df83995 100644 --- a/Common/Wrapper/Route.swift +++ b/Common/Wrapper/Route.swift @@ -8,105 +8,383 @@ import Foundation -public class Route { - public let path: String - public let method: Method +public struct Route: Sendable { + + // MARK: - Properties + private let resultType: T.Type - public let traktManager: TraktManager - public let requiresAuthentication: Bool + private let traktManager: TraktManager + + internal var path: String + internal let method: Method + internal let requiresAuthentication: Bool + internal var queryItems: [String: String] + + private var extended = [ExtendedType]() + private var page: Int? + private var limit: Int? - private var extended: [ExtendedType] = [] - private var _page: Int? - private var _limit: Int? - private var filters = [FilterType]() private var searchType: SearchType? private var searchQuery: String? - private var request: URLRequest { - var query: [String: String] = [:] + private var body: (any EncodableTraktObject)? + + // MARK: - Lifecycle + + public init( + path: String, + queryItems: [String: String] = [:], + body: (any TraktObject)? = nil, + method: Method, + requiresAuthentication: Bool = false, + resultType: T.Type = T.self, + traktManager: TraktManager + ) { + self.path = path + self.queryItems = queryItems + self.body = body + self.method = method + self.requiresAuthentication = requiresAuthentication + self.resultType = resultType + self.traktManager = traktManager + } + + public init( + paths: [CustomStringConvertible?], + queryItems: [String: String] = [:], + body: (any EncodableTraktObject)? = nil, + method: Method, + requiresAuthentication: Bool = false, + resultType: T.Type = T.self, + traktManager: TraktManager + ) { + self.path = paths.compactMap { $0?.description }.joined(separator: "/") + self.queryItems = queryItems + self.body = body + self.method = method + self.requiresAuthentication = requiresAuthentication + self.resultType = resultType + self.traktManager = traktManager + } + + // MARK: - Actions + + public func extend(_ extended: ExtendedType...) -> Self { + var copy = self + copy.extended = extended + return copy + } + + // MARK: - Pagination + + public func page(_ page: Int?) -> Self { + var copy = self + copy.page = page + return copy + } + + public func limit(_ limit: Int?) -> Self { + var copy = self + copy.limit = limit + return copy + } + + // MARK: - Filters + + public func filter(_ filter: TraktManager.Filter) -> Self { + var copy = self + copy.filters.append(filter) + return copy + } + + public func type(_ type: SearchType?) -> Self { + var copy = self + copy.searchType = type + return copy + } + + // MARK: - Search + + public func query(_ query: String?) -> Self { + var copy = self + copy.searchQuery = query + return copy + } + + // MARK: - Perform + + public func perform(retryLimit: Int = 3) async throws -> T { + let request = try createRequest() + return try await traktManager.perform(request: request, retryLimit: retryLimit) + } + + private func createRequest() throws -> URLRequest { + var query: [String: String] = queryItems if !extended.isEmpty { query["extended"] = extended.queryString() } // pagination - if let page = _page { + if let page { query["page"] = page.description } - if let limit = _limit { + if let limit { query["limit"] = limit.description } - + + // Search + if let searchType { query["type"] = searchType.rawValue } - + if let searchQuery { query["query"] = searchQuery } - + // Filters - if filters.isEmpty == false { + if !filters.isEmpty { for (key, value) in (filters.map { $0.value() }) { query[key] = value } } - return traktManager.mutableRequest(forPath: path, - withQuery: query, - isAuthorized: requiresAuthentication, - withHTTPMethod: method)! + return try traktManager.mutableRequest( + forPath: path, + withQuery: query, + isAuthorized: requiresAuthentication, + withHTTPMethod: method, + body: body + ) } +} + +// MARK: - No data response + +public struct EmptyRoute: Sendable { + private let traktManager: TraktManager + + internal var path: String + internal let method: Method + internal let requiresAuthentication: Bool - public init(path: String, method: Method, requiresAuthentication: Bool = false, traktManager: TraktManager, resultType: T.Type = T.self) { + private var body: (any EncodableTraktObject)? + + // MARK: - Lifecycle + + public init( + path: String, + body: (any TraktObject)? = nil, + method: Method, + requiresAuthentication: Bool = false, + traktManager: TraktManager + ) { self.path = path + self.body = body self.method = method self.requiresAuthentication = requiresAuthentication - self.resultType = resultType self.traktManager = traktManager } - public func extend(_ extended: ExtendedType...) -> Self { - self.extended = extended - return self + public init( + paths: [CustomStringConvertible?], + body: (any TraktObject)? = nil, + method: Method, + requiresAuthentication: Bool = false, + traktManager: TraktManager + ) { + self.path = paths.compactMap { $0?.description }.joined(separator: "/") + self.body = body + self.method = method + self.requiresAuthentication = requiresAuthentication + self.traktManager = traktManager } - - // MARK: - Pagination - public func page(_ page: Int?) -> Self { - self._page = page - return self + // MARK: - Perform + + public func perform(retryLimit: Int = 3) async throws { + let request = try traktManager.mutableRequest( + forPath: path, + withQuery: [:], + isAuthorized: requiresAuthentication, + withHTTPMethod: method, + body: body + ) + let _ = try await traktManager.fetchData(request: request, retryLimit: retryLimit) } +} - public func limit(_ limit: Int?) -> Self { - self._limit = limit - return self +// MARK: - Pagination + +public protocol PagedObjectProtocol { + static var objectType: Decodable.Type { get } + static func createPagedObject(with object: Decodable, currentPage: Int, pageCount: Int) -> Self +} + +public struct PagedObject: PagedObjectProtocol, TraktObject { + public let object: TraktModel + public let currentPage: Int + public let pageCount: Int + + public static var objectType: any Decodable.Type { + TraktModel.self } - - // MARK: - Filters - - public func filter(_ filter: TraktManager.Filter) -> Self { - filters.append(filter) - return self + + public static func createPagedObject(with object: Decodable, currentPage: Int, pageCount: Int) -> Self { + return PagedObject(object: object as! TraktModel, currentPage: currentPage, pageCount: pageCount) } - - public func type(_ type: SearchType?) -> Self { - searchType = type - return self +} + +extension Route where T: PagedObjectProtocol { + + /// Fetches all pages for a paginated endpoint, and returns the data in a Set. + public func fetchAllPages(maxConcurrentRequests preferredMaxConcurrentRequests: Int = 10) async throws -> Set where T.Type == PagedObject<[Element]>.Type { + // Fetch first page + let firstPage = try await self.page(1).perform() + var resultSet = Set(firstPage.object) + + // Return early if there's only one page + guard firstPage.pageCount > 1 else { return resultSet } + resultSet = await withTaskGroup(of: [Element].self, returning: Set.self) { group in + let maxConcurrentRequests = min(firstPage.pageCount - 1, preferredMaxConcurrentRequests) + let pages = 2...firstPage.pageCount + + let indexStream = AsyncStream { continuation in + for i in pages { + continuation.yield(i) + } + continuation.finish() + } + var indexIterator = indexStream.makeAsyncIterator() + var results = resultSet + + // Start with the initial batch of tasks + for _ in 0.. + if let index = await indexIterator.next() { + group.addTask { + (try? await self.page(index).perform())?.object ?? [] + } + } + } + + return results + } + + return resultSet } - - // MARK: - Search - - public func query(_ query: String?) -> Self { - searchQuery = query - return self + + /// Stream paged results one at a time + public func pagedResults(maxConcurrentRequests preferredMaxConcurrentRequests: Int = 10) -> AsyncThrowingStream<[Element], Error> where T.Type == PagedObject<[Element]>.Type { + AsyncThrowingStream { continuation in + let task = Task { + do { + // Fetch first page + let firstPage = try await self.page(1).perform() + continuation.yield(firstPage.object) + + guard firstPage.pageCount > 1 else { + continuation.finish() + return + } + + // Use a semaphore to limit concurrency + let semaphore = AsyncSemaphore(value: preferredMaxConcurrentRequests) + let pages = 2...firstPage.pageCount + + try await withThrowingTaskGroup(of: (Int, [Element]).self) { group in + for pageIndex in pages { + // Acquire permit before starting new task + try await semaphore.acquire() + + group.addTask { + do { + let pageResult = try await self.page(pageIndex).perform().object + await semaphore.release() + return (pageIndex, pageResult) + } catch { + await semaphore.release() + throw error + } + } + } + + // Process results in order + var nextPage = 2 + var buffer = [Int: [Element]]() + + while let result = try await group.next() { + let (pageIndex, items) = result + + if pageIndex == nextPage { + // We got the next page we need + continuation.yield(items) + nextPage += 1 + + // Check if we have any buffered pages that can be yielded now + while let bufferedItems = buffer[nextPage] { + continuation.yield(bufferedItems) + buffer[nextPage] = nil + nextPage += 1 + } + } else { + // Buffer out-of-order results + buffer[pageIndex] = items + } + } + } + + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + + continuation.onTermination = { _ in + task.cancel() + } + } } - - // MARK: - Perform +} + +// A simple AsyncSemaphore implementation +actor AsyncSemaphore { + private var value: Int + private var waiters: [CheckedContinuation] = [] + + init(value: Int) { + self.value = value + } + + func acquire() async throws { + if value > 0 { + value -= 1 + return + } - public func perform() async throws -> T { - try await traktManager.perform(request: request) + try await withCheckedThrowingContinuation { continuation in + waiters.append(continuation) + } + } + + func release() { + if let waiter = waiters.first { + waiters.removeFirst() + waiter.resume() + } else { + value += 1 + } } } diff --git a/Common/Wrapper/TraktManager.swift b/Common/Wrapper/TraktManager.swift index 195c648..bb132fc 100644 --- a/Common/Wrapper/TraktManager.swift +++ b/Common/Wrapper/TraktManager.swift @@ -7,194 +7,169 @@ // import Foundation +import os public extension Notification.Name { static let TraktAccountStatusDidChange = Notification.Name(rawValue: "signedInToTrakt") } -public class TraktManager { - - // TODO List: - // 1. Create a limit object, double check every paginated API call is marked as paginated - // 2. Call completion with custom error when creating request fails - - // MARK: - Properties - - private enum Constants { - static let tokenExpirationDefaultsKey = "accessTokenExpirationDate" - static let oneMonth: TimeInterval = 2629800 +public typealias TraktObject = Codable & Hashable & Sendable +public typealias EncodableTraktObject = Encodable & Hashable & Sendable + +public final class TraktManager: Sendable { + + // MARK: - Types + + /// Errors related to the operation of TraktKit. + public enum TraktKitError: Error { + case missingClientInfo + case malformedURL + case userNotAuthorized + case couldNotParseData + + // TODO: Move this enum + case invalidRefreshToken } - + + /// Possible errors thrown when polling for an access code. + public enum TraktTokenError: Error { + // 404 + case invalidDeviceCode + // 409 + case alreadyUsed + // 410 + case expired + // 418 + case denied + // 429 + case tooManyRequests + case unexpectedStatusCode + case missingAccessCode + } + + // MARK: - Properties + + static let logger = Logger(subsystem: "TraktKit", category: "TraktManager") + // MARK: Internal - private var staging: Bool? - private var clientID: String? - private var clientSecret: String? - private var redirectURI: String? - private var baseURL: String? - private var APIBaseURL: String? - private var isWaitingToToken: Bool = false - let jsonEncoder: JSONEncoder = { + private let staging: Bool + internal let clientId: String + internal let clientSecret: String + internal let redirectURI: String + private let apiHost: String + + private let authStorage: any TraktAuthentication + + private let authStateLock = NSLock() + nonisolated(unsafe) + private var cachedAuthState: AuthenticationState? + + internal static let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 return encoder }() - - // Keys - let accessTokenKey = "accessToken" - let refreshTokenKey = "refreshToken" - - let session: URLSessionProtocol - - public lazy var explore: ExploreResource = ExploreResource(traktManager: self) - + + let session: URLSession + // MARK: Public - public static let sharedManager = TraktManager() - + public var isSignedIn: Bool { get { - return accessToken != nil - } - } - public var oauthURL: URL? - - private var _accessToken: String? - public var accessToken: String? { - get { - if _accessToken != nil { - return _accessToken - } - if let accessTokenData = MLKeychain.loadData(forKey: accessTokenKey) { - if let accessTokenString = String(data: accessTokenData, encoding: .utf8) { - _accessToken = accessTokenString - return accessTokenString - } - } - - return nil - } - set { - // Save somewhere secure - _accessToken = newValue - if newValue == nil { - // Remove from keychain - MLKeychain.deleteItem(forKey: accessTokenKey) - } else { - // Save to keychain - let succeeded = MLKeychain.setString(value: newValue!, forKey: accessTokenKey) - #if DEBUG - print("Saved access token: \(succeeded)") - #endif - } + authStateLock.lock() + defer { authStateLock.unlock() } + return cachedAuthState != nil } } - - private var _refreshToken: String? - public var refreshToken: String? { - get { - if _refreshToken != nil { - return _refreshToken - } - if let refreshTokenData = MLKeychain.loadData(forKey: refreshTokenKey) { - if let refreshTokenString = String.init(data: refreshTokenData, encoding: .utf8) { - _refreshToken = refreshTokenString - return refreshTokenString - } - } - - return nil - } - set { - // Save somewhere secure - _refreshToken = newValue - if newValue == nil { - // Remove from keychain - MLKeychain.deleteItem(forKey: refreshTokenKey) - } else { - // Save to keychain - let succeeded = MLKeychain.setString(value: newValue!, forKey: refreshTokenKey) - #if DEBUG - print("Saved refresh token: \(succeeded)") - #endif - } - } + + public var oauthURL: URL? { + var urlComponents = URLComponents() + urlComponents.scheme = "https" + urlComponents.host = staging ? "staging.trakt.tv" : "trakt.tv" + urlComponents.path = "/oauth/authorize" + urlComponents.queryItems = [ + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "client_id", value: clientId), + URLQueryItem(name: "redirect_uri", value: redirectURI), + ] + return urlComponents.url } - + // MARK: - Lifecycle - - public init(session: URLSessionProtocol = URLSession(configuration: .default)) { + + /** + Initializes the TraktManager. + + > important: Call `refreshCurrentAuthState` shortly after initializing `TraktManager`, otherwise authenticated calls will fail. + */ + public init( + session: URLSession = URLSession(configuration: .default), + staging: Bool = false, + clientId: String, + clientSecret: String, + redirectURI: String, + authStorage: any TraktAuthentication = KeychainTraktAuthentication() + ) { self.session = session - } - - // MARK: - Setup - - public func set(clientID: String, clientSecret secret: String, redirectURI: String, staging: Bool = false) { - self.clientID = clientID - self.clientSecret = secret - self.redirectURI = redirectURI self.staging = staging - - self.baseURL = !staging ? "trakt.tv" : "staging.trakt.tv" - self.APIBaseURL = !staging ? "api.trakt.tv" : "api-staging.trakt.tv" - self.oauthURL = URL(string: "https://\(baseURL!)/oauth/authorize?response_type=code&client_id=\(clientID)&redirect_uri=\(redirectURI)") + self.clientId = clientId + self.clientSecret = clientSecret + self.redirectURI = redirectURI + self.apiHost = staging ? "api-staging.trakt.tv" : "api.trakt.tv" + self.authStorage = authStorage } - - internal func createErrorWithStatusCode(_ statusCode: Int) -> NSError { - let message: String - - if let traktMessage = StatusCodes.message(for: statusCode) { - message = traktMessage - } else { - message = "Request Failed: Gateway timed out (\(statusCode))" - } - - let userInfo = [ - "title": "Error", - NSLocalizedDescriptionKey: message, - NSLocalizedFailureReasonErrorKey: "", - NSLocalizedRecoverySuggestionErrorKey: "" - ] - let TraktKitIncorrectStatusError = NSError(domain: "com.litteral.TraktKit", code: statusCode, userInfo: userInfo) - - return TraktKitIncorrectStatusError + + /// Initialize TraktManager and refreshes the auth state + public init( + session: URLSession = URLSession(configuration: .default), + staging: Bool = false, + clientId: String, + clientSecret: String, + redirectURI: String, + authStorage: any TraktAuthentication = KeychainTraktAuthentication() + ) async { + self.session = session + self.staging = staging + self.clientId = clientId + self.clientSecret = clientSecret + self.redirectURI = redirectURI + self.apiHost = staging ? "api-staging.trakt.tv" : "api.trakt.tv" + self.authStorage = authStorage + + try? await refreshCurrentAuthState() } - + // MARK: - Actions - - public func signOut() { - accessToken = nil - refreshToken = nil - UserDefaults.standard.removeObject(forKey: Constants.tokenExpirationDefaultsKey) - } - - internal func mutableRequestForURL(_ url: URL?, authorization: Bool, HTTPMethod: Method) -> URLRequest? { - guard - let url = url else { return nil } - var request = URLRequest(url: url) - request.httpMethod = HTTPMethod.rawValue - - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("2", forHTTPHeaderField: "trakt-api-version") - if let clientID = clientID { - request.addValue(clientID, forHTTPHeaderField: "trakt-api-key") + + /** + Gets the current authentication state from the authentication storage, and caches the result to make requests. + You should only have to call this once shortly after initializing the `TraktManager`, unless you use the async initializer, which calls this function automatically. + */ + public func refreshCurrentAuthState() async throws(AuthenticationError) { + let currentState = try await authStorage.getCurrentState() + authStateLock.withLock { + cachedAuthState = currentState } - - if authorization { - if let accessToken = accessToken { - request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") - } - else { - return nil - } + } + + public func signOut() async { + await authStorage.clear() + authStateLock.withLock { + cachedAuthState = nil } - - return request } - - internal func mutableRequest(forPath path: String, withQuery query: [String: String], isAuthorized authorized: Bool, withHTTPMethod httpMethod: Method) -> URLRequest? { - guard let apiBaseURL = APIBaseURL else { preconditionFailure("Call `set(clientID:clientSecret:redirectURI:staging:)` before making any API requests") } - let urlString = "https://\(apiBaseURL)/" + path - guard var components = URLComponents(string: urlString) else { return nil } - + + internal func mutableRequest( + forPath path: String, + withQuery query: [String: String] = [:], + isAuthorized authorized: Bool, + withHTTPMethod httpMethod: Method, + body: Encodable? = nil + ) throws -> URLRequest { + // Build URL + let urlString = "https://\(apiHost)/" + path + guard var components = URLComponents(string: urlString) else { throw TraktKitError.malformedURL } + if query.isEmpty == false { var queryItems: [URLQueryItem] = [] for (key, value) in query { @@ -202,417 +177,147 @@ public class TraktManager { } components.queryItems = queryItems } - - guard let url = components.url else { return nil } + + guard let url = components.url else { throw TraktKitError.malformedURL } + + // Request var request = URLRequest(url: url) request.httpMethod = httpMethod.rawValue - request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - + + // Headers request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("2", forHTTPHeaderField: "trakt-api-version") - if let clientID = clientID { - request.addValue(clientID, forHTTPHeaderField: "trakt-api-key") - } + request.addValue(clientId, forHTTPHeaderField: "trakt-api-key") if authorized { - if let accessToken = accessToken { + if let accessToken = cachedAuthState?.accessToken { request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } else { + throw TraktKitError.userNotAuthorized } } - - return request - } - - func post(_ path: String, query: [String: String] = [:], body: Body) -> URLRequest? { - guard let apiBaseURL = APIBaseURL else { preconditionFailure("Call `set(clientID:clientSecret:redirectURI:staging:)` before making any API requests") } - let urlString = "https://\(apiBaseURL)/" + path - guard var components = URLComponents(string: urlString) else { return nil } - if query.isEmpty == false { - var queryItems: [URLQueryItem] = [] - for (key, value) in query { - queryItems.append(URLQueryItem(name: key, value: value)) - } - components.queryItems = queryItems - } - - guard let url = components.url else { return nil } - var request = URLRequest(url: url) - request.httpMethod = Method.POST.rawValue - - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("2", forHTTPHeaderField: "trakt-api-version") - if let clientID = clientID { - request.addValue(clientID, forHTTPHeaderField: "trakt-api-key") - } - - if let accessToken = accessToken { - request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") - } - - do { - request.httpBody = try jsonEncoder.encode(body) - } catch { - return nil + + // Body + if let body { + request.httpBody = try Self.jsonEncoder.encode(body) } + return request } + func post(_ path: String, query: [String: String] = [:], body: Body) throws -> URLRequest { + try mutableRequest(forPath: path, withQuery: query, isAuthorized: true, withHTTPMethod: .POST, body: body) + } + // MARK: - Authentication - - public func getTokenFromAuthorizationCode(code: String, completionHandler: SuccessCompletionHandler?) throws { - guard - let clientID = clientID, - let clientSecret = clientSecret, - let redirectURI = redirectURI - else { - completionHandler?(.fail) - return - } - - let urlString = "https://\(baseURL!)/oauth/token" - let url = URL(string: urlString) - guard var request = mutableRequestForURL(url, authorization: false, HTTPMethod: .POST) else { - completionHandler?(.fail) - return - } - - let json = [ - "code": code, - "client_id": clientID, - "client_secret": clientSecret, - "redirect_uri": redirectURI, - "grant_type": "authorization_code", - ] - request.httpBody = try JSONSerialization.data(withJSONObject: json, options: []) - - session._dataTask(with: request) { [weak self] data, response, error in - guard - let welf = self else { return } - guard error == nil else { - completionHandler?(.fail) - return - } - - // Check response - guard - let HTTPResponse = response as? HTTPURLResponse, - HTTPResponse.statusCode == StatusCodes.Success else { - completionHandler?(.fail) - return - } - - // Check data - guard - let data = data else { - completionHandler?(.fail) - return - } - - do { - let decoder = JSONDecoder() - let authenticationInfo = try decoder.decode(AuthenticationInfo.self, from: data) - #if DEBUG - print(authenticationInfo) - print("[\(#function)] Access token is \(String(describing: welf.accessToken))") - print("[\(#function)] Refresh token is \(String(describing: welf.refreshToken))") - #endif - - welf.accessToken = authenticationInfo.accessToken - welf.refreshToken = authenticationInfo.refreshToken - // Save expiration date - let expiresDate = Date(timeIntervalSinceNow: authenticationInfo.expiresIn) - UserDefaults.standard.set(expiresDate, forKey: Constants.tokenExpirationDefaultsKey) - - // Post notification - DispatchQueue.main.async { - NotificationCenter.default.post(name: .TraktAccountStatusDidChange, object: nil) - } - - completionHandler?(.success) - } catch { - completionHandler?(.fail) - } - }.resume() + + public func getToken(authorizationCode code: String) async throws -> AuthenticationInfo { + let authenticationInfo = try await auth().getAccessToken(for: code).perform() + await saveCredentials(for: authenticationInfo, postAccountStatusChange: true) + return authenticationInfo } - - public func getAppCode(completionHandler: @escaping (_ result: DeviceCode?) -> Void) { - guard let clientID = clientID else { - completionHandler(nil) - return - } - let urlString = "https://\(APIBaseURL!)/oauth/device/code/" - - let url = URL(string: urlString) - guard var request = mutableRequestForURL(url, authorization: false, HTTPMethod: .POST) else { - completionHandler(nil) - return - } - - let json = ["client_id": clientID] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: json, options: []) - - session._dataTask(with: request) { data, response, error in - guard error == nil else { - completionHandler(nil) - return - } - - // Check response - guard - let HTTPResponse = response as? HTTPURLResponse, - HTTPResponse.statusCode == StatusCodes.Success - else { - completionHandler(nil) - return - } - - // Check data - guard let data = data else { - completionHandler(nil) - return - } - do { - let deviceCode = try JSONDecoder().decode(DeviceCode.self, from: data) - completionHandler(deviceCode) - } catch { - completionHandler(nil) - } - }.resume() - } catch { - completionHandler(nil) - } + + // MARK: - Authentication - Devices + + /** + Generate new codes to start the device authentication process. The `device_code` and interval will be used later to poll for the `access_token`. The `user_code` and `verification_url` should be presented to the user as mentioned in the flow steps above. + + **QR Code** + + You might consider generating a QR code for the user to easily scan on their mobile device. The QR code should be a URL that redirects to the `verification_url` and appends the `user_code`. For example, `https://trakt.tv/activate/5055CC52` would load the Trakt hosted `verification_url` and pre-fill in the `user_code`. + */ + public func getAppCode() async throws -> DeviceCode { + try await auth().generateDeviceCode().perform() } - - public func getTokenFromDevice(code: DeviceCode?, completionHandler: ProgressCompletionHandler?) { - guard - let clientID = self.clientID, - let clientSecret = self.clientSecret, - let deviceCode = code - else { - completionHandler?(.fail(0)) - return - } - - let urlString = "https://\(APIBaseURL!)/oauth/device/token" - let url = URL(string: urlString) - guard var request = mutableRequestForURL(url, authorization: false, HTTPMethod: .POST) else { - completionHandler?(.fail(0)) - return - } - - let json = [ - "code": deviceCode.deviceCode, - "client_id": clientID, - "client_secret": clientSecret, - ] - guard let body = try? JSONSerialization.data(withJSONObject: json, options: []) else { - completionHandler?(.fail(0)) - return - } - request.httpBody = body - self.isWaitingToToken = true - - DispatchQueue.global().async { - var i = 1 - while self.isWaitingToToken { - if i >= deviceCode.expiresIn { - self.isWaitingToToken = false - continue - } - self.send(request: request, count: i) { result in - completionHandler?(result) - switch result { - case .success: - self.isWaitingToToken = false - case .fail(let progress): - if progress == 0 { - self.isWaitingToToken = false - } - } + + public func pollForAccessToken(deviceCode: DeviceCode) async throws { + let startTime = Date() + + while true { + let (tokenResponse, statusCode) = try await auth().requestAccessToken(code: deviceCode.deviceCode) + + switch statusCode { + case 200: + if let tokenResponse { + await saveCredentials(for: tokenResponse, postAccountStatusChange: true) + return } - i += 1 - sleep(1) - } - } - } - - private func send(request: URLRequest, count: Int, completionHandler: ProgressCompletionHandler?) { - session._dataTask(with: request) { [weak self] data, response, error in - guard let self = self else { return } - guard error == nil else { - completionHandler?(.fail(0)) - return - } - - // Check response - if let HTTPResponse = response as? HTTPURLResponse, - HTTPResponse.statusCode == StatusCodes.BadRequest { - completionHandler?(.fail(count)) - return - } - - guard let HTTPResponse = response as? HTTPURLResponse, - HTTPResponse.statusCode == StatusCodes.Success else { - completionHandler?(.fail(0)) - return - } - - // Check data - guard let data = data else { - completionHandler?(.fail(0)) - return + throw TraktTokenError.missingAccessCode + + case 400: + // Pending - continue polling + break + case 404: throw TraktTokenError.invalidDeviceCode + case 409: throw TraktTokenError.alreadyUsed + case 410: throw TraktTokenError.expired + case 418: throw TraktTokenError.denied + case 429: + // Too many requests - wait before polling again + try await Task.sleep(for: .seconds(10)) + continue + default: + throw TraktTokenError.unexpectedStatusCode } - - do { - if let accessTokenDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject] { - self.saveCredentials(accessTokenDict) - completionHandler?(.success) - } - } catch { - completionHandler?(.fail(0)) + + // Stop polling if `expires_in` time has elapsed + if Date().timeIntervalSince(startTime) >= deviceCode.expiresIn { + throw TraktTokenError.expired } - }.resume() - } - - private func saveCredentials(_ credentials: [String: AnyObject]) { - self.accessToken = credentials["access_token"] as? String - self.refreshToken = credentials["refresh_token"] as? String - - #if DEBUG - print("[\(#function)] Access token is \(String(describing: self.accessToken))") - print("[\(#function)] Refresh token is \(String(describing: self.refreshToken))") - #endif - - // Save expiration date - let timeInterval = credentials["expires_in"] as! NSNumber - let expiresDate = Date(timeIntervalSinceNow: timeInterval.doubleValue) - - UserDefaults.standard.set(expiresDate, forKey: "accessTokenExpirationDate") - UserDefaults.standard.synchronize() - - // Post notification - DispatchQueue.main.async { - NotificationCenter.default.post(name: .TraktAccountStatusDidChange, object: nil) + + try await Task.sleep(for: .seconds(deviceCode.interval)) } } - - // MARK: Refresh access token - - public enum RefreshState { - case noTokens, validTokens, refreshTokens, expiredTokens - } - - public enum RefreshTokenError: Error { - case missingRefreshToken, invalidRequest, invalidRefreshToken, unsuccessfulNetworkResponse(Int), missingData, expiredTokens - } - - /// Returns the local token state. This could be wrong if a user revokes application access from Trakt.tv - public var refreshState: RefreshState { - guard let expiredDate = UserDefaults.standard.object(forKey: Constants.tokenExpirationDefaultsKey) as? Date else { - return .noTokens - } - let refreshDate = expiredDate.addingTimeInterval(-Constants.oneMonth) - let now = Date() - - if now >= expiredDate { - return .expiredTokens + + // TODO: Find replacement for posting `TraktAccountStatusDidChange` to alert apps of account change. + private func saveCredentials(for authInfo: AuthenticationInfo, postAccountStatusChange: Bool = false) async { + let expiresDate = Date(timeIntervalSince1970: authInfo.createdAt).addingTimeInterval(authInfo.expiresIn) + + let authenticationState = AuthenticationState( + accessToken: authInfo.accessToken, + refreshToken: authInfo.refreshToken, + expirationDate: expiresDate + ) + await authStorage.updateState(authenticationState) + authStateLock.withLock { + cachedAuthState = authenticationState } - - if now >= refreshDate { - return .refreshTokens + + // Post notification + if postAccountStatusChange { + await MainActor.run { + NotificationCenter.default.post(name: .TraktAccountStatusDidChange, object: nil) + } } - - return .validTokens } - - public func checkToRefresh(completion: @escaping (_ result: Swift.Result) -> Void) { - switch refreshState { - case .refreshTokens: - do { - try getAccessTokenFromRefreshToken(completionHandler: completion) - } catch { - completion(.failure(error)) + + // MARK: Refresh access token + + public func checkToRefresh() async throws { + do throws(AuthenticationError) { + let currentState = try await authStorage.getCurrentState() + authStateLock.withLock { + cachedAuthState = currentState } - case .expiredTokens: - completion(.failure(RefreshTokenError.expiredTokens)) - default: - completion(.success(())) + } catch .tokenExpired(let refreshToken) { + try await refreshAccessToken(with: refreshToken) + } catch .noStoredCredentials { + throw TraktKitError.userNotAuthorized } } - - public func getAccessTokenFromRefreshToken(completionHandler: @escaping (_ result: Swift.Result) -> Void) throws { - guard - let clientID = clientID, - let clientSecret = clientSecret, - let redirectURI = redirectURI, - let rToken = refreshToken - else { - completionHandler(.failure(RefreshTokenError.missingRefreshToken)) - return - } - - let urlString = "https://\(baseURL!)/oauth/token" - let url = URL(string: urlString) - guard var request = mutableRequestForURL(url, authorization: false, HTTPMethod: .POST) else { - completionHandler(.failure(RefreshTokenError.invalidRequest)) - return + + /** + Use the `refresh_token` to get a new `access_token` without asking the user to re-authenticate. The `access_token` is valid for 24 hours before it needs to be refreshed again. + */ + @discardableResult + private func refreshAccessToken(with refreshToken: String) async throws -> AuthenticationInfo { + do { + let authenticationInfo = try await auth().getAccessToken(from: refreshToken).perform() + await saveCredentials(for: authenticationInfo) + return authenticationInfo + } catch TraktError.unauthorized { // 401 - Invalid refresh token + throw TraktKitError.invalidRefreshToken + } catch { + throw error } - - let json = [ - "refresh_token": rToken, - "client_id": clientID, - "client_secret": clientSecret, - "redirect_uri": redirectURI, - "grant_type": "refresh_token", - ] - request.httpBody = try JSONSerialization.data(withJSONObject: json, options: []) - - session._dataTask(with: request) { [weak self] (data, response, error) -> Void in - guard let welf = self else { return } - if let error = error { - completionHandler(.failure(error)) - return - } - - // Check response - guard let HTTPResponse = response as? HTTPURLResponse else { return } - - switch HTTPResponse.statusCode { - case 401: - completionHandler(.failure(RefreshTokenError.invalidRefreshToken)) - case 200...299: // Success - break - default: - completionHandler(.failure(RefreshTokenError.unsuccessfulNetworkResponse(HTTPResponse.statusCode))) - } - - // Check data - guard let data = data else { - completionHandler(.failure(RefreshTokenError.missingData)) - return - } - - do { - let decoder = JSONDecoder() - let authenticationInfo = try decoder.decode(AuthenticationInfo.self, from: data) - #if DEBUG - print(authenticationInfo) - print("[\(#function)] Access token is \(String(describing: welf.accessToken))") - print("[\(#function)] Refresh token is \(String(describing: welf.refreshToken))") - #endif - - welf.accessToken = authenticationInfo.accessToken - welf.refreshToken = authenticationInfo.refreshToken - // Save expiration date - let expiresDate = Date(timeIntervalSinceNow: authenticationInfo.expiresIn) - UserDefaults.standard.set(expiresDate, forKey: Constants.tokenExpirationDefaultsKey) - UserDefaults.standard.synchronize() - - completionHandler(.success(())) - } catch { - completionHandler(.failure(error)) - } - }.resume() } } diff --git a/Common/Wrapper/URLSessionProtocol.swift b/Common/Wrapper/URLSessionProtocol.swift deleted file mode 100644 index cd181e3..0000000 --- a/Common/Wrapper/URLSessionProtocol.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// URLSessionProtocol.swift -// TraktKit -// -// Created by Maximilian Litteral on 3/11/18. -// Copyright © 2018 Maximilian Litteral. All rights reserved. -// - -import Foundation - -public protocol URLSessionProtocol { - typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void - - func _dataTask(with request: URLRequest, completion: @escaping DataTaskResult) -> URLSessionDataTaskProtocol - func data(for request: URLRequest) async throws -> (Data, URLResponse) -} - -public protocol URLSessionDataTaskProtocol { - func resume() - func cancel() -} - -// MARK: Conform to protocols - -extension URLSession: URLSessionProtocol { - public func _dataTask(with request: URLRequest, completion: @escaping DataTaskResult) -> URLSessionDataTaskProtocol { - dataTask(with: request, completionHandler: completion) as URLSessionDataTaskProtocol - } - - public func data(for request: URLRequest) async throws -> (Data, URLResponse) { - try await data(for: request, delegate: nil) - } -} - -extension URLSessionDataTask: URLSessionDataTaskProtocol {} - -// MARK: MOCK - -class MockURLSession: URLSessionProtocol { - var nextDataTask = MockURLSessionDataTask() - var nextData: Data? - var nextStatusCode: Int = StatusCodes.Success - var nextError: Error? - - private (set) var lastURL: URL? - - func successHttpURLResponse(request: URLRequest) -> URLResponse { - return HTTPURLResponse(url: request.url!, statusCode: nextStatusCode, httpVersion: "HTTP/1.1", headerFields: nil)! - } - - public func _dataTask(with request: URLRequest, completion: @escaping DataTaskResult) -> URLSessionDataTaskProtocol { - lastURL = request.url - completion(nextData, successHttpURLResponse(request: request), nextError) - return nextDataTask - } - - public func data(for request: URLRequest) async throws -> (Data, URLResponse) { - lastURL = request.url - - if let nextData = nextData { - return (nextData, successHttpURLResponse(request: request)) - } else if let nextError = nextError { - throw nextError - } else { - fatalError("No error or data") - } - } -} - -class MockURLSessionDataTask: URLSessionDataTaskProtocol { - private(set) var resumeWasCalled = false - private(set) var cancelWasCalled = false - - func resume() { - resumeWasCalled = true - } - - func cancel() { - cancelWasCalled = true - } -} diff --git a/Example/TraktKitExample.xcworkspace/contents.xcworkspacedata b/Example/TraktKitExample.xcworkspace/contents.xcworkspacedata index ab6efae..848351c 100644 --- a/Example/TraktKitExample.xcworkspace/contents.xcworkspacedata +++ b/Example/TraktKitExample.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/Example/TraktKitExample/TraktKitExample.xcodeproj/project.pbxproj b/Example/TraktKitExample/TraktKitExample.xcodeproj/project.pbxproj index ba183e1..45ef153 100644 --- a/Example/TraktKitExample/TraktKitExample.xcodeproj/project.pbxproj +++ b/Example/TraktKitExample/TraktKitExample.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -13,9 +13,8 @@ 217335BA21E9355D0080BC5B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 217335B821E9355D0080BC5B /* Main.storyboard */; }; 217335BC21E9355F0080BC5B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 217335BB21E9355F0080BC5B /* Assets.xcassets */; }; 217335BF21E9355F0080BC5B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 217335BD21E9355F0080BC5B /* LaunchScreen.storyboard */; }; - 217335D621E940030080BC5B /* TraktKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 217335D521E940030080BC5B /* TraktKit.framework */; }; - 217335D721E940100080BC5B /* TraktKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 217335D521E940030080BC5B /* TraktKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 217335DA21E949C60080BC5B /* TraktProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217335D921E949C60080BC5B /* TraktProfileViewController.swift */; }; + F58A0FC52D67396100DF9BF4 /* TraktKit in Frameworks */ = {isa = PBXBuildFile; productRef = F58A0FC42D67396100DF9BF4 /* TraktKit */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -25,7 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 217335D721E940100080BC5B /* TraktKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -50,7 +48,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 217335D621E940030080BC5B /* TraktKit.framework in Frameworks */, + F58A0FC52D67396100DF9BF4 /* TraktKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,6 +110,7 @@ buildRules = ( ); dependencies = ( + F58A0FC72D673E6100DF9BF4 /* PBXTargetDependency */, ); name = TraktKitExample; productName = TraktKitExample; @@ -124,8 +123,9 @@ 217335A921E9355D0080BC5B /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Maximilian Litteral"; TargetAttributes = { 217335B021E9355D0080BC5B = { @@ -142,6 +142,9 @@ Base, ); mainGroup = 217335A821E9355D0080BC5B; + packageReferences = ( + F58A0FC32D67396100DF9BF4 /* XCLocalSwiftPackageReference "../../../TraktKit" */, + ); productRefGroup = 217335B221E9355D0080BC5B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -178,6 +181,13 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F58A0FC72D673E6100DF9BF4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = F58A0FC62D673E6100DF9BF4 /* TraktKit */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 217335B821E9355D0080BC5B /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -202,6 +212,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -224,6 +235,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -235,6 +247,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -249,13 +262,14 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -263,6 +277,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -285,6 +300,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -296,6 +312,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -304,12 +321,13 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -320,14 +338,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TraktKitExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.litteral.TraktKitExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -338,14 +356,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TraktKitExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.litteral.TraktKitExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -372,6 +390,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + F58A0FC32D67396100DF9BF4 /* XCLocalSwiftPackageReference "../../../TraktKit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../TraktKit; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + F58A0FC42D67396100DF9BF4 /* TraktKit */ = { + isa = XCSwiftPackageProductDependency; + productName = TraktKit; + }; + F58A0FC62D673E6100DF9BF4 /* TraktKit */ = { + isa = XCSwiftPackageProductDependency; + package = F58A0FC32D67396100DF9BF4 /* XCLocalSwiftPackageReference "../../../TraktKit" */; + productName = TraktKit; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 217335A921E9355D0080BC5B /* Project object */; } diff --git a/Example/TraktKitExample/TraktKitExample/AppDelegate.swift b/Example/TraktKitExample/TraktKitExample/AppDelegate.swift index b96af09..863c990 100644 --- a/Example/TraktKitExample/TraktKitExample/AppDelegate.swift +++ b/Example/TraktKitExample/TraktKitExample/AppDelegate.swift @@ -13,7 +13,13 @@ extension Notification.Name { static let TraktSignedIn = Notification.Name(rawValue: "TraktSignedIn") } -@UIApplicationMain +let traktManager = TraktManager( + clientId: Constants.clientId, + clientSecret: Constants.clientSecret, + redirectURI: Constants.redirectURI +) + +@main class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Properties @@ -27,14 +33,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - // MARK: - Lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - TraktManager.sharedManager.set(clientID: Constants.clientId, - clientSecret: Constants.clientSecret, - redirectURI: Constants.redirectURI) return true } @@ -44,42 +46,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if url.host == "auth", let code = queryDict["code"] as? String { // Get authorization code - do { - try TraktManager.sharedManager.getTokenFromAuthorizationCode(code: code) { result in - switch result { - case .success: - print("Signed in to Trakt") - DispatchQueue.main.async { - NotificationCenter.default.post(name: .TraktSignedIn, object: nil) - } - case .fail: - print("Failed to sign in to Trakt") - } - } - } catch { - print(error.localizedDescription) - } - } - return true - } -} -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 - } + Task { @MainActor in + do { + let authorization = try await traktManager.getToken(authorizationCode: code) + print("Signed in to Trakt") + NotificationCenter.default.post(name: .TraktSignedIn, object: nil) + } catch { + print("Failed to get token: \(error)") } } } - return info + + return true } } - diff --git a/Example/TraktKitExample/TraktKitExample/Info.plist b/Example/TraktKitExample/TraktKitExample/Info.plist index 36e0d24..489a9e6 100644 --- a/Example/TraktKitExample/TraktKitExample/Info.plist +++ b/Example/TraktKitExample/TraktKitExample/Info.plist @@ -44,15 +44,13 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown diff --git a/Example/TraktKitExample/TraktKitExample/QRCodeViewController.swift b/Example/TraktKitExample/TraktKitExample/QRCodeViewController.swift index 682fd75..8e4db26 100644 --- a/Example/TraktKitExample/TraktKitExample/QRCodeViewController.swift +++ b/Example/TraktKitExample/TraktKitExample/QRCodeViewController.swift @@ -30,7 +30,7 @@ final class QRCodeViewController: UIViewController { private func start() { - TraktManager.sharedManager.getTokenFromDevice(code: self.data) { result in + traktManager.getTokenFromDevice(code: self.data) { result in switch result { case .fail(let progress): print(progress) diff --git a/Example/TraktKitExample/TraktKitExample/SearchResultsViewController.swift b/Example/TraktKitExample/TraktKitExample/SearchResultsViewController.swift index 35c2cbc..705ea4c 100644 --- a/Example/TraktKitExample/TraktKitExample/SearchResultsViewController.swift +++ b/Example/TraktKitExample/TraktKitExample/SearchResultsViewController.swift @@ -12,6 +12,7 @@ import TraktKit final class SearchResultsViewController: UITableViewController { // MARK: - Properties + private var shows: [TraktShow] = [] { didSet { tableView.reloadData() @@ -22,10 +23,12 @@ final class SearchResultsViewController: UITableViewController { // MARK: - Lifecycle + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { fatalError() } + @available(*, unavailable) override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { fatalError() } @@ -45,7 +48,7 @@ final class SearchResultsViewController: UITableViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - TraktManager.sharedManager.search(query: query, types: [.show], extended: [.Full], pagination: nil, filters: nil, fields: nil) { [weak self] result in + traktManager.search(query: query, types: [.show], extended: [.Full], pagination: nil, filters: nil, fields: nil) { [weak self] result in switch result { case .success(let objects): DispatchQueue.main.async { diff --git a/Example/TraktKitExample/TraktKitExample/TraktProfileViewController.swift b/Example/TraktKitExample/TraktKitExample/TraktProfileViewController.swift index 20b91f3..f2cb34c 100644 --- a/Example/TraktKitExample/TraktKitExample/TraktProfileViewController.swift +++ b/Example/TraktKitExample/TraktKitExample/TraktProfileViewController.swift @@ -26,7 +26,7 @@ final class TraktProfileViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - TraktManager.sharedManager.getUserProfile { [weak self] result in + traktManager.getUserProfile { [weak self] result in switch result { case .success(let user): DispatchQueue.main.async { [weak self] in diff --git a/Example/TraktKitExample/TraktKitExample/ViewController.swift b/Example/TraktKitExample/TraktKitExample/ViewController.swift index 2fd0fcf..18824de 100644 --- a/Example/TraktKitExample/TraktKitExample/ViewController.swift +++ b/Example/TraktKitExample/TraktKitExample/ViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2019 Maximilian Litteral. All rights reserved. // +import Combine import UIKit import SafariServices import TraktKit @@ -16,6 +17,8 @@ final class ViewController: UIViewController { private let stackView = UIStackView() + private var cancellables: Set = [] + // MARK: - Lifecycle override func viewDidLoad() { @@ -27,14 +30,14 @@ final class ViewController: UIViewController { // MARK: - Actions private func presentLogIn() { - guard let oauthURL = TraktManager.sharedManager.oauthURL else { return } + guard let oauthURL = traktManager.oauthURL else { return } let traktAuth = SFSafariViewController(url: oauthURL) present(traktAuth, animated: true, completion: nil) } private func signOut() { - TraktManager.sharedManager.signOut() + traktManager.signOut() refreshUI() } @@ -86,10 +89,15 @@ final class ViewController: UIViewController { } private func setupObservers() { - NotificationCenter.default.addObserver(forName: .TraktSignedIn, object: nil, queue: nil) { [weak self] _ in - self?.dismiss(animated: true, completion: nil) // Dismiss the SFSafariViewController - self?.refreshUI() - } + NotificationCenter.default + .publisher(for: .TraktSignedIn) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self else { return } + dismiss(animated: true, completion: nil) // Dismiss the SFSafariViewController + refreshUI() + } + .store(in: &cancellables) } private func refreshUI() { @@ -104,7 +112,7 @@ final class ViewController: UIViewController { var views = [UIView]() - if TraktManager.sharedManager.isSignedIn { + if traktManager.isSignedIn { let signInButton = createButton(title: "Sign Out", action: { [weak self] _ in self?.signOut() }) views.append(signInButton) diff --git a/License.md b/License.md index f381d2a..b718421 100644 --- a/License.md +++ b/License.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Maximilian Litteral +Copyright (c) 2015-2025 Maximilian Litteral Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Package.swift b/Package.swift index bdfb1b0..86a1a8b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "TraktKit", platforms: [ - .macOS(.v12), - .iOS(.v15), - .tvOS(.v15), - .watchOS(.v8) + .macOS(.v14), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9) ], products: [ .library( @@ -30,5 +30,5 @@ let package = Package( ] ), ], - swiftLanguageVersions: [.version("5.7")] + swiftLanguageModes: [.version("6.0")] ) diff --git a/README.md b/README.md index dadf887..4b2f9f2 100644 --- a/README.md +++ b/README.md @@ -5,54 +5,13 @@ # TraktKit Swift wrapper for Trakt.tv API. -[![Pod version](https://badge.fury.io/co/TraktKit.svg)](https://badge.fury.io/co/TraktKit) -[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg)](https://github.com/Carthage/Carthage) -[![License](https://img.shields.io/cocoapods/l/TraktKit.svg)](https://cocoapods.org/pods/TraktKit) -[![Platform](https://img.shields.io/cocoapods/p/TraktKit.svg)](https://cocoapods.org/pods/TraktKit) -![language Swift 4.2](https://img.shields.io/badge/language-Swift%204.2-orange.svg) +[![SPM: Compatible](https://img.shields.io/badge/SPM-Compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) +[![Platforms: iOS | watchOS | macCatalyst | macOS | tvOS | visionOS](https://img.shields.io/badge/Platforms-iOS%20%7C%20watchOS%20%7C%20macCatalyst%20%7C%20macOS%20%7C%20tvOS%20%7C%20visionOS-blue.svg?style=flat)] +[![Language: Swift 6.0](https://img.shields.io/badge/Language-Swift%206.0-F48041.svg?style=flat)](https://developer.apple.com/swift) +[![License: MIT](http://img.shields.io/badge/License-MIT-lightgray.svg?style=flat)](https://github.com/MaxHasADHD/TraktKit/blob/master/License.md) -## Installation - -### CocoaPods - -[CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: - -```bash -$ gem install cocoapods -``` - -To integrate TraktKit into your Xcode project using CocoaPods, specify it in your `Podfile`: - -```ruby -use_frameworks! - -pod 'TraktKit' -``` - -Then, run the following command: - -```bash -$ pod install -``` -### Carthage - -[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. - -You can install Carthage with [Homebrew](http://brew.sh/) using the following command: - -```bash -$ brew update -$ brew install carthage -``` - -To integrate TraktKit into your Xcode project using Carthage, specify it in your `Cartfile`: - -```ogdl -github "MaxHasADHD/TraktKit" -``` - -Run `carthage update` to build the framework and drag the built `TraktKit.framework` into your Xcode project. +## Installation ### Swift Package Manager @@ -63,16 +22,19 @@ To integrate TraktKit into your Xcode project using Swift Package Manager. Selec When prompted, simply search for TraktKit or specify the project's GitHub repository: ``` -git@github.com:MaxHasADHD/TraktKit.git -``` +https://github.com/MaxHasADHD/TraktKit +``` ### Usage See the [example project](https://github.com/MaxHasADHD/TraktKit/tree/master/Example) for usage +### Author +Maximilian Litteral + ### License The MIT License (MIT) -Copyright (c) 2016 Maximilian Litteral +Copyright (c) 2015-2025 Maximilian Litteral Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Tests/TraktKitTests/AuthenticationInfoTests.swift b/Tests/TraktKitTests/AuthenticationInfoTests.swift index 8accbd1..3dc8f38 100644 --- a/Tests/TraktKitTests/AuthenticationInfoTests.swift +++ b/Tests/TraktKitTests/AuthenticationInfoTests.swift @@ -9,8 +9,7 @@ import XCTest @testable import TraktKit -class AuthenticationInfoTests: XCTestCase { - +final class AuthenticationInfoTests: XCTestCase { func testParsingAuthenticationInfo() { let authenticationInfo = decode("AuthenticationInfo", to: AuthenticationInfo.self)! XCTAssertEqual(authenticationInfo.accessToken, "dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781") diff --git a/Tests/TraktKitTests/CalendarTests.swift b/Tests/TraktKitTests/CalendarTests.swift index 96d9560..9bd126e 100644 --- a/Tests/TraktKitTests/CalendarTests.swift +++ b/Tests/TraktKitTests/CalendarTests.swift @@ -10,22 +10,12 @@ import XCTest import Foundation @testable import TraktKit -class CalendarTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class CalendarTests: TraktTestCase { // MARK: - My Shows - func test_get_my_shows() { - session.nextData = jsonData(named: "test_get_my_shows") + func test_get_my_shows() throws { + try mock(.GET, "https://api.trakt.tv/calendars/my/shows/2014-09-01/7", result: .success(jsonData(named: "test_get_my_shows"))) let expectation = XCTestExpectation(description: "My Shows") traktManager.myShows(startDateString: "2014-09-01", days: 7) { result in @@ -37,8 +27,7 @@ class CalendarTests: XCTestCase { let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/my/shows/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -49,8 +38,8 @@ class CalendarTests: XCTestCase { // MARK: - My New Shows - func test_get_my_new_shows() { - session.nextData = jsonData(named: "test_get_my_new_shows") + func test_get_my_new_shows() throws { + try mock(.GET, "https://api.trakt.tv/calendars/my/shows/new/2014-09-01/7", result: .success(jsonData(named: "test_get_my_new_shows"))) let expectation = XCTestExpectation(description: "My New Shows") traktManager.myNewShows(startDateString: "2014-09-01", days: 7) { result in @@ -62,8 +51,7 @@ class CalendarTests: XCTestCase { let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/my/shows/new/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -74,8 +62,8 @@ class CalendarTests: XCTestCase { // MARK: - My Season Premiers - func test_get_my_season_premieres() { - session.nextData = jsonData(named: "test_get_my_season_premieres") + func test_get_my_season_premieres() throws { + try mock(.GET, "https://api.trakt.tv/calendars/my/shows/premieres/2014-09-01/7", result: .success(jsonData(named: "test_get_my_season_premieres"))) let expectation = XCTestExpectation(description: "My New Seasons") traktManager.mySeasonPremieres(startDateString: "2014-09-01", days: 7) { result in @@ -87,8 +75,7 @@ class CalendarTests: XCTestCase { let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/my/shows/premieres/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -99,8 +86,8 @@ class CalendarTests: XCTestCase { // MARK: - My Movies - func test_get_my_movies() { - session.nextData = jsonData(named: "test_get_my_movies") + func test_get_my_movies() throws { + try mock(.GET, "https://api.trakt.tv/calendars/my/movies/2014-09-01/7", result: .success(jsonData(named: "test_get_my_movies"))) let expectation = XCTestExpectation(description: "My movies") traktManager.myMovies(startDateString: "2014-09-01", days: 7) { result in @@ -110,8 +97,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/my/movies/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -122,8 +108,8 @@ class CalendarTests: XCTestCase { // MARK: - My DVD - func test_get_my_dvd() { - session.nextData = jsonData(named: "test_get_my_dvd") + func test_get_my_dvd() throws { + try mock(.GET, "https://api.trakt.tv/calendars/my/dvd/2014-09-01/7", result: .success(jsonData(named: "test_get_my_dvd"))) let expectation = XCTestExpectation(description: "My dvds") traktManager.myDVDReleases(startDateString: "2014-09-01", days: 7) { result in @@ -133,8 +119,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/my/dvd/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -145,8 +130,8 @@ class CalendarTests: XCTestCase { // MARK: - All Shows - func test_get_all_shows() { - session.nextData = jsonData(named: "test_get_all_shows") + func test_get_all_shows() throws { + try mock(.GET, "https://api.trakt.tv/calendars/all/shows/2014-09-01/7", result: .success(jsonData(named: "test_get_all_shows"))) let expectation = XCTestExpectation(description: "All Shows") traktManager.allShows(startDateString: "2014-09-01", days: 7) { result in @@ -156,8 +141,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/all/shows/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -168,8 +152,8 @@ class CalendarTests: XCTestCase { // MARK: - All New Shows - func test_get_all_new_shows() { - session.nextData = jsonData(named: "test_get_all_new_shows") + func test_get_all_new_shows() throws { + try mock(.GET, "https://api.trakt.tv/calendars/all/shows/new/2014-09-01/7", result: .success(jsonData(named: "test_get_all_new_shows"))) let expectation = XCTestExpectation(description: "All New Shows") traktManager.allNewShows(startDateString: "2014-09-01", days: 7) { result in @@ -179,8 +163,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/all/shows/new/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -191,8 +174,8 @@ class CalendarTests: XCTestCase { // MARK: - All Season Premiers - func test_get_season_premieres() { - session.nextData = jsonData(named: "test_get_season_premieres") + func test_get_season_premieres() throws { + try mock(.GET, "https://api.trakt.tv/calendars/all/shows/premieres/2014-09-01/7", result: .success(jsonData(named: "test_get_season_premieres"))) let expectation = XCTestExpectation(description: "All Season Premieres") traktManager.allSeasonPremieres(startDateString: "2014-09-01", days: 7) { result in @@ -202,8 +185,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/all/shows/premieres/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -214,8 +196,8 @@ class CalendarTests: XCTestCase { // MARK: - All Movies - func test_get_all_movies() { - session.nextData = jsonData(named: "test_get_all_movies") + func test_get_all_movies() throws { + try mock(.GET, "https://api.trakt.tv/calendars/all/movies/2014-09-01/7?extended=min", result: .success(jsonData(named: "test_get_all_movies"))) let expectation = XCTestExpectation(description: "All Movies") traktManager.allMovies(startDateString: "2014-09-01", days: 7) { result in @@ -225,8 +207,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/all/movies/2014-09-01/7?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -237,8 +218,8 @@ class CalendarTests: XCTestCase { // MARK: - All DVD - func test_get_all_dvd() { - session.nextData = jsonData(named: "test_get_all_dvd") + func test_get_all_dvd() throws { + try mock(.GET, "https://api.trakt.tv/calendars/all/dvd/2014-09-01/7", result: .success(jsonData(named: "test_get_all_dvd"))) let expectation = XCTestExpectation(description: "All DVDs") traktManager.allDVD(startDateString: "2014-09-01", days: 7) { result in @@ -248,8 +229,7 @@ class CalendarTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/calendars/all/dvd/2014-09-01/7") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/CertificationsTests.swift b/Tests/TraktKitTests/CertificationsTests.swift index 16109c1..1d23fce 100644 --- a/Tests/TraktKitTests/CertificationsTests.swift +++ b/Tests/TraktKitTests/CertificationsTests.swift @@ -10,20 +10,9 @@ import XCTest import Foundation @testable import TraktKit -class CertificationsTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - - func test_get_certifications() { - session.nextData = jsonData(named: "test_get_certifications") +final class CertificationsTests: TraktTestCase { + func test_get_certifications() throws { + try mock(.GET, "https://api.trakt.tv/certifications", result: .success(jsonData(named: "test_get_certifications"))) let expectation = XCTestExpectation(description: "Get Certifications") traktManager.getCertifications { result in @@ -33,8 +22,7 @@ class CertificationsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/certifications") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/CheckinTests.swift b/Tests/TraktKitTests/CheckinTests.swift index 6573ab1..c8d3851 100644 --- a/Tests/TraktKitTests/CheckinTests.swift +++ b/Tests/TraktKitTests/CheckinTests.swift @@ -10,34 +10,25 @@ import XCTest import Foundation @testable import TraktKit -class CheckinTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) +final class CheckinTests: TraktTestCase { - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - - func test_checkin_movie() { - session.nextData = jsonData(named: "test_checkin_movie") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_checkin_movie() throws { + try mock(.POST, "https://api.trakt.tv/checkin", result: .success(jsonData(named: "test_checkin_movie"))) let expectation = XCTestExpectation(description: "Checkin a movie") let checkin = TraktCheckinBody(movie: SyncId(trakt: 12345)) - try! traktManager.checkIn(checkin) { result in - if case .success(let checkin) = result { + traktManager.checkIn(checkin) { result in + defer { expectation.fulfill() } + switch result { + case .success(let checkin): XCTAssertEqual(checkin.id, 3373536619) XCTAssertNotNil(checkin.movie) - expectation.fulfill() + case .error: + XCTFail("Expected movie checkin") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/checkin") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -46,23 +37,24 @@ class CheckinTests: XCTestCase { } } - func test_checkin_episode() { - session.nextData = jsonData(named: "test_checkin_episode") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_checkin_episode() throws { + try mock(.POST, "https://api.trakt.tv/checkin", result: .success(jsonData(named: "test_checkin_episode"))) let expectation = XCTestExpectation(description: "Checkin a episode") let checkin = TraktCheckinBody(episode: SyncId(trakt: 12345)) - try! traktManager.checkIn(checkin) { result in - if case .success(let checkin) = result { + traktManager.checkIn(checkin) { result in + defer { expectation.fulfill() } + switch result { + case .success(let checkin): XCTAssertEqual(checkin.id, 3373536620) XCTAssertNotNil(checkin.episode) XCTAssertNotNil(checkin.show) - expectation.fulfill() + case .error: + XCTFail("Expected episode checkin") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/checkin") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -71,21 +63,22 @@ class CheckinTests: XCTestCase { } } - func test_already_checked_in() { - session.nextData = jsonData(named: "test_already_checked_in") - session.nextStatusCode = StatusCodes.Conflict + func test_already_checked_in() throws { + try mock(.POST, "https://api.trakt.tv/checkin", result: .success(jsonData(named: "test_already_checked_in")), httpCode: StatusCodes.Conflict) let expectation = XCTestExpectation(description: "Checkin an existing item") let checkin = TraktCheckinBody(episode: SyncId(trakt: 12345)) - try! traktManager.checkIn(checkin) { result in - if case .checkedIn(let expiration) = result { - XCTAssertEqual(expiration.dateString(withFormat: "YYYY-MM-dd"), "2014-10-15") - expectation.fulfill() + traktManager.checkIn(checkin) { result in + defer { expectation.fulfill() } + switch result { + case .success: + XCTFail("Expecting a 409 response error") + case .error(let error): + XCTAssertEqual(error as? TraktManager.TraktError, TraktManager.TraktError.resourceAlreadyCreated) } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/checkin") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -94,8 +87,8 @@ class CheckinTests: XCTestCase { } } - func test_delete_active_checkins() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_delete_active_checkins() throws { + try mock(.POST, "https://api.trakt.tv/checkin", result: .success(jsonData(named: "test_already_checked_in")), httpCode: StatusCodes.SuccessNoContentToReturn) let expectation = XCTestExpectation(description: "Delete active checkins") traktManager.deleteActiveCheckins { result in @@ -104,7 +97,6 @@ class CheckinTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/checkin") switch result { case .timedOut: diff --git a/Tests/TraktKitTests/CommentTests.swift b/Tests/TraktKitTests/CommentTests.swift index 043b66a..b5a7339 100644 --- a/Tests/TraktKitTests/CommentTests.swift +++ b/Tests/TraktKitTests/CommentTests.swift @@ -10,33 +10,21 @@ import XCTest import Foundation @testable import TraktKit -class CommentTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class CommentTests: TraktTestCase { // MARK: - Comments - func test_post_a_comment() { - session.nextData = jsonData(named: "test_post_a_comment") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_post_a_comment() throws { + try mock(.POST, "https://api.trakt.tv/comments", result: .success(jsonData(named: "test_post_a_comment"))) let expectation = XCTestExpectation(description: "Post a comment") - try! traktManager.postComment(movie: SyncId(trakt: 12345), comment: "Oh, I wasn't really listening.", isSpoiler: false) { result in + try traktManager.postComment(movie: SyncId(trakt: 12345), comment: "Oh, I wasn't really listening.", isSpoiler: false) { result in if case .success = result { expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -47,22 +35,22 @@ class CommentTests: XCTestCase { // MARK: - Comment - func test_get_a_comment() { - session.nextData = jsonData(named: "test_get_a_comment") + func test_get_a_comment() throws { + try mock(.GET, "https://api.trakt.tv/comments/417", result: .success(jsonData(named: "test_get_a_comment"))) let expectation = XCTestExpectation(description: "Get a comment") - traktManager.getComment(commentID: "417") { result in + try traktManager.getComment(commentID: "417") { result in if case .success(let comment) = result { XCTAssertEqual(comment.likes, 0) XCTAssertEqual(comment.userRating, 8) + XCTAssertEqual(comment.userStats.rating, 8) XCTAssertEqual(comment.spoiler, false) XCTAssertEqual(comment.review, false) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -71,21 +59,20 @@ class CommentTests: XCTestCase { } } - func test_update_a_comment() { - session.nextData = jsonData(named: "test_update_a_comment") + func test_update_a_comment() throws { + try mock(.PUT, "https://api.trakt.tv/comments/417", result: .success(jsonData(named: "test_update_a_comment"))) let newComment = "Agreed, this show is awesome. AMC in general has awesome shows and I can't wait to see what they come up with next." let expectation = XCTestExpectation(description: "Update a comment") - try! traktManager.updateComment(commentID: "417", newComment: newComment) { result in + try traktManager.updateComment(commentID: "417", newComment: newComment) { result in if case .success(let comment) = result { XCTAssertEqual(comment.comment, newComment) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -94,17 +81,16 @@ class CommentTests: XCTestCase { } } - func test_delete_a_comment() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_delete_a_comment() throws { + try mock(.DELETE, "https://api.trakt.tv/comments/417", result: .success(jsonData(named: "test_update_a_comment"))) let expectation = XCTestExpectation(description: "Delete a comment") - traktManager.deleteComment(commentID: "417") { result in + try traktManager.deleteComment(commentID: "417") { result in if case .success = result { expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417") switch result { case .timedOut: @@ -116,19 +102,18 @@ class CommentTests: XCTestCase { // MARK: - Replies - func test_get_replies_for_comment() { - session.nextData = jsonData(named: "test_get_replies_for_comment") + func test_get_replies_for_comment() throws { + try mock(.GET, "https://api.trakt.tv/comments/417/replies", result: .success(jsonData(named: "test_get_replies_for_comment"))) let expectation = XCTestExpectation(description: "Get replies for comment") - traktManager.getReplies(commentID: "417") { result in + try traktManager.getReplies(commentID: "417") { result in if case .success(let replies) = result { XCTAssertEqual(replies.count, 1) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/replies") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -137,21 +122,20 @@ class CommentTests: XCTestCase { } } - func test_post_reply_for_comment() { - session.nextData = jsonData(named: "test_post_reply_for_comment") + func test_post_reply_for_comment() throws { + try mock(.POST, "https://api.trakt.tv/comments/417/replies", result: .success(jsonData(named: "test_post_reply_for_comment"))) let reply = "Couldn't agree more with your review!" let expectation = XCTestExpectation(description: "Get replies for comment") - try! traktManager.postReply(commentID: "417", comment: reply) { result in + try traktManager.postReply(commentID: "417", comment: reply) { result in if case .success(let postedReply) = result { XCTAssertEqual(postedReply.comment, reply) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/replies") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -162,19 +146,18 @@ class CommentTests: XCTestCase { // MARK: - Item - func test_get_attached_media_item() { - session.nextData = jsonData(named: "test_get_attached_media_item") + func test_get_attached_media_item() throws { + try mock(.GET, "https://api.trakt.tv/comments/417/item", result: .success(jsonData(named: "test_get_attached_media_item"))) let expectation = XCTestExpectation(description: "Get attached media item") - traktManager.getAttachedMediaItem(commentID: "417") { result in + try traktManager.getAttachedMediaItem(commentID: "417") { result in if case .success(let mediaItem) = result { XCTAssertNotNil(mediaItem.show) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/item") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -185,19 +168,18 @@ class CommentTests: XCTestCase { // MARK: - Likes - func test_users_who_liked_comment() { - session.nextData = jsonData(named: "test_users_who_liked_comment") + func test_users_who_liked_comment() throws { + try mock(.GET, "https://api.trakt.tv/comments/417/likes", result: .success(jsonData(named: "test_users_who_liked_comment"))) let expectation = XCTestExpectation(description: "Get users who liked comment") - traktManager.getUsersWhoLikedComment(commentID: "417") { result in + try traktManager.getUsersWhoLikedComment(commentID: "417") { result in if case .success(let users) = result { XCTAssertEqual(users.count, 2) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/likes") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -208,17 +190,16 @@ class CommentTests: XCTestCase { // MARK: - Like - func test_like_a_comment() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_like_a_comment() throws { + try mock(.POST, "https://api.trakt.tv/comments/417/like", result: .success(.init())) let expectation = XCTestExpectation(description: "Like a comment") - traktManager.likeComment(commentID: "417") { result in + try traktManager.likeComment(commentID: "417") { result in if case .success = result { expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/like") switch result { case .timedOut: @@ -228,17 +209,16 @@ class CommentTests: XCTestCase { } } - func test_remove_like() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_remove_like() throws { + try mock(.DELETE, "https://api.trakt.tv/comments/417/like", result: .success(.init())) let expectation = XCTestExpectation(description: "Like a comment") - traktManager.removeLikeOnComment(commentID: "417") { result in + try traktManager.removeLikeOnComment(commentID: "417") { result in if case .success = result { expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/417/like") switch result { case .timedOut: @@ -250,19 +230,18 @@ class CommentTests: XCTestCase { // MARK: - Trending - func test_get_trending_comments() { - session.nextData = jsonData(named: "test_get_trending_comments") + func test_get_trending_comments() throws { + try mock(.GET, "https://api.trakt.tv/comments/trending/all/all?include_replies=true", result: .success(jsonData(named: "test_get_trending_comments"))) let expectation = XCTestExpectation(description: "Get trending comments") - traktManager.getTrendingComments(commentType: .all, mediaType: .All, includeReplies: true) { result in + try traktManager.getTrendingComments(commentType: .all, mediaType: .All, includeReplies: true) { result in if case .success(let trendingComments) = result { XCTAssertEqual(trendingComments.count, 5) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/trending/all/all?include_replies=true") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -273,19 +252,18 @@ class CommentTests: XCTestCase { // MARK: - Recent - func test_get_recently_created_comments() { - session.nextData = jsonData(named: "test_get_recently_created_comments") + func test_get_recently_created_comments() throws { + try mock(.GET, "https://api.trakt.tv/comments/recent/all/all?include_replies=true", result: .success(jsonData(named: "test_get_recently_created_comments"))) let expectation = XCTestExpectation(description: "Get recently created comments") - traktManager.getRecentComments(commentType: .all, mediaType: .All, includeReplies: true) { result in + try traktManager.getRecentComments(commentType: .all, mediaType: .All, includeReplies: true) { result in if case .success(let recentComments) = result { XCTAssertEqual(recentComments.count, 5) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/recent/all/all?include_replies=true") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -296,19 +274,18 @@ class CommentTests: XCTestCase { // MARK: - Updates - func test_get_recently_updated_comments() { - session.nextData = jsonData(named: "test_get_recently_updated_comments") + func test_get_recently_updated_comments() throws { + try mock(.GET, "https://api.trakt.tv/comments/updates/all/all?include_replies=true", result: .success(jsonData(named: "test_get_recently_updated_comments"))) let expectation = XCTestExpectation(description: "Get recently updated comments") - traktManager.getRecentlyUpdatedComments(commentType: .all, mediaType: .All, includeReplies: true) { result in + try traktManager.getRecentlyUpdatedComments(commentType: .all, mediaType: .All, includeReplies: true) { result in if case .success(let recentComments) = result { XCTAssertEqual(recentComments.count, 5) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/comments/updates/all/all?include_replies=true") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/EpisodeTests.swift b/Tests/TraktKitTests/EpisodeTests.swift index f5f500b..c8647c9 100644 --- a/Tests/TraktKitTests/EpisodeTests.swift +++ b/Tests/TraktKitTests/EpisodeTests.swift @@ -10,22 +10,12 @@ import XCTest import Foundation @testable import TraktKit -class EpisodeTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - +final class EpisodeTests: TraktTestCase { + // MARK: - Summary - func test_get_min_episode() { - session.nextData = jsonData(named: "Episode_Min") + func test_get_min_episode() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=min", result: .success(jsonData(named: "Episode_Min"))) let expectation = XCTestExpectation(description: "EpisodeSummary") traktManager.getEpisodeSummary(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -40,8 +30,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -50,8 +39,8 @@ class EpisodeTests: XCTestCase { } } - func test_get_full_episode() { - session.nextData = jsonData(named: "Episode_Full") + func test_get_full_episode() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=full", result: .success(jsonData(named: "Episode_Full"))) let expectation = XCTestExpectation(description: "EpisodeSummary") traktManager.getEpisodeSummary(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1, extended: [.Full]) { result in @@ -70,8 +59,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=full") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -81,7 +69,7 @@ class EpisodeTests: XCTestCase { } func test_get_full_episode_async() async throws { - session.nextData = jsonData(named: "Episode_Full") + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=full", result: .success(jsonData(named: "Episode_Full"))) let episode = try await traktManager .show(id: "game-of-thrones") @@ -97,15 +85,35 @@ class EpisodeTests: XCTestCase { XCTAssertNotNil(episode.overview) XCTAssertNotNil(episode.firstAired) XCTAssertNotNil(episode.updatedAt) - XCTAssertEqual(episode.availableTranslations!, ["en"]) - - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1?extended=full") + XCTAssertEqual(episode.absoluteNumber, 1) + XCTAssertEqual(episode.availableTranslations, ["en"]) + } + + func test_get_episode_images() async throws { + try mock(.GET, "https://api.trakt.tv/shows/severance/seasons/2/episodes/5?extended=images", result: .success(jsonData(named: "Episode_Images"))) + + let episode = try await traktManager + .show(id: "severance") + .season(2).episode(5) + .summary() + .extend(.images) + .perform() + + XCTAssertEqual(episode.title, "Trojan's Horse") + XCTAssertEqual(episode.season, 2) + XCTAssertEqual(episode.number, 5) + XCTAssertEqual(episode.ids.trakt, 12103031) + XCTAssertNil(episode.overview) + XCTAssertNil(episode.firstAired) + XCTAssertNil(episode.updatedAt) + XCTAssertNil(episode.absoluteNumber) + XCTAssertNil(episode.availableTranslations) } // MARK: - Translations - func test_get_all_episode_translations() { - session.nextData = jsonData(named: "test_get_all_episode_translations") + func test_get_all_episode_translations() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/translations", result: .success(jsonData(named: "test_get_all_episode_translations"))) let expectation = XCTestExpectation(description: "Get episode translations") traktManager.getEpisodeTranslations(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -117,8 +125,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/translations") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -129,8 +136,8 @@ class EpisodeTests: XCTestCase { // MARK: - Comments - func test_get_episode_comments() { - session.nextData = jsonData(named: "test_get_episode_comments") + func test_get_episode_comments() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/comments", result: .success(jsonData(named: "test_get_episode_comments"))) let expectation = XCTestExpectation(description: "Get episode comments") traktManager.getEpisodeComments(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -142,8 +149,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/comments") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -154,8 +160,8 @@ class EpisodeTests: XCTestCase { // MARK: - Lists - func test_get_lists_containing_episode() { - session.nextData = jsonData(named: "test_get_lists_containing_episode") + func test_get_lists_containing_episode() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/lists/official/added", result: .success(jsonData(named: "test_get_lists_containing_episode"))) let expectation = XCTestExpectation(description: "Get lists containing episode") traktManager.getListsContainingEpisode(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1, listType: .official, sortBy: .added) { result in @@ -165,8 +171,7 @@ class EpisodeTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/lists/official/added") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -175,10 +180,27 @@ class EpisodeTests: XCTestCase { } } + func test_get_listsContainingEpisode_async() async throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/lists/official/added?extended=full", result: .success(jsonData(named: "test_get_lists_containing_episode"))) + + let route = traktManager + .show(id: "game-of-thrones") + .season(1).episode(1) + .containingLists() + .listType(.official) + .sort(by: .added) + .extend(.Full) + + XCTAssertEqual(route.path, "shows/game-of-thrones/seasons/1/episodes/1/lists/official/added") + + let lists = try await route.perform() + XCTAssertEqual(lists.count, 1) + } + // MARK: - Ratings - func test_get_episode_ratings() { - session.nextData = jsonData(named: "test_get_episode_ratings") + func test_get_episode_ratings() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/ratings", result: .success(jsonData(named: "test_get_episode_ratings"))) let expectation = XCTestExpectation(description: "Get episode ratings") traktManager.getEpisodeRatings(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -191,8 +213,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/ratings") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -203,8 +224,8 @@ class EpisodeTests: XCTestCase { // MARK: - Stats - func test_get_episode_stats() { - session.nextData = jsonData(named: "test_get_episode_stats") + func test_get_episode_stats() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/stats", result: .success(jsonData(named: "test_get_episode_stats"))) let expectation = XCTestExpectation(description: "Get episode stats") traktManager.getEpisodeStatistics(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -222,8 +243,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/stats") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -234,8 +254,8 @@ class EpisodeTests: XCTestCase { // MARK: - Watching - func test_get_users_watching_now() { - session.nextData = jsonData(named: "test_get_users_watching_now") + func test_get_users_watching_now() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/watching", result: .success(jsonData(named: "test_get_users_watching_now"))) let expectation = XCTestExpectation(description: "Get users watching episode") traktManager.getUsersWatchingEpisode(showID: "game-of-thrones", seasonNumber: 1, episodeNumber: 1) { result in @@ -247,8 +267,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/watching") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -259,8 +278,8 @@ class EpisodeTests: XCTestCase { // MARK: - People - func test_get_show_people_min() { - session.nextData = jsonData(named: "test_get_episode_cast") + func test_get_show_people_min() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/people?extended=min", result: .success(jsonData(named: "test_get_episode_cast"))) let expectation = XCTestExpectation(description: "ShowCastAndCrew") traktManager.getPeopleInEpisode(showID: "game-of-thrones", season: 1, episode: 1) { result in @@ -279,8 +298,7 @@ class EpisodeTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/episodes/1/people?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/GenreTests.swift b/Tests/TraktKitTests/GenreTests.swift index 5c381b8..db08669 100644 --- a/Tests/TraktKitTests/GenreTests.swift +++ b/Tests/TraktKitTests/GenreTests.swift @@ -10,20 +10,9 @@ import XCTest import Foundation @testable import TraktKit -class GenreTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - - func test_get_genres() { - session.nextData = jsonData(named: "test_get_genres") +final class GenreTests: TraktTestCase { + func test_get_genres() throws { + try mock(.GET, "https://api.trakt.tv/genres/movies", result: .success(jsonData(named: "test_get_genres"))) let expectation = XCTestExpectation(description: "Get movie genres") traktManager.listGenres(type: .Movies) { result in @@ -33,8 +22,7 @@ class GenreTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/genres/movies") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/HelperFunctions.swift b/Tests/TraktKitTests/HelperFunctions.swift index 42e919f..a52de8e 100644 --- a/Tests/TraktKitTests/HelperFunctions.swift +++ b/Tests/TraktKitTests/HelperFunctions.swift @@ -61,10 +61,10 @@ extension Bundle { return Bundle(for: bundleClass) #endif } - - private static var testingBundle = packageBundle(for: "TraktKit_TraktKitTests") - private static var modulebundle = packageBundle() - + + private static let testingBundle = packageBundle(for: "TraktKit_TraktKitTests") + private static let modulebundle = packageBundle() + /// Searches all bundles for the correct one since Swift Package bundles don't work. /// - Parameter bundleName: "packageName_ProductName" private static func packageBundle(for bundleName: String = "TraktKit_TraktKit") -> Bundle { diff --git a/Tests/TraktKitTests/JSONEncodingTests.swift b/Tests/TraktKitTests/JSONEncodingTests.swift index b9a4667..6919e0b 100644 --- a/Tests/TraktKitTests/JSONEncodingTests.swift +++ b/Tests/TraktKitTests/JSONEncodingTests.swift @@ -12,7 +12,7 @@ import Foundation class JSONEncodingTests: XCTestCase { - let jsonEncoder = TraktManager.sharedManager.jsonEncoder + let jsonEncoder = TraktManager.jsonEncoder func testEncodeShowIds() { let expectation: RawJSON = [ diff --git a/Tests/TraktKitTests/ListsTests.swift b/Tests/TraktKitTests/ListsTests.swift index 17daa9f..9b8789a 100644 --- a/Tests/TraktKitTests/ListsTests.swift +++ b/Tests/TraktKitTests/ListsTests.swift @@ -9,20 +9,9 @@ import XCTest @testable import TraktKit -class ListsTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - - func test_get_trending_lists() { - session.nextData = jsonData(named: "test_get_trending_lists") +final class ListsTests: TraktTestCase { + func test_get_trending_lists() throws { + try mock(.GET, "https://api.trakt.tv/lists/trending", result: .success(jsonData(named: "test_get_trending_lists"))) let expectation = XCTestExpectation(description: "Get trending lists") traktManager.getTrendingLists { result in @@ -32,8 +21,7 @@ class ListsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/lists/trending") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -42,8 +30,8 @@ class ListsTests: XCTestCase { } } - func test_get_popular_lists() { - session.nextData = jsonData(named: "test_get_popular_lists") + func test_get_popular_lists() throws { + try mock(.GET, "https://api.trakt.tv/lists/popular", result: .success(jsonData(named: "test_get_popular_lists"))) let expectation = XCTestExpectation(description: "Get popular lists") traktManager.getPopularLists { result in @@ -53,8 +41,7 @@ class ListsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/lists/popular") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -62,5 +49,4 @@ class ListsTests: XCTestCase { break } } - } diff --git a/Tests/TraktKitTests/Models/Checkin/test_checkin_episode.json b/Tests/TraktKitTests/Models/Checkin/test_checkin_episode.json index 74124d3..4d6b9a5 100644 --- a/Tests/TraktKitTests/Models/Checkin/test_checkin_episode.json +++ b/Tests/TraktKitTests/Models/Checkin/test_checkin_episode.json @@ -1,10 +1,11 @@ { "id" : 3373536620, "watched_at" : "2014-08-06T06:54:36.859Z", - "sharing" : { - "twitter" : true, - "facebook" : true, - "tumblr" : false + "sharing": { + "facebook": false, + "twitter": false, + "mastodon": false, + "tumblr": false }, "show" : { "title" : "Breaking Bad", diff --git a/Tests/TraktKitTests/Models/Checkin/test_checkin_movie.json b/Tests/TraktKitTests/Models/Checkin/test_checkin_movie.json index 030aaf6..8dfe99a 100644 --- a/Tests/TraktKitTests/Models/Checkin/test_checkin_movie.json +++ b/Tests/TraktKitTests/Models/Checkin/test_checkin_movie.json @@ -11,10 +11,11 @@ }, "id" : 3373536619, "watched_at" : "2014-08-06T01:11:37.000Z", - "sharing" : { - "twitter" : true, - "facebook" : true, - "tumblr" : false - } + "sharing": { + "facebook": false, + "twitter": false, + "mastodon": false, + "tumblr": false + }, } diff --git a/Tests/TraktKitTests/Models/Comments/test_get_a_comment.json b/Tests/TraktKitTests/Models/Comments/test_get_a_comment.json index 96b3fb7..48172d7 100644 --- a/Tests/TraktKitTests/Models/Comments/test_get_a_comment.json +++ b/Tests/TraktKitTests/Models/Comments/test_get_a_comment.json @@ -1,21 +1,26 @@ { - "review" : false, - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin Nemeth", - "ids" : { - "slug" : "justin" + "id": 1, + "parent_id": 0, + "created_at": "2010-11-03T06:30:13.000Z", + "comment": "Agreed, this show is awesome. AMC in general has awesome shows.", + "spoiler": false, + "review": false, + "replies": 1, + "likes": 0, + "user_rating": 8, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin Nemeth", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } } - }, - "replies" : 1, - "id" : 1, - "created_at" : "2010-11-03T06:30:13.000Z", - "parent_id" : 0, - "spoiler" : false, - "comment" : "Agreed, this show is awesome. AMC in general has awesome shows.", - "likes" : 0, - "user_rating" : 8 } diff --git a/Tests/TraktKitTests/Models/Comments/test_get_recently_created_comments.json b/Tests/TraktKitTests/Models/Comments/test_get_recently_created_comments.json index bb71530..464497c 100644 --- a/Tests/TraktKitTests/Models/Comments/test_get_recently_created_comments.json +++ b/Tests/TraktKitTests/Models/Comments/test_get_recently_created_comments.json @@ -1,204 +1,225 @@ [ - { - "type" : "movie", - "movie" : { - "title" : "Batman Begins", - "year" : 2005, - "ids" : { - "tmdb" : 272, - "slug" : "batman-begins-2005", - "trakt" : 1, - "imdb" : "tt0372784" - } - }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 267, - "created_at" : "2015-04-25T00:14:57.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "movie", + "movie": { + "title": "Batman Begins", + "year": 2005, + "ids": { + "trakt": 1, + "slug": "batman-begins-2005", + "imdb": "tt0372784", + "tmdb": 272 + } + }, + "comment": { + "id": 267, + "comment": "Great kickoff to a new Batman trilogy!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-25T00:14:57.000Z", + "updated_at": "2015-04-25T00:14:57.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Great kickoff to a new Batman trilogy!", - "updated_at" : "2015-04-25T00:14:57.000Z", - "likes" : 0 - } - }, - { - "type" : "show", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 199, - "created_at" : "2015-02-18T06:02:30.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "show", + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + }, + "comment": { + "id": 199, + "comment": "Skyler, I AM THE DANGER.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-02-18T06:02:30.000Z", + "updated_at": "2015-02-18T06:02:30.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Skyler, I AM THE DANGER.", - "updated_at" : "2015-02-18T06:02:30.000Z", - "likes" : 0 - } - }, - { - "season" : { - "number" : 1, - "ids" : { - "trakt" : 3958, - "tvdb" : 274431, - "tmdb" : 60394 - } - }, - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "season", - "comment" : { - "review" : false, - "user_rating" : 8, - "replies" : 0, - "id" : 220, - "created_at" : "2015-04-21T06:53:25.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 3958, + "tvdb": 274431, + "tmdb": 60394 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 220, + "comment": "Kicking off season 1 for a new Batman show.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T06:53:25.000Z", + "updated_at": "2015-04-21T06:53:25.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Kicking off season 1 for a new Batman show.", - "updated_at" : "2015-04-21T06:53:25.000Z", - "likes" : 0 - } - }, - { - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "episode", - "comment" : { - "review" : false, - "user_rating" : 7, - "replies" : 1, - "id" : 229, - "created_at" : "2015-04-21T15:42:31.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "episode", + "episode": { + "season": 1, + "number": 1, + "title": "Jim Gordon", + "ids": { + "trakt": 63958, + "tvdb": 4768720, + "imdb": "tt3216414", + "tmdb": 975968 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 229, + "comment": "Is this the OC?", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T15:42:31.000Z", + "updated_at": "2015-04-21T15:42:31.000Z", + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 7, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Is this the OC?", - "updated_at" : "2015-04-21T15:42:31.000Z", - "likes" : 0 }, - "episode" : { - "number" : 1, - "season" : 1, - "title" : "Jim Gordon", - "ids" : { - "tmdb" : 975968, - "tvdb" : 4768720, - "trakt" : 63958, - "imdb" : "tt3216414" - } - } - }, - { - "type" : "list", - "comment" : { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 268, - "created_at" : "2014-12-08T17:34:51.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "list", + "list": { + "name": "Star Wars", + "description": "The complete Star Wars saga!", + "privacy": "public", + "share_link": "https://trakt.tv/lists/51", + "display_numbers": false, + "allow_comments": true, + "updated_at": "2015-04-22T22:01:39.000Z", + "item_count": 8, + "comment_count": 0, + "likes": 0, + "ids": { + "trakt": 51, + "slug": "star-wars" + } + }, + "comment": { + "id": 268, + "comment": "May the 4th be with you!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2014-12-08T17:34:51.000Z", + "updated_at": "2014-12-08T17:34:51.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "May the 4th be with you!", - "updated_at" : "2014-12-08T17:34:51.000Z", - "likes" : 0 - }, - "list" : { - "ids" : { - "trakt" : 51, - "slug" : "star-wars" - }, - "display_numbers" : false, - "privacy" : "public", - "allow_comments" : true, - "comment_count" : 0, - "item_count" : 8, - "description" : "The complete Star Wars saga!", - "updated_at" : "2015-04-22T22:01:39.000Z", - "name" : "Star Wars", - "likes" : 0 } - } ] diff --git a/Tests/TraktKitTests/Models/Comments/test_get_recently_updated_comments.json b/Tests/TraktKitTests/Models/Comments/test_get_recently_updated_comments.json index bb71530..464497c 100644 --- a/Tests/TraktKitTests/Models/Comments/test_get_recently_updated_comments.json +++ b/Tests/TraktKitTests/Models/Comments/test_get_recently_updated_comments.json @@ -1,204 +1,225 @@ [ - { - "type" : "movie", - "movie" : { - "title" : "Batman Begins", - "year" : 2005, - "ids" : { - "tmdb" : 272, - "slug" : "batman-begins-2005", - "trakt" : 1, - "imdb" : "tt0372784" - } - }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 267, - "created_at" : "2015-04-25T00:14:57.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "movie", + "movie": { + "title": "Batman Begins", + "year": 2005, + "ids": { + "trakt": 1, + "slug": "batman-begins-2005", + "imdb": "tt0372784", + "tmdb": 272 + } + }, + "comment": { + "id": 267, + "comment": "Great kickoff to a new Batman trilogy!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-25T00:14:57.000Z", + "updated_at": "2015-04-25T00:14:57.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Great kickoff to a new Batman trilogy!", - "updated_at" : "2015-04-25T00:14:57.000Z", - "likes" : 0 - } - }, - { - "type" : "show", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 199, - "created_at" : "2015-02-18T06:02:30.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "show", + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + }, + "comment": { + "id": 199, + "comment": "Skyler, I AM THE DANGER.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-02-18T06:02:30.000Z", + "updated_at": "2015-02-18T06:02:30.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Skyler, I AM THE DANGER.", - "updated_at" : "2015-02-18T06:02:30.000Z", - "likes" : 0 - } - }, - { - "season" : { - "number" : 1, - "ids" : { - "trakt" : 3958, - "tvdb" : 274431, - "tmdb" : 60394 - } - }, - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "season", - "comment" : { - "review" : false, - "user_rating" : 8, - "replies" : 0, - "id" : 220, - "created_at" : "2015-04-21T06:53:25.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 3958, + "tvdb": 274431, + "tmdb": 60394 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 220, + "comment": "Kicking off season 1 for a new Batman show.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T06:53:25.000Z", + "updated_at": "2015-04-21T06:53:25.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Kicking off season 1 for a new Batman show.", - "updated_at" : "2015-04-21T06:53:25.000Z", - "likes" : 0 - } - }, - { - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "episode", - "comment" : { - "review" : false, - "user_rating" : 7, - "replies" : 1, - "id" : 229, - "created_at" : "2015-04-21T15:42:31.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "episode", + "episode": { + "season": 1, + "number": 1, + "title": "Jim Gordon", + "ids": { + "trakt": 63958, + "tvdb": 4768720, + "imdb": "tt3216414", + "tmdb": 975968 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 229, + "comment": "Is this the OC?", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T15:42:31.000Z", + "updated_at": "2015-04-21T15:42:31.000Z", + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 7, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Is this the OC?", - "updated_at" : "2015-04-21T15:42:31.000Z", - "likes" : 0 }, - "episode" : { - "number" : 1, - "season" : 1, - "title" : "Jim Gordon", - "ids" : { - "tmdb" : 975968, - "tvdb" : 4768720, - "trakt" : 63958, - "imdb" : "tt3216414" - } - } - }, - { - "type" : "list", - "comment" : { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 268, - "created_at" : "2014-12-08T17:34:51.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "list", + "list": { + "name": "Star Wars", + "description": "The complete Star Wars saga!", + "privacy": "public", + "share_link": "https://trakt.tv/lists/51", + "display_numbers": false, + "allow_comments": true, + "updated_at": "2015-04-22T22:01:39.000Z", + "item_count": 8, + "comment_count": 0, + "likes": 0, + "ids": { + "trakt": 51, + "slug": "star-wars" + } + }, + "comment": { + "id": 268, + "comment": "May the 4th be with you!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2014-12-08T17:34:51.000Z", + "updated_at": "2014-12-08T17:34:51.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "May the 4th be with you!", - "updated_at" : "2014-12-08T17:34:51.000Z", - "likes" : 0 - }, - "list" : { - "ids" : { - "trakt" : 51, - "slug" : "star-wars" - }, - "display_numbers" : false, - "privacy" : "public", - "allow_comments" : true, - "comment_count" : 0, - "item_count" : 8, - "description" : "The complete Star Wars saga!", - "updated_at" : "2015-04-22T22:01:39.000Z", - "name" : "Star Wars", - "likes" : 0 } - } ] diff --git a/Tests/TraktKitTests/Models/Comments/test_get_replies_for_comment.json b/Tests/TraktKitTests/Models/Comments/test_get_replies_for_comment.json index a0b2cf9..206ac40 100644 --- a/Tests/TraktKitTests/Models/Comments/test_get_replies_for_comment.json +++ b/Tests/TraktKitTests/Models/Comments/test_get_replies_for_comment.json @@ -1,24 +1,28 @@ [ - { - "review" : false, - "user_rating" : 8, - "replies" : 0, - "id" : 19, - "created_at" : "2014-07-27T23:06:59.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 1, - "updated_at" : "2014-07-27T23:06:59.000Z", - "comment" : "Season 2 has really picked up the action!", - "spoiler" : true, - "likes" : 0 - } + { + "id": 19, + "parent_id": 1, + "created_at": "2014-07-27T23:06:59.000Z", + "updated_at": "2014-07-27T23:06:59.000Z", + "comment": "Season 2 has really picked up the action!", + "spoiler": true, + "review": false, + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Comments/test_get_trending_comments.json b/Tests/TraktKitTests/Models/Comments/test_get_trending_comments.json index bb71530..464497c 100644 --- a/Tests/TraktKitTests/Models/Comments/test_get_trending_comments.json +++ b/Tests/TraktKitTests/Models/Comments/test_get_trending_comments.json @@ -1,204 +1,225 @@ [ - { - "type" : "movie", - "movie" : { - "title" : "Batman Begins", - "year" : 2005, - "ids" : { - "tmdb" : 272, - "slug" : "batman-begins-2005", - "trakt" : 1, - "imdb" : "tt0372784" - } - }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 267, - "created_at" : "2015-04-25T00:14:57.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "movie", + "movie": { + "title": "Batman Begins", + "year": 2005, + "ids": { + "trakt": 1, + "slug": "batman-begins-2005", + "imdb": "tt0372784", + "tmdb": 272 + } + }, + "comment": { + "id": 267, + "comment": "Great kickoff to a new Batman trilogy!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-25T00:14:57.000Z", + "updated_at": "2015-04-25T00:14:57.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Great kickoff to a new Batman trilogy!", - "updated_at" : "2015-04-25T00:14:57.000Z", - "likes" : 0 - } - }, - { - "type" : "show", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 199, - "created_at" : "2015-02-18T06:02:30.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "show", + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + }, + "comment": { + "id": 199, + "comment": "Skyler, I AM THE DANGER.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-02-18T06:02:30.000Z", + "updated_at": "2015-02-18T06:02:30.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Skyler, I AM THE DANGER.", - "updated_at" : "2015-02-18T06:02:30.000Z", - "likes" : 0 - } - }, - { - "season" : { - "number" : 1, - "ids" : { - "trakt" : 3958, - "tvdb" : 274431, - "tmdb" : 60394 - } - }, - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "season", - "comment" : { - "review" : false, - "user_rating" : 8, - "replies" : 0, - "id" : 220, - "created_at" : "2015-04-21T06:53:25.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 3958, + "tvdb": 274431, + "tmdb": 60394 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 220, + "comment": "Kicking off season 1 for a new Batman show.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T06:53:25.000Z", + "updated_at": "2015-04-21T06:53:25.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Kicking off season 1 for a new Batman show.", - "updated_at" : "2015-04-21T06:53:25.000Z", - "likes" : 0 - } - }, - { - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "episode", - "comment" : { - "review" : false, - "user_rating" : 7, - "replies" : 1, - "id" : 229, - "created_at" : "2015-04-21T15:42:31.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "episode", + "episode": { + "season": 1, + "number": 1, + "title": "Jim Gordon", + "ids": { + "trakt": 63958, + "tvdb": 4768720, + "imdb": "tt3216414", + "tmdb": 975968 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 229, + "comment": "Is this the OC?", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T15:42:31.000Z", + "updated_at": "2015-04-21T15:42:31.000Z", + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 7, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Is this the OC?", - "updated_at" : "2015-04-21T15:42:31.000Z", - "likes" : 0 }, - "episode" : { - "number" : 1, - "season" : 1, - "title" : "Jim Gordon", - "ids" : { - "tmdb" : 975968, - "tvdb" : 4768720, - "trakt" : 63958, - "imdb" : "tt3216414" - } - } - }, - { - "type" : "list", - "comment" : { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 268, - "created_at" : "2014-12-08T17:34:51.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "list", + "list": { + "name": "Star Wars", + "description": "The complete Star Wars saga!", + "privacy": "public", + "share_link": "https://trakt.tv/lists/51", + "display_numbers": false, + "allow_comments": true, + "updated_at": "2015-04-22T22:01:39.000Z", + "item_count": 8, + "comment_count": 0, + "likes": 0, + "ids": { + "trakt": 51, + "slug": "star-wars" + } + }, + "comment": { + "id": 268, + "comment": "May the 4th be with you!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2014-12-08T17:34:51.000Z", + "updated_at": "2014-12-08T17:34:51.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "May the 4th be with you!", - "updated_at" : "2014-12-08T17:34:51.000Z", - "likes" : 0 - }, - "list" : { - "ids" : { - "trakt" : 51, - "slug" : "star-wars" - }, - "display_numbers" : false, - "privacy" : "public", - "allow_comments" : true, - "comment_count" : 0, - "item_count" : 8, - "description" : "The complete Star Wars saga!", - "updated_at" : "2015-04-22T22:01:39.000Z", - "name" : "Star Wars", - "likes" : 0 } - } ] diff --git a/Tests/TraktKitTests/Models/Comments/test_post_reply_for_comment.json b/Tests/TraktKitTests/Models/Comments/test_post_reply_for_comment.json index b6c6c54..8b749c6 100644 --- a/Tests/TraktKitTests/Models/Comments/test_post_reply_for_comment.json +++ b/Tests/TraktKitTests/Models/Comments/test_post_reply_for_comment.json @@ -1,22 +1,26 @@ { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 2, - "created_at" : "2014-09-01T06:30:13.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin Nemeth", - "ids" : { - "slug" : "justin" + "id": 2, + "parent_id": 1, + "created_at": "2014-09-01T06:30:13.000Z", + "updated_at": "2014-09-01T06:30:13.000Z", + "comment": "Couldn't agree more with your review!", + "spoiler": false, + "review": false, + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin Nemeth", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } } - }, - "parent_id" : 1, - "updated_at" : "2014-09-01T06:30:13.000Z", - "comment" : "Couldn't agree more with your review!", - "spoiler" : false, - "likes" : 0 } diff --git a/Tests/TraktKitTests/Models/Comments/test_update_a_comment.json b/Tests/TraktKitTests/Models/Comments/test_update_a_comment.json index 37f9ae3..74b3c1c 100644 --- a/Tests/TraktKitTests/Models/Comments/test_update_a_comment.json +++ b/Tests/TraktKitTests/Models/Comments/test_update_a_comment.json @@ -1,23 +1,26 @@ { - "review" : false, - "user_rating" : null, - "replies" : 1, - "id" : 1, - "created_at" : "2010-11-03T06:30:13.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin Nemeth", - "ids" : { - "slug" : "justin" + "id": 1, + "parent_id": 0, + "created_at": "2010-11-03T06:30:13.000Z", + "updated_at": "2010-11-13T06:30:13.000Z", + "comment": "Agreed, this show is awesome. AMC in general has awesome shows and I can't wait to see what they come up with next.", + "spoiler": false, + "review": false, + "replies": 1, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin Nemeth", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } } - }, - "parent_id" : 0, - "updated_at" : "2010-11-13T06:30:13.000Z", - "comment" : "Agreed, this show is awesome. AMC in general has awesome shows and I can't wait to see what they come up with next.", - "spoiler" : false, - "likes" : 0 } - diff --git a/Tests/TraktKitTests/Models/Episodes/Episode_Full.json b/Tests/TraktKitTests/Models/Episodes/Episode_Full.json index 0f56173..94f6a43 100644 --- a/Tests/TraktKitTests/Models/Episodes/Episode_Full.json +++ b/Tests/TraktKitTests/Models/Episodes/Episode_Full.json @@ -9,7 +9,7 @@ "tmdb": 63056, "tvrage": null }, - "number_abs": null, + "number_abs": 1, "overview": "Ned Stark, Lord of Winterfell learns that his mentor, Jon Arryn, has died and that King Robert is on his way north to offer Ned Arryn’s position as the King’s Hand. Across the Narrow Sea in Pentos, Viserys Targaryen plans to wed his sister Daenerys to the nomadic Dothraki warrior leader, Khal Drogo to forge an alliance to take the throne.", "first_aired": "2011-04-18T01:00:00.000Z", "updated_at": "2014-08-29T23:16:39.000Z", diff --git a/Tests/TraktKitTests/Models/Episodes/Episode_Images.json b/Tests/TraktKitTests/Models/Episodes/Episode_Images.json new file mode 100644 index 0000000..c6fee60 --- /dev/null +++ b/Tests/TraktKitTests/Models/Episodes/Episode_Images.json @@ -0,0 +1,17 @@ +{ + "season": 2, + "number": 5, + "title": "Trojan's Horse", + "ids": { + "trakt": 12103031, + "tvdb": 10592762, + "imdb": "tt15242966", + "tmdb": 5469120, + "tvrage": null + }, + "images": { + "screenshot": [ + "walter-r2.trakt.tv/images/episodes/012/103/031/screenshots/medium/d610df59c0.jpg.webp" + ] + } +} diff --git a/Tests/TraktKitTests/Models/Episodes/test_get_episode_comments.json b/Tests/TraktKitTests/Models/Episodes/test_get_episode_comments.json index 872b702..745f102 100644 --- a/Tests/TraktKitTests/Models/Episodes/test_get_episode_comments.json +++ b/Tests/TraktKitTests/Models/Episodes/test_get_episode_comments.json @@ -1,24 +1,28 @@ [ - { - "review" : false, - "user_rating" : 8, - "replies" : 1, - "id" : 8, - "created_at" : "2011-03-25T22:35:17.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 0, - "updated_at" : "2011-03-25T22:35:17.000Z", - "comment" : "Great episode!", - "spoiler" : false, - "likes" : 0 - } + { + "id": 8, + "parent_id": 0, + "created_at": "2011-03-25T22:35:17.000Z", + "updated_at": "2011-03-25T22:35:17.000Z", + "comment": "Great episode!", + "spoiler": false, + "review": false, + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Movies/test_get_cast_and_crew.json b/Tests/TraktKitTests/Models/Movies/test_get_cast_and_crew.json index 0698a15..9160f7f 100644 --- a/Tests/TraktKitTests/Models/Movies/test_get_cast_and_crew.json +++ b/Tests/TraktKitTests/Models/Movies/test_get_cast_and_crew.json @@ -1,10 +1,9 @@ { "cast": [ { - "character": "Tony Stark, Iron Man", + "character": "Tony Stark", "characters": [ - "Tony Stark", - "Iron Man" + "Tony Stark" ], "person": { "name": "Robert Downey Jr.", @@ -13,19 +12,28 @@ "slug": "robert-downey-jr", "imdb": "nm0000375", "tmdb": 3223, - "tvrage": 10038 + "tvrage": null + }, + "social_ids": { + "twitter": "RobertDowneyJr", + "facebook": "robertdowneyjr", + "instagram": "robertdowneyjr", + "wikipedia": null }, - "biography": "Robert John Downey Jr. (born April 4, 1965) is an American actor and producer. Downey made his screen debut in 1970, at the age of five, when he appeared in his father's film Pound, and has worked consistently in film and television ever since. He received two Academy Award nominations for his roles in films Chaplin (1992) and Tropic Thunder (2008).", + "biography": "Robert John Downey, Jr. (born April 4, 1965) is an American actor. His films as a leading actor have grossed over $14 billion worldwide, making him one of the highest-grossing actors of all time. Downey's career has been characterized by some early success, a period of drug-related problems and run-ins with the law, and a surge in popular and commercial success in the 2000s. In 2008, Downey was named by Time magazine as one of the 100 most influential people in the world. From 2013 to 2015, he was listed by Forbes as Hollywood's highest-paid actor.\n\nAt the age of five, Downey made his acting debut in his father Robert Downey Sr.'s film Pound in 1970. He subsequently worked with the Brat Pack in the teen films Weird Science (1985) and Less than Zero (1987). Downey's portrayal of Charlie Chaplin in the 1992 biopic Chaplin received a BAFTA Award. Following a stint at the Corcoran Substance Abuse Treatment Facility on drug charges, he joined the TV series Ally McBeal in 2000, and won a Golden Globe Award for the role. Downey was fired from the show in 2001 in the wake of additional drug charges. He stayed in a court-ordered drug treatment program and has maintained his sobriety since 2003.\n\nDowney made his acting comeback in the 2003 film The Singing Detective, after Mel Gibson paid his insurance bond because completion bond companies would not insure himю He went on to star in the black comedy Kiss Kiss Bang Bang (2005), the thriller Zodiac (2007), and the action comedy Tropic Thunder (2008). Downey gained global recognition for starring as Iron Man in ten films within the Marvel Cinematic Universe, beginning with Iron Man (2008), and leading up to Avengers: Endgame (2019). He has also played Sherlock Holmes in Guy Ritchie's Sherlock Holmes (2009), which earned him his second Golden Globe, and its sequel, Sherlock Holmes: A Game of Shadows (2011). Downey has also taken on dramatic parts in The Judge (2014) and Oppenheimer (2023), winning an Academy Award, a Golden Globe, and a BAFTA Award for his portrayal of Lewis Strauss in the latter.\n\nDescription above from the Wikipedia article Robert Downey Jr., licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1965-04-04", "death": null, - "birthplace": "Manhattan, New York City, New York, USA", - "homepage": null + "birthplace": "New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-21T08:06:13.000Z" } }, { - "character": "James \"Rhodey\" Rhodes", + "character": "Rhodey", "characters": [ - "James \"Rhodey\" Rhodes" + "Rhodey" ], "person": { "name": "Terrence Howard", @@ -34,20 +42,28 @@ "slug": "terrence-howard", "imdb": "nm0005024", "tmdb": 18288, - "tvrage": 59344 + "tvrage": null }, - "biography": "Terrence Dashon Howard (born March 11, 1969) is an Academy Award-nominated American actor. Having his first major role in the 1995 film Mr. Holland's Opus, which subsequently led to a number of roles in films and high visibility among African American audiences. Howard broke into the mainstream with a succession of well-reviewed television and film roles between 2004 and 2006. Among his roles in movies includes Ray, Lackawanna Blues, Crash, Four Brothers, Hustle & Flow, Get Rich or Die Tryin', Idlewild, The Brave One. Howard co-starred in Iron Man and reprised the role in the video game adaption. He was replaced in this role in the sequel Iron Man 2, by Academy Award nominee Don Cheadle (his Crash co-star). Howard was born in Chicago, Illinois and raised in Cleveland, Ohio. At 18, he moved to New York City to pursue an acting career. He had auditioned for The Cosby Show and was cast in the role and later, Howard had a principal role in a short-lived CBS sitcom, Tall Hopes. He went on to portray Jackie Jackson in The Jacksons: An American Dream, an ABC miniseries. Three years later, he made his big film break in 1995's Mr. Holland's Opus. He continued with television and movie roles and co-starred as Greg Sparks in the late-1990s television series Sparks. Howard also appeared in The Best Man in 1999, in Ashanti's music video for her 2002 single \"Foolish\", and in Mary J. Blige's video for \"Be Without You\". Howard also made an appearance on the TV series Family Matters. Howard has also worked as a film producer for the movie Pride. In 2008, Howard hosted the PBS' series Independent Lens. Also in 2008, he made his Broadway debut, playing Brick in an all-African-American production of Tennessee Williams' Cat on a Hot Tin Roof, directed by Debbie Allen. Later that year, Howard made a guest appearance in the short film For All Mankind directed by Daniel L. Clifton and in 2009, he starred in the movie Fighting. In 2010, Howard joined the cast of the new Law & Order: Los Angeles installment of the Law & Order franchise, wherein he will play Deputy District Attorney Joe Dekker. Terrence ventured into the music industry with his debut pop album, Shine through It, heavily inspired by popular soul singers such as Marvin Gaye and Curtis Mayfield, was released in September 2008. Howard, who lives outside Philadelphia in Lafayette Hill, He has three children from a previous marriage: two daughters (Aubrey and Heaven) and a son (Hunter).", + "social_ids": { + "twitter": "terrencehoward", + "facebook": "terrencehoward", + "instagram": "theterrencehoward", + "wikipedia": null + }, + "biography": "Terrence Dashon Howard (born March 11, 1969) is an American actor. Having his first major roles in the 1995 films Dead Presidents and Mr. Holland's Opus, Howard broke into the mainstream with a succession of television and cinema roles between 2004 and 2006. He was nominated for the Academy Award for Best Actor for his role in Hustle \u0026 Flow.\n\nHoward has had prominent roles in many other movies, including Winnie Mandela, Ray, Lackawanna Blues, Crash, Four Brothers, Big Momma's House, Get Rich or Die Tryin', Idlewild, Biker Boyz, August Rush, The Brave One, and Prisoners. Howard played James \"Rhodey\" Rhodes in the first Iron Man film. He starred as the lead character Lucious Lyon in the television series Empire. His debut album, Shine Through It, was released in September 2008.\n\nDescription above is from the Wikipedia article Terrence Howard, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1969-03-11", "death": null, - "birthplace": "Chicago, Illinois, USA ", - "homepage": null + "birthplace": "Chicago, Illinois, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { - "character": "Obadiah Stane, Iron Monger", + "character": "Obadiah Stane", "characters": [ - "Obadiah Stane", - "Iron Monger" + "Obadiah Stane" ], "person": { "name": "Jeff Bridges", @@ -56,19 +72,28 @@ "slug": "jeff-bridges", "imdb": "nm0000313", "tmdb": 1229, - "tvrage": 59067 + "tvrage": null + }, + "social_ids": { + "twitter": "TheJeffBridges", + "facebook": "JeffBridgesOfficial", + "instagram": "thejeffbridges", + "wikipedia": null }, - "biography": "American actor Jeff Bridges has starred in over 50 films during his lengthy career. He comes from a family of performers and was introduced to show business by his father Lloyd. Jeff is most famous for his work in the films Big Lebowski (1998), Seabiscuit (2003), Crazy Heart (2009), and most recently True Grit (2010). His accolades include a long list of Academy Award nominations, and one win for Best Actor in 2010.\n\nBridges was born in 1949 and grew up in Los Angeles, California. After high school Jeff traveled to New York to study acting at the famous Herbert Berghof Studio. In the late 1960's Jeff served in the US Coast Guard, and stayed on reserve through the early 1970's to avoid Vietnam. As a child Jeff got a start in acting through his father Lloyd, and even starred in two of his television shows. Jeff achieved major success early in his career when he was nominated for an Academy Award in his first major film, the 1971 drama Last Picture Show. Three years later he starred opposite of Clint Eastwood in Thunderbolt and Lightfoot.\n\nBy 1976 Bridges was casted in the first remake of King Kong, which turned out to be a huge commercial success raking in more than 90 million dollars. Over the next three decades Bridges went on to star in over 50 films and win countless awards for his work. More recently Jeff starred in the 2009 film Crazy Heart, which earned him his first Academy Award for Best Actor.\n\nIn 1977 Jeff married Susan Geston, the two met while filming Ranch Deluxe. Together they have three daughters.\n\nAlong with other entertainment industry leaders, in 1984 Bridges founded the End Hunger Network, a humanitarian effort which aims to eliminate childhood hunger in the US by 2015.\n\nAlong with his charitable work, Jeff is also known for his music and photography.", + "biography": "Jeffrey Leon Bridges (born December 4, 1949) is an American actor, singer, and producer. He comes from a prominent acting family and appeared on the television series Sea Hunt (1958–60), with his father, Lloyd Bridges and brother, Beau Bridges. He has won numerous accolades, including the Academy Award for Best Actor for his role as an alcoholic singer in the 2009 film Crazy Heart.\n\nBridges also earned Academy Award nominations for his roles in The Last Picture Show (1971), Thunderbolt and Lightfoot (1974), Starman (1984), The Contender (2000), True Grit (2010), and Hell or High Water (2016).\n\nDescription above from the Wikipedia article Jeff Bridges, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1949-12-04", "death": null, "birthplace": "Los Angeles, California, USA", - "homepage": "http://www.jeffbridges.com" + "homepage": "https://www.jeffbridges.com/", + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-21T08:06:13.000Z" } }, { - "character": "Virginia \"Pepper\" Potts", + "character": "Pepper Potts", "characters": [ - "Virginia \"Pepper\" Potts" + "Pepper Potts" ], "person": { "name": "Gwyneth Paltrow", @@ -77,13 +102,22 @@ "slug": "gwyneth-paltrow", "imdb": "nm0000569", "tmdb": 12052, - "tvrage": 58927 + "tvrage": null + }, + "social_ids": { + "twitter": "GwynethPaltrow", + "facebook": "GwynethPaltrowOfficial", + "instagram": "gwynethpaltrow", + "wikipedia": null }, - "biography": "Paltrow made her acting debut on stage in 1990 and started appearing in films in 1991. She gained early notice for her work in films such as Se7en (1995), Emma (1996), in which she played the title role, and Sliding Doors (1998). She garnered worldwide recognition through her performance in Shakespeare in Love (1998), for which she won the Academy Award for Best Actress, a Golden Globe Award and two Screen Actors Guild Awards, for Outstanding Lead Actress and as a member of the Outstanding Cast. Since then, Paltrow has portrayed supporting as well as lead roles in films such as The Talented Mr. Ripley (1999), Shallow Hal (2001) and Proof (2005), for which she earned a Golden Globe nomination as Best Actress in Motion Picture Drama. In 2008, she appeared in the highest grossing movie of her career, the superhero film Iron Man, and then reprised her role as Pepper Potts in its sequel, Iron Man 2 (2010). Paltrow has been the face of Estée Lauder's Pleasures perfume since 2005.", + "biography": "Gwyneth Kate Paltrow (/ˈpæltroʊ/ PAL-troh; born September 27, 1972) is an American actress and businesswoman. The daughter of filmmaker Bruce Paltrow and actress Blythe Danner, she established herself as a leading lady, appearing in mainly mid-budget and period films during the 1990s and early 2000s, before transitioning to blockbusters and franchises. Her accolades include an Academy Award, a Golden Globe Award, and a Primetime Emmy Award.\n\nPaltrow gained notice for her early work in films such as Seven (1995), Emma (1996), Sliding Doors (1998), and A Perfect Murder (1998). She garnered wider acclaim for her role as Viola de Lesseps in the historical romance Shakespeare in Love (1998), which earned her the Academy Award for Best Actress. This was followed by roles in The Talented Mr. Ripley (1999), The Royal Tenenbaums (2001), and Shallow Hal (2001). She made her West End debut in the David Auburn play Proof (2003), earning a Laurence Olivier Award for Best Actress nomination, and reprised the role in the 2005 film of the same name.\n\nAfter becoming a parent in 2004, Paltrow reduced her acting workload by making intermittent appearances in films such as Two Lovers (2008), Country Strong (2010), and Contagion (2011). Paltrow's career revived through her portrayal of Pepper Potts in the Marvel Cinematic Universe from Iron Man (2008) to Avengers: Endgame (2019). On television, she had a recurring guest role as Holly Holliday on the Fox musical television series Glee (2010–2011), for which she received the Primetime Emmy Award for Outstanding Guest Actress in a Comedy Series. After starring in the Netflix series The Politician (2019–2020), she took a break from acting.\n\nIn 2005 Paltrow became a \"face\" of Estée Lauder Companies; she was previously the face of the American fashion brand Coach. She is the founder and CEO of the lifestyle company Goop, which has been criticised for promoting pseudoscience, and has written several cookbooks. She received a Grammy Award nomination for Best Spoken Word Album for Children for the Brown Bear and Friends (2009). She hosted the documentary series The Goop Lab for Netflix in 2020.\n\nDescription above from the Wikipedia article Gwyneth Paltrow, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1972-09-27", "death": null, "birthplace": "Los Angeles, California, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { @@ -98,13 +132,22 @@ "slug": "leslie-bibb", "imdb": "nm0004753", "tmdb": 57451, - "tvrage": 35954 + "tvrage": null + }, + "social_ids": { + "twitter": "mslesliebibb", + "facebook": null, + "instagram": "mslesliebibb", + "wikipedia": null }, - "biography": "Leslie Louise Bibb (born November 17, 1974) is an American actress and former fashion model. She transitioned into film and television in late 1990s. She appeared in television shows such as Home Improvement (1996), before she appeared in her first film, the comedy Private Parts (1997), which was followed by her first show The Big Easy. She received a role in This Space Between Us (1999).\n\nHer role as Brooke McQueen on the WB Network comedy–drama series Popular (1999–2001) brought her to the attention of a wider audience, and received a Teen Choice Award for Television Choice Actress. During the series, she also gained recognition for her roles in the thriller The Skulls (2000) and in the comedy See Spot Run (2001). She had a recurring role in the medical–drama show ER (2002–2003).\n\nBibb was cast as Carley Bobby, in the comedy Talladega Nights: The Ballad of Ricky Bobby (2006). She had a minor supporting role in the hit action film Iron Man (2008) and appeared in the crime-thriller Law Abiding Citizen (2009). She reprised her role in the sequel to Iron Man, Iron Man 2 (2010). Recently, she joined the cast of the upcoming family–comedy Zookeeper (2011) as Miranda Davis.\n\nDescription above from the Wikipedia article  Leslie Bibb, licensed under CC-BY-SA, full list of contributors on Wikipedia.", - "birthday": "1974-11-17", + "biography": "Leslie Louise Bibb (born November 17, 1973) is an American actress and former fashion model. She transitioned into film and television in late 1990s. She appeared in television shows such as Home Improvement (1996), before she appeared in her first film, the comedy Private Parts (1997), which was followed by her first show The Big Easy. She received a role in This Space Between Us (1999).\n\nHer role as Brooke McQueen on the WB Network comedy–drama series Popular (1999–2001) brought her to the attention of a wider audience, and received a Teen Choice Award for Television Choice Actress. During the series, she also gained recognition for her roles in the thriller The Skulls (2000) and in the comedy See Spot Run (2001). She had a recurring role in the medical–drama show ER (2002–2003), Crossing Jordan, and on the sitcoms The League and American Housewife. She had a starring role on the drama GCB, the sitcom Burning Love and the fantasy series Jupiter's Legacy as well as God's Favorite Idiot.\n\nBibb was cast as Carley Bobby, in the comedy Talladega Nights: The Ballad of Ricky Bobby (2006). She had a minor supporting role in the hit action film Iron Man (2008) and appeared in the crime-thriller Law Abiding Citizen (2009). She reprised her role in the sequel Iron Man 2 (2010). She appeared in the family–comedy Zookeeper (2011) as Miranda Davis. She also appeared in Confessions of a Shopaholic, Law Abiding Citizen, A Good Old-Fashioned Orgy, Movie 43, No Good Deed, To the Bone, and Tag, among others.", + "birthday": "1973-11-17", "death": null, "birthplace": "Bismarck, North Dakota, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { @@ -119,13 +162,22 @@ "slug": "shaun-toub", "imdb": "nm0869467", "tmdb": 17857, - "tvrage": 22694 + "tvrage": null + }, + "social_ids": { + "twitter": "shauntoub", + "facebook": "ShaunToubOfficial", + "instagram": "shauntoub", + "wikipedia": null }, "biography": "Shaun Toub is an Iranian-American film and television actor. He is perhaps best known for his role as Farhad in the 2004 movie Crash, as Rahim Khan in the movie The Kite Runner, and as Yinsen in the film adaptation of the Iron Man comic book series.\n\nToub, who is of Persian Jewish background, was born in Tehran, Iran and raised in Manchester, England (his family left Iran before the 1979 revolution). At the age of fourteen, he moved to Switzerland and after a two year stay, he crossed the Atlantic to Nashua, New Hampshire to finish his last year of high school. His high school yearbook notes: \"The funniest guy in school and the most likely to succeed in the entertainment world.\" After two years of college in Massachusetts, Shaun transferred to USC.\n\nShaun is active in the Iranian Jewish community. Through various charity events and public speaking engagements, he inspires the community to embrace the arts, as the arts enhance everyday life. He has been a recipient of the Sephard award at the Los Angeles Sephardic Film Festival. Toub currently resides in Los Angeles.", - "birthday": "1963-01-01", + "birthday": "1958-02-15", "death": null, - "birthplace": "Tehran - Iran", - "homepage": null + "birthplace": "Tehran, Iran", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-13T08:10:25.000Z" } }, { @@ -142,17 +194,26 @@ "tmdb": 57452, "tvrage": null }, - "biography": "Faran Tahir is actor.", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Faran Haroon Tahir (Urdu: فاران ہارون طاہر) is an American actor.\n\nTahir made his film debut as Nathoo in Disney's 1994 film The Jungle Book. He went on to star in a variety of roles, such as Raza in Iron Man (2008), Captain Robau in Star Trek (2009), and President Patel in Elysium (2013). In 2016, he played the title role of Othello in a production by the Shakespeare Theatre Company in Washington, D.C.\n\nDescription above from the Wikipedia article Faran Tahir, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1963-02-16", "death": null, "birthplace": "Los Angeles, California, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T08:07:18.000Z" } }, { - "character": "Phil Coulson", + "character": "Agent Coulson", "characters": [ - "Phil Coulson" + "Agent Coulson" ], "person": { "name": "Clark Gregg", @@ -161,13 +222,22 @@ "slug": "clark-gregg", "imdb": "nm0163988", "tmdb": 9048, - "tvrage": 28033 + "tvrage": null + }, + "social_ids": { + "twitter": "clarkgregg", + "facebook": "clarkgreggofficial", + "instagram": "clarkgregg", + "wikipedia": null }, - "biography": "From Wikipedia, the free encyclopedia\n\nRobert Clark Gregg (born April 2, 1962) is an American actor, screenwriter and director. He co-starred as Christine Campbell's ex-husband Richard in the CBS sitcom The New Adventures of Old Christine, which debuted in March 2006 and concluded in May 2010. He is best known for playing Agent Phil Coulson in the Marvel Cinematic Universe.", + "biography": "Robert Clark Gregg Jr. (born April 2, 1962) is an American actor, director, and screenwriter. He is best known for portraying Phil Coulson in films and television series set in the Marvel Cinematic Universe from 2008 to 2021. Gregg also voiced Coulson in the animated television series Ultimate Spider-Man (2012–2017) and the video games Lego Marvel Super Heroes (2013), Marvel Heroes (2013), and Lego Marvel's Avengers (2016).\n\nGregg is also known for his role as FBI Special Agent Mike Casper on the NBC political drama series The West Wing (2001–2004) and as Richard, the ex-husband of Christine Campbell, in the CBS sitcom The New Adventures of Old Christine (2006–2010).\n\nHe wrote the horror film What Lies Beneath (2000) and wrote and directed the black comedy Choke (2008) and the comedy-drama Trust Me (2013). He appeared in the films The Adventures of Sebastian Cole (1998), One Hour Photo (2002), We Were Soldiers (2002), In Good Company (2004), When a Stranger Calls (2006), 500 Days of Summer (2009), Much Ado About Nothing (2012), The To Do List (2013), Labour Day (2013), Live by Night (2016), and Being the Ricardos (2021).\n\nDescription above from the Wikipedia article Clark Gregg, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1962-04-02", "death": null, - "birthplace": "Boston - Massachusetts - USA", - "homepage": null + "birthplace": "Boston, Massachusetts, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-20T21:48:35.000Z" } }, { @@ -182,13 +252,22 @@ "slug": "bill-smitrovich", "imdb": "nm0810488", "tmdb": 17200, - "tvrage": 15300 + "tvrage": null + }, + "social_ids": { + "twitter": "BillSmitrovich", + "facebook": null, + "instagram": null, + "wikipedia": null }, - "biography": "Bill Smitrovich (born June 16, 1947) is an American actor.", + "biography": "William Stanley Zmitrowicz Jr. (born May 16, 1947), known professionally as Bill Smitrovich, is an American actor.", "birthday": "1947-05-16", "death": null, "birthplace": "Bridgeport, Connecticut, USA", - "homepage": "http://www.billsmitrovich.com/" + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-01T08:07:32.000Z" } }, { @@ -203,19 +282,28 @@ "slug": "sayed-badreya", "imdb": "nm0046223", "tmdb": 173810, - "tvrage": 8046 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "Egyptian-born film-maker and actor Sayed Badreya realized a childhood dream by winning roles in major Hollywood films such as The Insider, Three Kings, and Independence Day. Growing up in poverty in Port Said, Sayed Badreya's dreams of movie stardom looked as bleak as the prospect of peace in the Middle East. From the Six Day War in '67 through the Yom Kippur War in '73, his only escape from the world he knew was the movie theater, where films transported him to a magical land. But it was here that he determined he was destined to be a part of that magic. After attending New York University film school, and then moving to Hollywood, Sayed first worked in the film industry as an assistant to actor/director Anthony Perkins, and then with director James Cameron on True Lies. His mission - to make movies that told the Arabic- American story, since it had yet to be told - led to the creation of his own production company, Zoom In Focus. Under this banner, he directed and produced the documentary, Saving Egyptian Film Classics as well as The Interrogation, which won Best Creative Short Film at New York International Film Festival. He also produced and starred in Hesham Issawi's short, T for Terrorist, which was awarded Best Short Film at the Boston International Film Festival and the San Francisco World Film Festival. In 2007, he played his first leading role in the English language motion picture American East, a film that he also co-wrote. 2008 was Sayed Badreya's breakout year. He captivated audiences as Abu Bakaar, the villainous arms dealer who kidnaps Tony Stark (Robert Downey Jr.) in the summer's blockbuster Iron Man. Also that year, Sayed played the comedic Palestinian cab driver opposite Adam Sandler in You Don't Mess with the Zohan. This summer, Sayed can be seen in Paramount Pictures' feature film El traspatio aka Backyard, directed by Oscar-nominated Carlos Carrera in which he plays a serial killer opposite Ana de la Reguera. Sayed can also be seen this summer in Movie 43, where he plays opposite Halle Berry. Additional forthcoming films include The Three Stooges, his fifth film with the Farrelly brothers; The Dictator, playing Sascha Baron Cohen's father as the original Dictator, and Just Like a Woman, with Oscar nominated director Rachid Bouchareb. Also, Sayed is going to a new frontier in the new video game Uncharted 3, playing Ramses the Great Pirate Captain. Most recently he completed his second leading role in the New York independent feature, Cargo, about human traffickers, directed by Yan Vizinberg. And he just finished co-starring opposite Oscar-nominated actress Melissa Leo in film The Space Between, directed by Travis Fine. Sayed has also worked as an actor, Arabic dialect coach, and Islamic technical advisor on Path to 9/11, a $40 million mini-series about the events leading up to 9/11 produced by ABC/Touchstone. Sayed's efforts to bring attention to Arab-Americans in the motion picture industry have received much coverage over the years on radio, television, and in major publications around the world, such as The New York Times, GQ, NPR, ABC's \"Politically Incorrect\" with Bill Maher, BBC's \"Panorama,\" CNN, \"Fox Report with Shepard Smith\", The Hollywood Reporter and Egypt Today.", "birthday": "1957-05-01", "death": null, "birthplace": "Port Said, Egypt", - "homepage": "http://www.sayedbadreya.com/" + "homepage": "http://www.sayedbadreya.com/", + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-26T08:08:54.000Z" } }, { - "character": "J.A.R.V.I.S. (voice)", + "character": "Jarvis (voice)", "characters": [ - "J.A.R.V.I.S. (voice)" + "Jarvis (voice)" ], "person": { "name": "Paul Bettany", @@ -224,19 +312,28 @@ "slug": "paul-bettany", "imdb": "nm0079273", "tmdb": 6162, - "tvrage": 59102 + "tvrage": null + }, + "social_ids": { + "twitter": "Paul_Bettany", + "facebook": "PaulBettanyOfficial", + "instagram": "paulbettany", + "wikipedia": null }, - "biography": "Paul Bettany was born into a theatre family. His father, Thane Bettany, is still an actor but his mother, Anne Kettle, has retired from acting. His maternal grandmother, Olga Gwynne, was a successful actress, while his maternal grandfather, Lesley Kettle, was a musician and promoter. He was brought up in North West London and, after the age of 9, in Hertfordshire (Brookmans Park). Immediately after finishing at Drama Centre, he went into the West End to join the cast of \"An Inspector Calls\", though when asked to go on tour with this play, he chose to stay in England.", + "biography": "Paul Bettany (born 27 May 1971) is an English actor. He is best known for his roles as J.A.R.V.I.S. and Vision in the Marvel Cinematic Universe, including the Disney+ miniseries WandaVision (2021), for which he garnered a Primetime Emmy Award nomination.\n\nBettany first gained popularity for appearing in the films Gangster No. 1 (2000), A Knight's Tale (2001), and A Beautiful Mind (2001). He was nominated for a BAFTA Award for playing Stephen Maturin in the film Master and Commander: The Far Side of the World (2003). Other films in which Bettany has appeared include Dogville (2003), Wimbledon (2004), The Da Vinci Code (2006), The Tourist (2010), Margin Call (2011), Legend (2015), and Solo: A Star Wars Story (2018). He made his directorial debut with the film Shelter (2014), which he also wrote and co-produced.\n\nIn television and theatre, Bettany has portrayed Ian Campbell, 11th Duke of Argyll, in the series A Very British Scandal and Andy Warhol in the play The Collaboration in the West End, which is set to transfer to Broadway.\n\nDescription above is from the Wikipedia article Paul Bettany, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1971-05-27", "death": null, - "birthplace": "Harlesden, London, England, UK ", - "homepage": null + "birthplace": "London, England, UK", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { - "character": "Harold \"Happy\" Hogan", + "character": "Hogan", "characters": [ - "Harold \"Happy\" Hogan" + "Hogan" ], "person": { "name": "Jon Favreau", @@ -245,13 +342,22 @@ "slug": "jon-favreau", "imdb": "nm0269463", "tmdb": 15277, - "tvrage": 29076 + "tvrage": null + }, + "social_ids": { + "twitter": "jon_favreau", + "facebook": "jonfavreau", + "instagram": "jonfavreau", + "wikipedia": null }, - "biography": "Jonathan Favreau (/ˈfævroʊ/; born October 19, 1966) is an American actor, director, producer, and screenwriter.\n\nAs an actor, he is known for roles in films such as \"Rudy\" (1993), \"Swingers\" (1996) (which he also wrote), \"Very Bad Things\" (1998), \"Daredevil\" (2003), \"The Break-Up\" (2006), \"Couples Retreat\" (2009), and \"Chef\" (2014) (which he also wrote and directed).\n\nHe has additionally directed the films \"Elf\" (2003), \"Zathura: A Space Adventure\" (2005), \"Iron Man\" (2008), \"Iron Man 2\" (2010), \"Cowboys & Aliens\" (2011), \"The Jungle Book\" (2016), and \"The Lion King\" (2019) and served as an executive producer on \"The Avengers\" (2012), \"Iron Man 3\" (2013), \"Avengers: Age of Ultron\" (2015), \"Avengers: Infinity War\" (2018) and \"Avengers: Endgame\" (2019). Favreau also portrays Happy Hogan in the Marvel Cinematic Universe and played Pete Becker during season three of the television sitcom \"Friends\". He produces films under his banner, Fairview Entertainment. The company has been credited as co-producers in most of Favreau's directorial ventures.\n\nDescription above from the Wikipedia article Jon Favreau, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "biography": "Jonathan Kolia Favreau (/ˈfævroʊ/ FAV-roh; born October 19, 1966) is an American filmmaker and actor. As an actor, Favreau has appeared in films such as Rudy (1993), PCU (1994), Swingers (1996), Very Bad Things (1998), Deep Impact (1998), The Replacements (2000), Daredevil (2003), The Break-Up (2006), Four Christmases (2008), Couples Retreat (2009), I Love You, Man (2009), People Like Us (2012), The Wolf of Wall Street (2013), and Chef (2014).\n\nAs a filmmaker, Favreau has been significantly involved with the Marvel Cinematic Universe. He directed, produced, and appeared as Happy Hogan in the films Iron Man (2008) and Iron Man 2 (2010). He also served as an executive producer for and/or appeared as the character in the films The Avengers (2012), Iron Man 3 (2013), Avengers: Age of Ultron (2015), Spider-Man: Homecoming (2017), Avengers: Infinity War (2018), Avengers: Endgame (2019), Spider-Man: Far From Home (2019), Spider-Man: No Way Home (2021), and Deadpool \u0026 Wolverine (2024).\n\nHe has also directed the films Elf (2003), Zathura: A Space Adventure (2005), Cowboys \u0026 Aliens (2011), Chef (2014), The Jungle Book (2016), The Lion King (2019), and The Mandalorian \u0026 Grogu (2026). Recently, Favreau has been known for his work on the Star Wars franchise with Dave Filoni, creating the Disney+ original series The Mandalorian (2019–present), which Filoni helped develop, with both serving as executive producers. Alongside Filoni, he serves as an executive producer on all of the show's spin-off series, including The Book of Boba Fett, Ahsoka, and the upcoming Skeleton Crew. He produces films under his production company banner, Fairview Entertainment, and also presented the variety series Dinner for Five and the cooking series The Chef Show.\n\nDescription above from the Wikipedia article Jon Favreau, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1966-10-19", "death": null, - "birthplace": "Queens, New York, USA", - "homepage": null + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { @@ -266,13 +372,22 @@ "slug": "peter-billingsley", "imdb": "nm0082526", "tmdb": 12708, - "tvrage": 73749 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "officialpeterb", + "wikipedia": null }, - "biography": "From Wikipedia, the free encyclopedia.  \n\nPeter Billingsley (born 16 April 1971), also known as Peter Michaelsen and Peter Billingsley-Michaelsen, is an American actor, director, and producer best known for his role as Ralphie in the 1983 movie A Christmas Story. He began his career as an infant, in television commercials.\n\nDescription above from the Wikipedia article Peter Billingsley licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "biography": "Peter Billingsley (born April 16, 1971), also known as Peter Michaelsen and Peter Billingsley-Michaelsen, is an American actor, director, and producer. His acting roles include Ralphie Parker in the 1983 movie A Christmas Story, Jack Simmons in The Dirt Bike Kid, Billy in Death Valley, and as Messy Marvin in Hershey's chocolate syrup commercials during the 1980s. He began his career as an infant in television commercials.\n\nDescription above from the Wikipedia article Peter Billingsley, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1971-04-16", "death": null, - "birthplace": "New York City, New York, US", - "homepage": null + "birthplace": "New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-21T08:10:50.000Z" } }, { @@ -287,13 +402,22 @@ "slug": "tim-guinee", "imdb": "nm0347375", "tmdb": 40275, - "tvrage": 27953 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "Tim Guinee (born November 18, 1962) is an American stage, television, and film actor.", "birthday": "1962-11-18", "death": null, "birthplace": "Los Angeles, California, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-04T08:08:39.000Z" } }, { @@ -308,13 +432,22 @@ "slug": "will-lyman", "imdb": "nm0528164", "tmdb": 163671, - "tvrage": 33553 + "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "William Lyman (born May 20, 1948) is an American actor. Being known for his polished, resonant voice, Lyman has narrated the PBS series Frontline since its second season in 1984 and played William Tell in the action/adventure television series Crossbow.", "birthday": "1948-05-20", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Burlington, Vermont, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T08:06:53.000Z" } }, { @@ -329,13 +462,22 @@ "slug": "tom-morello", "imdb": "nm0603780", "tmdb": 78299, - "tvrage": 210054 + "tvrage": null }, - "biography": "Tom Morello was born on May 30, 1964 in New York city. He has become an influential guitarist due to his work with Rage Against The Machine in the 1990s. Tom got his Social Studies degree from Harvard and went to L.A to start a band. He briefly played guitarist for a band named Lock Up with fellow Rage member Zack De La Rocha. IMDb Mini Biography By: Anonymous", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Tom Morello was born on May 30, 1964 in New York city. He has become an influential guitarist due to his work with Rage Against The Machine in the 1990s. Tom got his Social Studies degree from Harvard and went to L.A to start a band. He briefly played guitarist for a band named Lock Up with fellow Rage member Zack De La Rocha. IMDb Mini Biography.", "birthday": "1964-05-30", "death": null, "birthplace": "New York City, New York, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-16T08:07:12.000Z" } }, { @@ -352,11 +494,20 @@ "tmdb": 54809, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": "Tehran, Iran", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-28T08:08:42.000Z" } }, { @@ -373,11 +524,20 @@ "tmdb": 944830, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "dastankhalili", + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-05-28T08:08:15.000Z" } }, { @@ -386,19 +546,28 @@ "Guard" ], "person": { - "name": "Ido Ezra", + "name": "Ido Mor", "ids": { "trakt": 16285, - "slug": "ido-ezra", + "slug": "ido-mor", "imdb": "nm2789241", "tmdb": 1209417, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-20T08:03:01.000Z" } }, { @@ -415,11 +584,20 @@ "tmdb": 95698, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Las Vegas, Nevada, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-20T08:06:47.000Z" } }, { @@ -436,11 +614,20 @@ "tmdb": 1209418, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": "USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-20T08:03:01.000Z" } }, { @@ -457,11 +644,20 @@ "tmdb": 62037, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2025-02-21T08:07:16.000Z" } }, { @@ -476,13 +672,22 @@ "slug": "ahmed-ahmed", "imdb": "nm0014135", "tmdb": 183439, - "tvrage": 36506 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "", "birthday": "1970-06-27", "death": null, "birthplace": "Helwan, Egypt", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-05-30T08:10:00.000Z" } }, { @@ -499,11 +704,20 @@ "tmdb": 109669, "tvrage": null }, - "biography": "Born and raised in Kabul, Afghanistan, Fahim Fazli came to the United States as a refugee in his teens. He enjoyed a privileged childhood until the Russians invaded Afghanistan. As a young adult he supported the resistance and when he and his remaining family saw the opportunity they fled to Pakistan and then eventually to the United States. Fahim lives in Orange County, California. Fahim has written a memoir, Fahim Speaks, to be released in early 2012.", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "We are all born with the ability to dream. Somehow between childhood and adulthood people lose that ability. Fahim Fazli is \"A man of two worlds\" Afghanistan, the country of his birth, and the United States, the nation he adopted and learned to love. Fahim is also a man who escaped oppression, found his dream profession, and then Played it all forward by returning to Afghanistan as an interpreter with the U.S. Marines from 2009-2010. He came to the United States as a refugee in his teens. He enjoyed a privileged childhood until the Soviet Union invaded Afghanistan. As a young adult he supported the resistance and when he and his remaining family saw the opportunity they fled to Pakistan and then eventually to the United States. He moved to California with dreams of an acting career. Fahim wrote a memoir, Fahim Speaks, that was released in early 2012. \"Fahim Speaks\" received the 1st place for a biography from the Military Writers Society of America.\n\n- IMDb Mini Biography", "birthday": "1966-05-30", "death": null, "birthplace": "Kabul, Afghanistan", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-18T08:07:21.000Z" } }, { @@ -520,11 +734,20 @@ "tmdb": 104669, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-04-25T08:08:18.000Z" } }, { @@ -541,11 +764,20 @@ "tmdb": 1209419, "tvrage": null }, - "biography": "After serving for five years in the British Royal Navy as a Mine Clearance Diver, Tim came from England to Hollywood in 1992 to pursue his dream of a career as a stuntman. For the last 25 years, Tim has worked on most of the big budget action movies, and has been fortunate to have played a key role, stunt-wise in many of them. He has crashed, slid, jumped, and turned over cars, (“Mr & Mrs Smith”, “Dukes of Hazzard”, “Man of the House”, Fast & Furious 4), performed high falls and skydives; he has been strapped to an ejection seat during freefall (“Stealth” & Iron Man), BASE jumped from a moving car, (“xXx”), BASE jumped into Downtown L.A. (“Along Came Polly”), BASE jumped live into the 2004 Superbowl at Houston Reliant Stadium (in front of 80,000 people), flown his wingsuit over L.A. and landed in Paramount studios, (“World Stunt Awards”), Spent hours, days, & weeks underwater (“Sphere & “The Hunley” \"Man of Steel), been involved in extensive fight & weapons based work, with the finest fight choreographers in the business, (“300”), as well as all of the “nuts & bolts” of stunt work; falls, wire-work, crashes, fire burns, etc.\n\nDoubles: Brian Van Holt, Thomas Hayden Church, Daniel Bernhardt, Brian Thomson, Rupert Everett, Rhys Ifans, Dash Mihok.\n\nAwards & Achievements:\n\n2002 World Stunt Awards Nominee, Best Specialty Stunt: Swordfish\n\n2003 World Stunt Awards Winner, Best Specialty Stunt: xXx\n\n2003 World Stunt Awards Nominee, Best Overall Stunt: xXx\n\n2005 World Stunt Awards Nominee, Best Specialty Stunt: Along Came Polly\n\n2008 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble: 300\n\n2008 World Stunt Awards Winner, Best Fight: 300\n\n2009 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble\n\n2010 World Stunt Awards Nominee, Best Fire Stunt: Gamer\n\n2010 World Stunt Awards Nominee, Best Work With A Vehicle: Fast & Furious 4\n\n2001 World Stunt Awards Nominee, Best Water Work: Perfect Storm", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "After serving for five years in the British Royal Navy as a Mine Clearance Diver, Tim came from England to Hollywood in 1992 to pursue his dream of a career as a stuntman. For the last 25 years, Tim has worked on most of the big budget action movies, and has been fortunate to have played a key role, stunt-wise in many of them. He has crashed, slid, jumped, and turned over cars, (“Mr \u0026 Mrs Smith”, “Dukes of Hazzard”, “Man of the House”, Fast \u0026 Furious 4), performed high falls and skydives; he has been strapped to an ejection seat during freefall (“Stealth” \u0026 Iron Man), BASE jumped from a moving car, (“xXx”), BASE jumped into Downtown L.A. (“Along Came Polly”), BASE jumped live into the 2004 Superbowl at Houston Reliant Stadium (in front of 80,000 people), flown his wingsuit over L.A. and landed in Paramount studios, (“World Stunt Awards”), Spent hours, days, \u0026 weeks underwater (“Sphere \u0026 “The Hunley” \"Man of Steel), been involved in extensive fight \u0026 weapons based work, with the finest fight choreographers in the business, (“300”), as well as all of the “nuts \u0026 bolts” of stunt work; falls, wire-work, crashes, fire burns, etc.\n\nDoubles: Brian Van Holt, Thomas Hayden Church, Daniel Bernhardt, Brian Thomson, Rupert Everett, Rhys Ifans, Dash Mihok.\n\nAwards \u0026 Achievements:\n\n2002 World Stunt Awards Nominee, Best Specialty Stunt: Swordfish\n\n2003 World Stunt Awards Winner, Best Specialty Stunt: xXx\n\n2003 World Stunt Awards Nominee, Best Overall Stunt: xXx\n\n2005 World Stunt Awards Nominee, Best Specialty Stunt: Along Came Polly\n\n2008 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble: 300\n\n2008 World Stunt Awards Winner, Best Fight: 300\n\n2009 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble\n\n2010 World Stunt Awards Nominee, Best Fire Stunt: Gamer\n\n2010 World Stunt Awards Nominee, Best Work With A Vehicle: Fast \u0026 Furious 4\n\n2001 World Stunt Awards Nominee, Best Water Work: Perfect Storm", "birthday": null, "death": null, "birthplace": null, - "homepage": "http://www.timrigby.com/" + "homepage": "http://www.timrigby.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-16T08:11:43.000Z" } }, { @@ -562,11 +794,20 @@ "tmdb": 195442, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Russell Richardson is an American film actor.", "birthday": null, "death": null, - "birthplace": null, - "homepage": null + "birthplace": "USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-25T08:02:27.000Z" } }, { @@ -581,13 +822,22 @@ "slug": "nazanin-boniadi", "imdb": "nm2258164", "tmdb": 142213, - "tvrage": 186060 + "tvrage": null + }, + "social_ids": { + "twitter": "NazaninBoniadi", + "facebook": "OfficialNazaninBoniadi", + "instagram": "nazaninboniadi", + "wikipedia": null }, "biography": "Nazanin Boniadi was born in Tehran, Iran, at the height of the Iranian Revolution; her parents relocated to London shortly thereafter. She performed violin and ballet as a young girl.\n\nShe attended a private high school and later moved to the United States where she earned a bachelor's degree, with honors, in biological sciences from the University of California, Irvine. At UCI, she won the Chang Pin-Chun Undergraduate Research Award for molecular research involving cancer treatment and heart transplant rejection. She was also Assistant Editor-in-Chief of MedTimes, UCI's undergraduate medical newspaper.\n\nNazanin Boniadi is rapidly making her mark in both film and television. She co-starred as CIA analyst Fara Sherazi on seasons three and four of the Emmy and Golden Globe award-winning drama Homeland (2011), for which she shared a 2015 SAG Award nomination in the Outstanding Performance by an Ensemble in a Drama Series category. Boniadi appeared in the 2016 MGM-Paramount remake of Ben-Hur. Directed by Timur Bekmambetov, the film stars Ms. Boniadi in the female lead role of Esther opposite Jack Huston, Morgan Freeman and Toby Kebbell. She will next appear in a leading role opposite Armie Hammer and Dev Patel in Anthony Maras's Hotel Mumbai.\n\nAmong her many television credits, Boniadi portrayed Nora, a relatively longstanding love interest to Neil Patrick Harris's Barney Stinson, in seasons six and seven of How I Met Your Mother (2005). She also appeared as the notorious Adnan Salif in season three of Shonda Rhimes' hit political drama Scandal (2012). She will next star alongside J.K. Simmons in the original Starz series Counterpart (2017), created by Justin Marks and Executive Produced by Morten Tyldum.\n\nOn film, Boniadi appeared as Amira Ahmed in Jon Favreau's Iron Man (2008) and portrayed a young mother, Elaine, in Paul Haggis' The Next Three Days (2010). She also has several independent features to her credit.\n\nBorn in Tehran at the height of the Iranian Revolution, Boniadi's parents relocated to London, England, shortly thereafter, where she was raised with an emphasis on education. While she was involved in theatre early in life, Boniadi later decided she wanted to become a physician. She moved to the United States at the age of 19 to attend the University of California, Irvine, where she received her Bachelor's Degree, with Honors (Dean's Academic Achievement and Service Award) in Biological Sciences, and won the \"Chang Pin Chun\" Undergraduate Research Award for her work in heart-transplant rejection and cancer research.\n\nSwitching gears to pursue her first love, Boniadi then decided to study acting, which included training in Contemporary Drama at the Royal Academy of Dramatic Arts in London under the supervision of dramaturge Lloyd Trott.\n\nBoniadi is fluent in both English and Persian. She is a dedicated human rights activist. Boniadi served as a spokesperson for Amnesty International USA (AIUSA) 2009-2015, and continues to partner with the non-profit as an AIUSA Artist of Conscience.", "birthday": "1980-05-22", "death": null, "birthplace": "Tehran, Iran", - "homepage": "https://nazaninboniadi.com" + "homepage": "https://nazaninboniadi.com", + "known_for_department": "acting", + "gender": "female", + "updated_at": "2025-01-22T08:10:31.000Z" } }, { @@ -604,11 +854,20 @@ "tmdb": 1209702, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-01T08:02:02.000Z" } }, { @@ -625,11 +884,20 @@ "tmdb": 1209703, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-01T08:02:02.000Z" } }, { @@ -638,19 +906,28 @@ "Woman at Craps Table" ], "person": { - "name": "Stacy Stas", + "name": "Stacy Stas Hurst", "ids": { "trakt": 16343, - "slug": "stacy-stas", + "slug": "stacy-stas-hurst-16343", "imdb": "nm1680105", "tmdb": 183037, "tvrage": null }, - "biography": "Stacy Stas is an actress.", - "birthday": null, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Stacy Stas Hurst is an actress.", + "birthday": "1982-06-17", "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-06-17T08:01:32.000Z" } }, { @@ -667,11 +944,20 @@ "tmdb": 1209704, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-09-27T08:06:33.000Z" } }, { @@ -688,11 +974,20 @@ "tmdb": 214951, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2023-04-21T09:09:14.000Z" } }, { @@ -709,11 +1004,20 @@ "tmdb": 205362, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-10-20T08:06:35.000Z" } }, { @@ -730,11 +1034,20 @@ "tmdb": 203468, "tvrage": null }, + "social_ids": { + "twitter": "jimcramer", + "facebook": "JimCramerica", + "instagram": "jimcramer", + "wikipedia": null + }, "biography": "Jim Cramer is an American television personality, former hedge fund manager, and best-selling author, best known for hosting CNBC's television show _Mad Money_.", "birthday": "1955-02-10", "death": null, "birthplace": "Wyndmoor, Pennsylvania, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-07-27T08:07:31.000Z" } }, { @@ -751,11 +1064,20 @@ "tmdb": 939869, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Donna Evans is a stuntwoman and stunt double. She is the sister of Debbie Evans, who is also a stuntwoman.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2025-02-08T08:07:52.000Z" } }, { @@ -772,11 +1094,20 @@ "tmdb": 1209705, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-03-22T08:08:17.000Z" } }, { @@ -793,11 +1124,20 @@ "tmdb": 1209706, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-09-21T08:02:30.000Z" } }, { @@ -814,11 +1154,20 @@ "tmdb": 1209707, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", - "birthday": null, + "birthday": "2001-06-01", "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-10-31T08:12:00.000Z" } }, { @@ -835,11 +1184,20 @@ "tmdb": 1209708, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-01-14T08:16:42.000Z" } }, { @@ -856,11 +1214,20 @@ "tmdb": 1209709, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "calliecroughwell", + "wikipedia": null + }, "biography": "", "birthday": "1995-12-20", "death": null, - "birthplace": "California, - USA", - "homepage": null + "birthplace": "California, USA", + "homepage": "https://www.istunt.com/callie-croughwell-3853", + "known_for_department": "crew", + "gender": "female", + "updated_at": "2024-10-30T08:12:16.000Z" } }, { @@ -877,11 +1244,20 @@ "tmdb": 1209710, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-08-31T08:04:34.000Z" } }, { @@ -898,11 +1274,20 @@ "tmdb": 206423, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", - "birthday": null, + "birthday": "1973-03-25", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Iran", + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-01-30T23:49:06.000Z" } }, { @@ -919,11 +1304,20 @@ "tmdb": 133121, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": "1957-07-07", "death": null, "birthplace": "Norwalk, Connecticut, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-20T08:02:19.000Z" } }, { @@ -940,11 +1334,20 @@ "tmdb": 181895, "tvrage": null }, + "social_ids": { + "twitter": "adamharrington7", + "facebook": "adam.harrington.144", + "instagram": "adamharrington", + "wikipedia": null + }, "biography": "Adam Harrington is an American film and television actor and stand-up comedian.", "birthday": null, "death": null, "birthplace": "Wellesley Hills, Massachusetts, USA", - "homepage": "https://www.theadamharrington.com" + "homepage": "https://www.theadamharrington.com", + "known_for_department": "acting", + "gender": "male", + "updated_at": "2023-11-28T14:19:50.000Z" } }, { @@ -961,11 +1364,20 @@ "tmdb": 62843, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-11-27T08:25:46.000Z" } }, { @@ -982,11 +1394,20 @@ "tmdb": 204606, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-25T08:02:29.000Z" } }, { @@ -1003,11 +1424,20 @@ "tmdb": 210842, "tvrage": null }, - "biography": "Ricki Lander is an actress and producer.", - "birthday": null, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Ricki Noel Lander (born December 14, 1979) is an American actress, model, designer, and entrepreneur.", + "birthday": "1979-12-14", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Salt Lake City, Utah, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2025-01-26T08:08:54.000Z" } }, { @@ -1024,11 +1454,20 @@ "tmdb": 205720, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": "Columbus, Georgia, USA", - "homepage": "https://sunorstars.com/our-team/jeannine-kaspar/" + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-09-20T08:02:26.000Z" } }, { @@ -1045,11 +1484,20 @@ "tmdb": 1005698, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-09-25T08:03:31.000Z" } }, { @@ -1064,13 +1512,22 @@ "slug": "stan-lee", "imdb": "nm0498278", "tmdb": 7624, - "tvrage": 37350 + "tvrage": null + }, + "social_ids": { + "twitter": "TheRealStanLee", + "facebook": "realstanlee", + "instagram": "therealstanlee", + "wikipedia": null }, - "biography": "Stan Lee (born Stanley Martin Lieber, December 28, 1922 – November 12, 2018)  was an American comic book writer, editor, actor, producer, publisher, television personality, and the former president and chairman of Marvel Comics.\n\nIn collaboration with several artists, most notably Jack Kirby and Steve Ditko, he co-created Spider-Man, the Fantastic Four, the X-Men, the Avengers, Iron Man, the Hulk, Thor, Daredevil, Doctor Strange, and many other fictional characters, introducing complex, naturalistic characters and a thoroughly shared universe into superhero comic books. In addition, he headed the first major successful challenge to the industry's censorship organization, the Comics Code Authority, and forced it to reform its policies. Lee subsequently led the expansion of Marvel Comics from a small division of a publishing house to a large multimedia corporation.\n\nHe was inducted into the comic book industry's Will Eisner Comic Book Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995.", + "biography": "Stan Lee (born Stanley Martin Lieber /ˈliːbər/; December 28, 1922–November 12, 2018) was an American comic book writer, editor, publisher, and producer. He rose through the ranks of a family-run business called Timely Comics, which later became Marvel Comics. He was Marvel's primary creative leader for two decades, expanding it from a small publishing house division to a multimedia corporation that dominated the comics and film industries.\n\nIn collaboration with others at Marvel—particularly co-writers and artists Jack Kirby and Steve Ditko—he co-created iconic characters, including Spider-Man, the X-Men, Iron Man, Thor, the Hulk, Ant-Man, the Wasp, the Fantastic Four, Black Panther, Daredevil, Doctor Strange, the Scarlet Witch, and Black Widow. These and other characters' introductions in the 1960s pioneered a more naturalistic approach in superhero comics. In the 1970s, Lee challenged the restrictions of the Comics Code Authority, indirectly leading to changes in its policies. In the 1980s, he pursued the development of Marvel properties in other media, with mixed results.\n\nFollowing his retirement from Marvel in the 1990s, Lee remained a public figurehead for the company. He frequently made cameo appearances in films and television shows based on Marvel properties, on which he received an executive producer credit, which allowed him to become the person with the highest-grossing film total ever. He continued independent creative ventures until his death, aged 95, in 2018. Lee was inducted into the comic book industry's Will Eisner Award Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995. He received the NEA's National Medal of Arts in 2008.\n\nDescription above from the Wikipedia article Stan Lee, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1922-12-28", "death": "2018-11-12", "birthplace": "New York City, New York, USA", - "homepage": "http://therealstanlee.com/" + "homepage": "https://therealstanlee.com/", + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-02-15T08:09:58.000Z" } }, { @@ -1087,11 +1544,20 @@ "tmdb": 1209711, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-08-31T08:04:34.000Z" } }, { @@ -1108,11 +1574,20 @@ "tmdb": 90721, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2022-09-03T08:04:11.000Z" } }, { @@ -1127,13 +1602,22 @@ "slug": "lana-kinnear", "imdb": "nm0455698", "tmdb": 169681, - "tvrage": 49259 + "tvrage": null + }, + "social_ids": { + "twitter": "REALLANASTAR", + "facebook": "wowlana", + "instagram": "misslanaashleigh", + "wikipedia": null }, "biography": "Lana Kinnear is an American film and television actress and professional wrestler.", "birthday": "1976-07-29", "death": null, "birthplace": "Burbank, California, USA", - "homepage": "http://absolutelana.com" + "homepage": "http://absolutelana.com", + "known_for_department": "acting", + "gender": "female", + "updated_at": "2025-02-17T08:06:37.000Z" } }, { @@ -1150,11 +1634,20 @@ "tmdb": 1209712, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2022-05-23T08:09:38.000Z" } }, { @@ -1171,11 +1664,20 @@ "tmdb": 1209713, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-09-27T08:06:33.000Z" } }, { @@ -1190,13 +1692,22 @@ "slug": "gabrielle-tuite", "imdb": "nm0876266", "tmdb": 169642, - "tvrage": 58572 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, - "biography": "Gabrielle Tuite is an American actress and model best known for her five years on CBS's \"The Price is Right\". She has appeared on the covers of international magazines Maxim, Stuff, Muscle & Fitness, Iron Man, People and Men's Health.", + "biography": "Gabrielle Tuite is an American actress and model best known for her five years on CBS's \"The Price is Right\". She has appeared on the covers of international magazines Maxim, Stuff, Muscle \u0026amp; Fitness, Iron Man, People and Men's Health.", "birthday": "1977-12-03", "death": null, "birthplace": "Brooklyn, New York, USA", - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-07-16T08:01:36.000Z" } }, { @@ -1213,11 +1724,20 @@ "tmdb": 27031, "tvrage": null }, - "biography": "", - "birthday": null, + "social_ids": { + "twitter": "thetimgriffin", + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Tim Griffin is an American film and television actor. his more notable television roles are as Adam Hassler on Wayward Pines, Ron Kellaher on Aquarius, Ronny O'Malley on Grey's Anatomy, Seth Newman on Covert Affairs, and Detective Augie Blando on Prime Suspect.\n\nOften dubbed \"\"the actor who is in everything\", he has had an extensive career in television and film. Some of his film credits include Cloverfield, Leatherheads, The Men Who Stare at Goats, A Better Life, American Sniper, Super 8, Abduction, and Central Intelligence.", + "birthday": "1969-01-01", "death": null, "birthplace": "Chicago, Illinois, USA", - "homepage": "http://timgriffin.org/timgriffin.org/TIMGRIFFIN.ORG.html" + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-06-27T08:09:15.000Z" } }, { @@ -1232,13 +1752,22 @@ "slug": "joshua-harto", "imdb": "nm0367176", "tmdb": 34544, - "tvrage": 233807 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "", "birthday": "1979-01-09", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Huntington, West Virginia, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-31T08:10:54.000Z" } }, { @@ -1251,15 +1780,24 @@ "ids": { "trakt": 15575, "slug": "micah-a-hauptman", - "imdb": null, + "imdb": "nm1275916", "tmdb": 150669, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Micah Hauptman was born on December 26, 1973 in Philadelphia, Pennsylvania, USA. He is an actor and producer, known for Everest (2015), Homeland (2011) and Parker (2013).", "birthday": "1973-12-26", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Philadelphia, Pennsylvania, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-02T08:10:17.000Z" } }, { @@ -1274,98 +1812,22 @@ "slug": "james-bethea", "imdb": "nm1084018", "tmdb": 1209714, - "tvrage": 83621 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "James A. Bethea Jr. is an American television writer, producer and occasional performer.", "birthday": "1965-01-14", "death": null, "birthplace": "Harlem, New York City, New York, USA", - "homepage": null - } - }, - { - "character": "Nick Fury (uncredited), Nick Fury", - "characters": [ - "Nick Fury (uncredited)", - "Nick Fury" - ], - "person": { - "name": "Samuel L. Jackson", - "ids": { - "trakt": 9486, - "slug": "samuel-l-jackson", - "imdb": "nm0000168", - "tmdb": 2231, - "tvrage": 55720 - }, - "biography": "An American film and television actor and film producer. After Jackson became involved with the Civil Rights Movement, he moved on to acting in theater at Morehouse College, and then films. He had several small roles such as in the film Goodfellas, Def by Temptation, before meeting his mentor, Morgan Freeman, and the director Spike Lee. After gaining critical acclaim for his role in Jungle Fever in 1991, he appeared in films such as Patriot Games, Amos & Andrew, True Romance and Jurassic Park. In 1994 he was cast as Jules Winnfield in Pulp Fiction, and his performance received several award nominations and critical acclaim.\n\nJackson has since appeared in over 100 films including Die Hard with a Vengeance, The 51st State, Jackie Brown, Unbreakable, The Incredibles, Black Snake Moan, Shaft, Snakes on a Plane, as well as the Star Wars prequel trilogy and small roles in Quentin Tarantino's Kill Bill Vol. 2 and Inglourious Basterds. He played Nick Fury in Iron Man and Iron Man 2, Thor, the first two of a nine-film commitment as the character for the Marvel Cinematic Universe franchise. Jackson's many roles have made him one of the highest grossing actors at the box office. Jackson has won multiple awards throughout his career and has been portrayed in various forms of media including films, television series, and songs. In 1980, Jackson married LaTanya Richardson, with whom he has one daughter, Zoe.\n\nDescription above from the Wikipedia article Samuel L. Jackson, licensed under CC-BY-SA, full list of contributors on Wikipedia.", - "birthday": "1948-12-21", - "death": null, - "birthplace": "Washington, D.C., USA", - "homepage": null - } - }, - { - "character": "Photographer (uncredited)", - "characters": [ - "Photographer (uncredited)" - ], - "person": { - "name": "Jeffrey Ashkin", - "ids": { - "trakt": 15578, - "slug": "jeffrey-ashkin", - "imdb": "nm1644068", - "tmdb": 1209715, - "tvrage": null - }, - "biography": "", - "birthday": "1983-11-19", - "death": null, - "birthplace": "Brooklyn - New York - USA", - "homepage": null - } - }, - { - "character": "Georgio (uncredited)", - "characters": [ - "Georgio (uncredited)" - ], - "person": { - "name": "Russell Bobbitt", - "ids": { - "trakt": 15582, - "slug": "russell-bobbitt", - "imdb": "nm0090309", - "tmdb": 1004624, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Fireman's Wife (uncredited)", - "characters": [ - "Fireman's Wife (uncredited)" - ], - "person": { - "name": "Vianessa Castaños", - "ids": { - "trakt": 15583, - "slug": "vianessa-castanos", - "imdb": "nm3095343", - "tmdb": 984619, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-27T08:06:33.000Z" } }, { @@ -1382,95 +1844,80 @@ "tmdb": 1209716, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "Mike Cochrane is Actor and Stunts.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-09-19T08:43:33.000Z" } }, { - "character": "Dubai Beauty (uncredited)", + "character": "Reporter (uncredited)", "characters": [ - "Dubai Beauty (uncredited)" + "Reporter (uncredited)" ], "person": { - "name": "Crystal Marie Denha", + "name": "Flavia Manes Rossi", "ids": { - "trakt": 15586, - "slug": "crystal-marie-denha", - "imdb": "nm2407968", - "tmdb": 1209717, + "trakt": 15598, + "slug": "flavia-manes-rossi", + "imdb": "nm1098431", + "tmdb": 1089759, "tvrage": null }, - "biography": "", - "birthday": "1984-02-08", - "death": null, - "birthplace": "Detroit, Michigan, USA", - "homepage": null - } - }, - { - "character": "Dubai Girl (uncredited)", - "characters": [ - "Dubai Girl (uncredited)" - ], - "person": { - "name": "Mellany Gandara", - "ids": { - "trakt": 15588, - "slug": "mellany-gandara", - "imdb": "nm2081556", - "tmdb": 970218, - "tvrage": null + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2022-11-18T08:03:51.000Z" } }, { - "character": "House wife at Award Ceremony (uncredited)", + "character": "Nick Fury (uncredited)", "characters": [ - "House wife at Award Ceremony (uncredited)" + "Nick Fury (uncredited)" ], "person": { - "name": "Halla", + "name": "Samuel L. Jackson", "ids": { - "trakt": 15589, - "slug": "halla", - "imdb": "nm2955994", - "tmdb": 1209718, + "trakt": 9486, + "slug": "samuel-l-jackson", + "imdb": "nm0000168", + "tmdb": 2231, "tvrage": null }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Insurgent (uncredited)", - "characters": [ - "Insurgent (uncredited)" - ], - "person": { - "name": "Rodrick Hersh", - "ids": { - "trakt": 15590, - "slug": "rodrick-hersh", - "imdb": "nm2555437", - "tmdb": 1202546, - "tvrage": null + "social_ids": { + "twitter": "SamuelLJackson", + "facebook": "samuelljackson", + "instagram": "samuelljackson", + "wikipedia": null }, - "biography": "", - "birthday": null, + "biography": "Samuel Leroy Jackson (born December 21, 1948) is an American actor and producer. One of the most widely recognized actors of his generation, the films in which he has appeared have collectively grossed over $27 billion worldwide, making him the second highest-grossing actor of all time. The Academy of Motion Picture Arts and Sciences gave him an Academy Honorary Award in 2022 as \"A cultural icon whose dynamic work has resonated across genres and generations and audiences worldwide\".\n\nJackson started his career on stage making his professional theatre debut in Mother Courage and her Children in 1980 at The Public Theatre. From 1981 to 1983 he originated the role of Private Louis Henderson in A Soldier's Play Off-Broadway. He also originated the role of Boy Willie in August Wilson's The Piano Lesson in 1987 at the Yale Repertory Theatre. He returned to the play in the 2022 Broadway revival playing Doaker Charles. Jackson early film roles include Coming to America (1988), Goodfellas (1990), Patriot Games (1992), Juice (1992), True Romance (1993), and Jurassic Park (1993), Menace II Society (1993), and Fresh (1994). His collaborations with Spike Lee led to greater prominence with films such as School Daze (1988), Do the Right Thing (1989), Mo' Better Blues (1990), Jungle Fever (1991), Oldboy (2013), and Chi-Raq (2015).\n\nJackson's breakout role was in Quentin Tarantino's Pulp Fiction (1994) which earned him a BAFTA Award win and a nomination for the Academy Award for Best Supporting Actor. He further collaborated with Tarantino, acting in Jackie Brown (1997), Django Unchained (2012), and The Hateful Eight (2015). He's known for having appeared in a number of big-budget films, including Die Hard with a Vengeance (1995), A Time to Kill (1996), The Long Kiss Goodnight (1996), The Negotiator (1997), Deep Blue Sea (1999), Unbreakable (2000), Shaft (2000) and its reboot (2019), XXX (2002), S.W.A.T. (2003), Coach Carter (2005), Snakes on a Plane (2006), Kingsman: The Secret Service (2014), Kong: Skull Island (2017), and Glass (2019).\n\nHe also gained widespread recognition as the Jedi Mace Windu in the Star Wars prequel trilogy (1999–2005), later voicing the role in the animated film Star Wars: The Clone Wars (2008) and the video game Lego Star Wars: The Clone Wars (2011). With his permission, his likeness was used for the Ultimate version of the Marvel Comics character Nick Fury; he subsequently played Fury in 11 Marvel Cinematic Universe films, beginning with a cameo appearance in Iron Man (2008), as well as guest-starring in the television series Agents of S.H.I.E.L.D. He will reprise this role in the upcoming Disney+ series Secret Invasion, which is set to premiere on June 21, 2023. Jackson has provided his voice for several animated films, documentaries, television series, and video games, including Lucius Best / Frozone in the Pixar films The Incredibles (2004) and Incredibles 2 (2018).", + "birthday": "1948-12-21", "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Washington, D.C., USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-22T08:11:44.000Z" } }, { @@ -1481,764 +1928,5113 @@ "person": { "name": "Kristin J. Hooper", "ids": { - "trakt": 15592, - "slug": "kristin-j-hooper", + "trakt": 3136924, + "slug": "kristin-j-hooper-8808815a-ce4b-4009-9316-61707c9ace33", "imdb": "nm3923280", - "tmdb": 1209719, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Dubai Waiter (uncredited)", - "characters": [ - "Dubai Waiter (uncredited)" - ], - "person": { - "name": "Chris Jalandoni", - "ids": { - "trakt": 15593, - "slug": "chris-jalandoni", - "imdb": "nm1599104", - "tmdb": 1209720, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Party Guest (uncredited)", - "characters": [ - "Party Guest (uncredited)" - ], - "person": { - "name": "Stephen Janousek", - "ids": { - "trakt": 15594, - "slug": "stephen-janousek", - "imdb": "nm2999525", - "tmdb": 1209721, + "tmdb": 3955051, "tvrage": null }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Dancer in Ballroom (uncredited)", - "characters": [ - "Dancer in Ballroom (uncredited)" - ], - "person": { - "name": "Laura Liguori", - "ids": { - "trakt": 15597, - "slug": "laura-liguori", - "imdb": "nm2266584", - "tmdb": 1209722, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Reporter (uncredited)", - "characters": [ - "Reporter (uncredited)" - ], - "person": { - "name": "Flavia Manes Rossi", - "ids": { - "trakt": 15598, - "slug": "flavia-manes-rossi", - "imdb": "nm1098431", - "tmdb": 1089759, - "tvrage": null + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null - } - }, - { - "character": "Village Dad (uncredited)", - "characters": [ - "Village Dad (uncredited)" - ], - "person": { - "name": "Anthony Martins", - "ids": { - "trakt": 15601, - "slug": "anthony-martins", - "imdb": "nm1215266", - "tmdb": 1096679, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Reporter (uncredited)", - "characters": [ - "Reporter (uncredited)" - ], - "person": { - "name": "Robert McMurrer", - "ids": { - "trakt": 15602, - "slug": "robert-mcmurrer", - "imdb": "nm2258412", - "tmdb": 1209723, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Airforce Officer (uncredited)", - "characters": [ - "Airforce Officer (uncredited)" - ], - "person": { - "name": "James M. Myers", - "ids": { - "trakt": 15604, - "slug": "james-m-myers", - "imdb": "nm2653588", - "tmdb": 1209724, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Dubai Beauty #1 (uncredited)", - "characters": [ - "Dubai Beauty #1 (uncredited)" - ], - "person": { - "name": "America Olivo", - "ids": { - "trakt": 16432, - "slug": "america-olivo", - "imdb": "nm1760388", - "tmdb": 78434, - "tvrage": 31732 - }, - "biography": "From Wikipedia, the free encyclopedia.\n\nAmerica Olivo (a.k.a. America Campbell; born January 5, 1978) is an actress and singer most notable for her membership in the Spanish- & English-language band Soluna. Born in Los Angeles, she has multiple citizenships: US, Canadian, and Italian. Born to father Nello Olivo and mother Danica d'Hondt (Miss Canada 1959). She is currently appearing on Broadway as Arachne in Spider-Man: Turn Off the Dark.", - "birthday": "1978-01-05", - "death": null, - "birthplace": "Van Nuys, California, USA", - "homepage": "https://www.americaolivo.com/" - } - }, - { - "character": "Staff Sergeant (uncredited)", - "characters": [ - "Staff Sergeant (uncredited)" - ], - "person": { - "name": "Sylvette Ortiz", - "ids": { - "trakt": 15610, - "slug": "sylvette-ortiz", - "imdb": "nm3066181", - "tmdb": 1209725, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Journalist (uncredited)", - "characters": [ - "Journalist (uncredited)" - ], - "person": { - "name": "Brett Padelford", - "ids": { - "trakt": 15611, - "slug": "brett-padelford", - "imdb": "nm2703513", - "tmdb": 1209726, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Voice (uncredited)", - "characters": [ - "Voice (uncredited)" - ], - "person": { - "name": "Ajani Perkins", - "ids": { - "trakt": 15612, - "slug": "ajani-perkins", - "imdb": "nm3567687", - "tmdb": 1209727, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": "Oakland, California, USA", - "homepage": null - } - }, - { - "character": "Reporter (uncredited)", - "characters": [ - "Reporter (uncredited)" - ], - "person": { - "name": "Chris Reid", - "ids": { - "trakt": 15613, - "slug": "chris-reid", - "imdb": "nm1308942", - "tmdb": 1209728, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "News Cameraman (uncredited)", - "characters": [ - "News Cameraman (uncredited)" - ], - "person": { - "name": "Toi Rose", - "ids": { - "trakt": 15614, - "slug": "toi-rose", - "imdb": "nm3649434", - "tmdb": 1209729, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Rooftop Fireman (uncredited)", - "characters": [ - "Rooftop Fireman (uncredited)" - ], - "person": { - "name": "George F. Watson", - "ids": { - "trakt": 15616, - "slug": "george-f-watson", - "imdb": "nm1222064", - "tmdb": 1209730, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Whiplash One (voice) (uncredited)", - "characters": [ - "Whiplash One (voice) (uncredited)" - ], - "person": { - "name": "David Zyler", - "ids": { - "trakt": 15617, - "slug": "david-zyler", - "imdb": "nm0469487", - "tmdb": 1209731, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Reporter (uncredited)", - "characters": [ - "Reporter (uncredited)" - ], - "person": { - "name": "Nick W. Nicholson", - "ids": { - "trakt": 509352, - "slug": "nick-w-nicholson", - "imdb": "nm4414026", - "tmdb": 1429470, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "Waiter, Reporter (uncredited)", - "characters": [ - "Waiter", - "Reporter (uncredited)" - ], - "person": { - "name": "Elijah Samuel Quesada", - "ids": { - "trakt": 1259394, - "slug": "elijah-samuel-quesada", - "imdb": "nm6392685", - "tmdb": 2114972, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null - } - }, - { - "character": "", - "characters": [], - "person": { - "name": "Garrett Noel", - "ids": { - "trakt": 1791089, - "slug": "garrett-noel", - "imdb": null, - "tmdb": 2604526, - "tvrage": null - }, - "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2023-03-10T08:07:11.000Z" } } ], "crew": { - "production": [ + "crew": [ + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Vince Deadrick Jr.", + "ids": { + "trakt": 1128, + "slug": "vince-deadrick-jr", + "imdb": "nm0212604", + "tmdb": 12879, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1954-09-14", + "death": null, + "birthplace": "USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-12T08:08:22.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Loyd Catlett", + "ids": { + "trakt": 1713, + "slug": "loyd-catlett", + "imdb": "nm0146178", + "tmdb": 2277, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1953-05-14", + "death": null, + "birthplace": "Lubbock, Texas, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-13T08:05:51.000Z" + } + }, + { + "job": "Executive Music Producer", + "jobs": [ + "Executive Music Producer" + ], + "person": { + "name": "Hans Zimmer", + "ids": { + "trakt": 4615, + "slug": "hans-zimmer", + "imdb": "nm0001877", + "tmdb": 947, + "tvrage": null + }, + "social_ids": { + "twitter": "HansZimmer", + "facebook": "hanszimmer", + "instagram": "hanszimmer", + "wikipedia": null + }, + "biography": "Hans Florian Zimmer (German pronunciation: [ˈhans ˈfloːʁi̯aːn ˈtsɪmɐ]; born 12 September 1957) is a German film score composer and music producer. He has won two Oscars, four Grammys, and has been nominated for three Emmys and a Tony. Zimmer was also named on the list of Top 100 Living Geniuses, published by The Daily Telegraph in 2007.\n\nHis works are notable for integrating electronic music sounds with traditional orchestral arrangements. Since the 1980s, Zimmer has composed music for over 150 films. He has won two Academy Awards for Best Original Score for The Lion King (1994) and for Dune (2021). His works include Gladiator, The Last Samurai, the Pirates of the Caribbean series, The Dark Knight trilogy, Inception, Man of Steel, Interstellar, Dunkirk, No Time to Die, and the Dune series.\n\nZimmer spent the early part of his career in the United Kingdom before moving to the United States. He is the head of the film music division at DreamWorks Pictures and DreamWorks Animation studios and works with other composers through the company that he founded, Remote Control Productions, formerly known as Media Ventures. His studio in Santa Monica, California, has an extensive range of computer equipment and keyboards, allowing demo versions of film scores to be created quickly.\n\nZimmer has collaborated on multiple projects with directors including Christopher Nolan, Ridley Scott, Ron Howard, Gore Verbinski, Michael Bay, Guy Ritchie, Denis Villeneuve, and Tony Scott.\n\nDescription above from the Wikipedia article Hans Zimmer, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1957-09-12", + "death": null, + "birthplace": "Frankfurt am Main, Germany", + "homepage": "https://hans-zimmer.com", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-19T08:07:16.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Thomas Rosales Jr.", + "ids": { + "trakt": 9701, + "slug": "thomas-rosales-jr", + "imdb": "nm0740951", + "tmdb": 43010, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Thomas Rosales Jr. (born 3 February 1948) is an American stunt man who has appeared in more than one hundred and fifty movies. His first known appearance as a stuntman was in Battle for the Planet of the Apes in 1973. Rosales is arguably one of Hollywood's most recognizable stunt performers due to speaking roles, including ones where a film's protagonist wounds or kills him. His filmography includes RoboCop 2, The Crow, Tremors 2: Aftershocks, Universal Soldier, Predator 2, L. A. Confidential, Police Academy 2: Their First Assignment, U. S. Marshals, Deep Impact, The Running Man, The Hunter, Beverly Hills Cop III, Jurassic Park: The Lost World, Speed, and NCIS.", + "birthday": "1948-02-03", + "death": null, + "birthplace": "El Paso, Texas, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-16T08:06:56.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Chris Palermo", + "ids": { + "trakt": 11094, + "slug": "chris-palermo", + "imdb": "nm0657548", + "tmdb": 35546, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Chris has more than twenty years experience in all aspects of stunt work, specializing in vehicles.\n\nHis Awards \u0026 Achievements:\n\n2009 California Rally Champion G2\n\n2009 Rally America Southwest Champion G2\n\nCalifornia Rally Series: Rookie Of The Year\n\n4 Nominations: World Stunt Awards: Swordfish, Avengers, 22 Jumpstreet, Live By Night\n\n2 Wins: Sag Award, Best Stunt Ensemble, Star Trek 2010, Jason Bourne 2017\n\n6 Nominations: Sag Award, Best Stunt Ensemble 2008, 2009, 2010, 2013, 2016, 2017", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.palermoproductions.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-12T08:07:41.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mic Rodgers", + "ids": { + "trakt": 11711, + "slug": "mic-rodgers", + "imdb": "nm0734747", + "tmdb": 18889, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Mic's Experience: 40 years SAG Stunt Work, 30 years DGA 2nd Unit Dir; Mel Gibson's personal stunt double (1984-nov 1997).\n\nAwards \u0026 Achievements:\n\nAcademy Of Motion Picture Arts \u0026 Sciences Award For Technical Achievement\n\nWorld Taurus Stunt Awards- Best 2Nd Unit Dir/Stunt Coord - \"The Fast And The Furious\"\n\nWorld Taurus Stunt Awards- Best 2Nd Unit Dir/Stunt Coord - \"Fast And Furious 4\"", + "birthday": "1954-01-01", + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/mrodgers", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-22T12:46:34.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Max Daniels", + "ids": { + "trakt": 12140, + "slug": "max-daniels", + "imdb": "nm0200034", + "tmdb": 175600, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-01T08:08:14.000Z" + } + }, + { + "job": "Special Effects Coordinator", + "jobs": [ + "Special Effects Coordinator" + ], + "person": { + "name": "Daniel Sudick", + "ids": { + "trakt": 12238, + "slug": "daniel-sudick", + "imdb": "nm0837203", + "tmdb": 15356, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Dan Sudick is a special effects supervisor.\n\nSudick has been nominated for 13 Academy Awards for Best Visual Effects for Master and Commander: The Far Side of the World, War of the Worlds, Iron Man, Iron Man 2, The Avengers, Iron Man 3, Captain America: The Winter Soldier, Guardians of the Galaxy Vol. 2, Avengers: Infinity War, Avengers: Endgame, Free Guy, Spider-Man: No Way Home, and Black Panther: Wakanda Forever.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "visual effects", + "gender": "male", + "updated_at": "2025-02-19T08:05:24.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Craig Stecyk", + "ids": { + "trakt": 13519, + "slug": "craig-stecyk", + "imdb": "nm0824310", + "tmdb": 20072, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2024-09-23T08:01:47.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Freddie Hice", + "ids": { + "trakt": 13842, + "slug": "freddie-hice", + "imdb": "nm0382556", + "tmdb": 16618, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-06T08:27:13.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Eddie J. Fernandez", + "ids": { + "trakt": 14428, + "slug": "eddie-j-fernandez", + "imdb": "nm0272961", + "tmdb": 18300, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Eddie J. Fernandez is American award-winning stunt performer, martial artist, actor and stunt coordinator of over 300 films and television shows.", + "birthday": null, + "death": null, + "birthplace": "Chicago, Illinois, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-12T08:05:18.000Z" + } + }, + { + "job": "Stunt Coordinator", + "jobs": [ + "Stunt Coordinator" + ], + "person": { + "name": "Tim Rigby", + "ids": { + "trakt": 15499, + "slug": "tim-rigby", + "imdb": "nm0726601", + "tmdb": 1209419, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "After serving for five years in the British Royal Navy as a Mine Clearance Diver, Tim came from England to Hollywood in 1992 to pursue his dream of a career as a stuntman. For the last 25 years, Tim has worked on most of the big budget action movies, and has been fortunate to have played a key role, stunt-wise in many of them. He has crashed, slid, jumped, and turned over cars, (“Mr \u0026 Mrs Smith”, “Dukes of Hazzard”, “Man of the House”, Fast \u0026 Furious 4), performed high falls and skydives; he has been strapped to an ejection seat during freefall (“Stealth” \u0026 Iron Man), BASE jumped from a moving car, (“xXx”), BASE jumped into Downtown L.A. (“Along Came Polly”), BASE jumped live into the 2004 Superbowl at Houston Reliant Stadium (in front of 80,000 people), flown his wingsuit over L.A. and landed in Paramount studios, (“World Stunt Awards”), Spent hours, days, \u0026 weeks underwater (“Sphere \u0026 “The Hunley” \"Man of Steel), been involved in extensive fight \u0026 weapons based work, with the finest fight choreographers in the business, (“300”), as well as all of the “nuts \u0026 bolts” of stunt work; falls, wire-work, crashes, fire burns, etc.\n\nDoubles: Brian Van Holt, Thomas Hayden Church, Daniel Bernhardt, Brian Thomson, Rupert Everett, Rhys Ifans, Dash Mihok.\n\nAwards \u0026 Achievements:\n\n2002 World Stunt Awards Nominee, Best Specialty Stunt: Swordfish\n\n2003 World Stunt Awards Winner, Best Specialty Stunt: xXx\n\n2003 World Stunt Awards Nominee, Best Overall Stunt: xXx\n\n2005 World Stunt Awards Nominee, Best Specialty Stunt: Along Came Polly\n\n2008 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble: 300\n\n2008 World Stunt Awards Winner, Best Fight: 300\n\n2009 Screen Actors Guild Awards Nominee, Outstanding Performance By A Stunt Ensemble\n\n2010 World Stunt Awards Nominee, Best Fire Stunt: Gamer\n\n2010 World Stunt Awards Nominee, Best Work With A Vehicle: Fast \u0026 Furious 4\n\n2001 World Stunt Awards Nominee, Best Water Work: Perfect Storm", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.timrigby.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-16T08:11:43.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Donna Evans", + "ids": { + "trakt": 15526, + "slug": "donna-evans", + "imdb": "nm0262719", + "tmdb": 939869, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Donna Evans is a stuntwoman and stunt double. She is the sister of Debbie Evans, who is also a stuntwoman.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2025-02-08T08:07:52.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Anthony Martins", + "ids": { + "trakt": 15601, + "slug": "anthony-martins", + "imdb": "nm1215266", + "tmdb": 1096679, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-08-16T08:09:09.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Jeff Dashnaw", + "ids": { + "trakt": 17310, + "slug": "jeff-dashnaw", + "imdb": "nm0202002", + "tmdb": 23285, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Jeffrey James Dashnaw is an American award-winning stunt coordinator and stunt performer with over 35 years experience in the stunt business. He is married to fellow stunt coordinator/stunt performer Tracy Keehn-Dashnaw; they have 5 children: their sons J.J., Chad, and Jake are also stunt coordinators/stunt performers. Their son Nick was a stunt performer until his death in 2016.", + "birthday": "1956-04-01", + "death": null, + "birthplace": "Los Angeles, California, USA", + "homepage": "http://www.brandxstunts.org/jdashnaw", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-14T08:01:16.000Z" + } + }, + { + "job": "Additional Music", + "jobs": [ + "Additional Music" + ], + "person": { + "name": "Ryeland Allison", + "ids": { + "trakt": 24367, + "slug": "ryeland-allison", + "imdb": "nm0021485", + "tmdb": 30871, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2024-10-25T08:09:50.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Jason Rodriguez", + "ids": { + "trakt": 31019, + "slug": "jason-rodriguez", + "imdb": "nm0718264", + "tmdb": 75617, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Doubles: Ron Livingston, Paul Rudd.\n\nAwards \u0026 Achievements:\n\nNational Collegiate Rodeo Assoc. Top 20, Calf Roping/Team Roping\n\nBest Director 168 Film Festival - \"Day Of Reckoning\"\n\nBest Dramatic Short, International Family Film Festival Hollywood - \"Day Of Reckoning\" - Director\n\nBest Narrative, San Antonio Independent Christian Film Festival - Director\n\nTaurus Stunt Award Winner \"Best Fire” (2002)\n\nTaurus Stunt Award Nominee \"Best Work With Animal\"", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/jrodriguez", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-01T08:10:03.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mario Roberts", + "ids": { + "trakt": 32176, + "slug": "mario-roberts", + "imdb": "nm0731390", + "tmdb": 166543, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Mario Roberts (born April 4, 1952) is a stuntman and actor.", + "birthday": "1952-04-04", + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-07T08:06:49.000Z" + } + }, + { + "job": "Additional Music", + "jobs": [ + "Additional Music" + ], + "person": { + "name": "Atli Örvarsson", + "ids": { + "trakt": 38322, + "slug": "atli-orvarsson", + "imdb": "nm0651414", + "tmdb": 72268, + "tvrage": null + }, + "social_ids": { + "twitter": "AtliOrvarsson", + "facebook": null, + "instagram": "atliorvarsson", + "wikipedia": null + }, + "biography": "", + "birthday": "1970-07-07", + "death": null, + "birthplace": "Akureyri, Iceland", + "homepage": "https://www.atliorvarsson.com/", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-01T08:07:50.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Geo Corvera", + "ids": { + "trakt": 38654, + "slug": "geo-corvera-38654", + "imdb": "nm1559220", + "tmdb": 53254, + "tvrage": null + }, + "social_ids": { + "twitter": "geocorvera", + "facebook": null, + "instagram": "1latingeo", + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-12-23T08:08:54.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "J.J. Perry", + "ids": { + "trakt": 45222, + "slug": "j-j-perry", + "imdb": "nm0675102", + "tmdb": 131532, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "J.J. Perry (born October 25, 1967) is a stunt performer, martial artist, film director, film producer, stunt coordinator and film actor who has made several uncredited appearances on 24.", + "birthday": "1967-10-25", + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-19T08:08:45.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Clay Cullen", + "ids": { + "trakt": 45895, + "slug": "clay-cullen", + "imdb": "nm1247999", + "tmdb": 60536, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Working in the stunt buisness since 1996.\n\nDoubles: Charlie Hunnam, Frank Grillo, Kim Coats, Martin Henderson, Peter Facinelli, Robert Carradine.\n\nAwards \u0026 Achievements:\n\n— Red Bull Watercraft Surf Challenge Champion (Ranked One Of Top Surf Riders Worldwide)\n\n— Pro Jet Ski Rookie Of The Year\n\n— Taurus Award For Best Specialty Stunt (2004; Boat Work Itailian Job)\n\n— Inductee Internatinal Jet Ski Boating Association Hall Of Fame (2008)", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/ccullen", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-03T08:05:20.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Rosine 'Ace' Hatem", + "ids": { + "trakt": 47292, + "slug": "rosine-ace-hatem", + "imdb": "nm0368819", + "tmdb": 62568, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Rosine \"Ace\" Hatem (born August 5, 1960) is a stunt performer and actress.", + "birthday": "1960-08-05", + "death": null, + "birthplace": "Methuen, Massachusetts, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-11-23T08:11:09.000Z" + } + }, + { + "job": "Post Production Supervisor", + "jobs": [ + "Post Production Supervisor" + ], + "person": { + "name": "Luminita Docan", + "ids": { + "trakt": 48380, + "slug": "luminita-docan", + "imdb": "nm3029176", + "tmdb": 1265249, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-06-10T08:07:56.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mark Kubr", + "ids": { + "trakt": 48450, + "slug": "mark-kubr", + "imdb": "nm0473578", + "tmdb": 193946, + "tvrage": null + }, + "social_ids": { + "twitter": "markkubr", + "facebook": null, + "instagram": "markkubr", + "wikipedia": null + }, + "biography": "Raised by Czech parents in America, Mark Kubr was taught to be disciplined in arts, culture, athletics and religion. Kubr's unique world-view gives him a talent for maintaining lasting friendships with people from diverse cultures.\n\nHis career began just out of high school. While bagging groceries at the local market, an actress neighbor told Mark he had what it takes for entertainment and drove him to her agency in Los Angeles. Kubr signed with the agent and immediately began working as a print model and commercial actor, booking campaigns for Marlboro, Versace, Gucci and Ralph Lauren traveling the world. He always brought his cameras with him to build his photography portfolio. His photography has been published in Vogue, Cosmopolitan and Bride magazines.\n\nHe continued modeling and acting while pursuing his education after being awarded a full athletic scholarship. His acting and athletic ability was a perfect fit for stuntwork. Kubr is Mickey Rourke's stunt double.", + "birthday": null, + "death": null, + "birthplace": "USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-07T08:11:13.000Z" + } + }, + { + "job": "Stunt Coordinator", + "jobs": [ + "Stunt Coordinator" + ], + "person": { + "name": "Keith Woulard", + "ids": { + "trakt": 54113, + "slug": "keith-woulard", + "imdb": "nm0941887", + "tmdb": 92486, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Keith Woulard is a Stunts, Actor, Second Unit Director and Assistant Director.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-21T08:01:23.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Gene Hartline", + "ids": { + "trakt": 54663, + "slug": "gene-hartline", + "imdb": "nm0366905", + "tmdb": 184792, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-01T08:07:14.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Freddy Bouciegues", + "ids": { + "trakt": 55961, + "slug": "freddy-bouciegues", + "imdb": "nm1569291", + "tmdb": 179861, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-19T08:09:02.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Bob Brown", + "ids": { + "trakt": 56074, + "slug": "bob-brown", + "imdb": "nm0113136", + "tmdb": 146352, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Robert 'Bob' Francis Brown is an American stunt coordinator, stunt performer, stunt double, second unit director, and world champion high diver. He has doubled for Kevin Costner, Nicolas Cage, Kevin Spacey, Arnold Schwarzenegger, Jim Carrey, Sam Neill, Robert Patrick, and Brent Spiner.\n\nBrown received three Taurus World Stunt Award nominations: in 2002, in the category Best Driving for his work on the action thriller Swordfish, shared with Scott Rogers, Gilbert Combs, Brett A. Jones, and Mike Justus, in 2005, in the category Best Specialty Stunt for his work on the action drama Flight of the Phoenix, and in 2008, in the category Best High Work on the thriller The Number 23.\n\nIn 2011, he won a Taurus World Stunt Award in the category Best High Work on the science fiction prequel Predators, shared with Jeremy Fitzgerald. Brown was also part of the stunt team which received Screen Actors Guild Award nominations in the category Outstanding Performance by a Stunt Ensemble in a Motion Picture for his work on the comic adaptation Iron Man in 2009 and on the science fiction sequel Transformers: Revenge of the Fallen in 2010.\n\nHe has worked as a stunt double for Brent Spiner in the series Star Trek: The Next Generation - pilot episode \"Encounter at Farpoint\" (1987), for Robert Patrick in Terminator 2: Judgment Day (1991), for Arnold Schwarzenegger in Last Action Hero (1993), for Jim Carrey in The Mask (1994) and The Cable Guy (1996), for Kevin Spacey in Se7en (1995), for William Baldwin in Fair Game (1995), for Kevin Costner in Waterworld (1995), for Nicolas Cage in Face/Off (1997), for Sam Neill in Jurassic Park III (2001), for Rory Cochrane in the television series CSI: Miami (2003), and for Gregory Sims in NCIS: Los Angeles (2009).\n\nHe was a member of the United States High Diving Team (1985-1991). His diving awards \u0026 achievements include Mediterranean Cup High Diving Champion (1996) and World High Diving Champion (1991, 1989, 1988). He was also a National \u0026 World Age Group Trampoline Champion (1974).\n\nHe has been married to stuntwoman Marta Merrifield since 1996 and they have 3 children.", + "birthday": "1959-01-01", + "death": null, + "birthplace": null, + "homepage": "http://bobbrownaction.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-08T08:16:39.000Z" + } + }, + { + "job": "Additional Music", + "jobs": [ + "Additional Music" + ], + "person": { + "name": "Bobby Tahouri", + "ids": { + "trakt": 70383, + "slug": "bobby-tahouri", + "imdb": "nm1692616", + "tmdb": 124715, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "sound", + "gender": "male", + "updated_at": "2024-09-29T08:09:18.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Richard Bucher", + "ids": { + "trakt": 89284, + "slug": "richard-bucher", + "imdb": "nm0118150", + "tmdb": 1130026, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-31T08:10:29.000Z" + } + }, + { + "job": "Choreographer", + "jobs": [ + "Choreographer" + ], + "person": { + "name": "Alison Faulk", + "ids": { + "trakt": 93988, + "slug": "alison-faulk", + "imdb": "nm0269035", + "tmdb": 101687, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Alison grew up in South Florida with musician parents. She found her love of dance at an early age and it hasn’t let up. She moved to Los Angeles to study on a Joe Tremaine Scholarship and learn about as many dance styles as she could. This lead to a successful dance career in film, television, and with artists such as Janet Jackson, Britney Spears, Missy Elliot and Miley Cyrus. She is a proud member of the all female hip-hop crew The Beat Freaks, as well as the Groovaloos. Some of her choreography credits include P!NK, Madonna, Jennifer Lopez and Ricky Martin. She has choreographed episodes of Mindy Project, Lip Sync Battle, and The Sing Off. She loves film and to date has worked on 10 features. Along with her partners Teresa and Luke, she choreographed both Magic Mike films and collaborated with Channing Tatum on the creation of Magic Mike Live.", + "birthday": "1977-01-08", + "death": null, + "birthplace": "Pembroke Pines, Florida, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2023-12-29T00:42:30.000Z" + } + }, + { + "job": "Additional Music", + "jobs": [ + "Additional Music" + ], + "person": { + "name": "Lorne Balfe", + "ids": { + "trakt": 119208, + "slug": "lorne-balfe-7bd543fb-13a3-4040-a49b-e3aba4da7fff", + "imdb": "nm1154632", + "tmdb": 929145, + "tvrage": null + }, + "social_ids": { + "twitter": "Lornebalfe", + "facebook": "lornebalfemusic", + "instagram": "lornebalfe", + "wikipedia": null + }, + "biography": "Lorne Balfe (born 23 February 1976) is a Scottish composer and record producer of film, television, and video game scores.\n\nA veteran of Hans Zimmer's Remote Control Productions, Balfe's scoring credits include the films Megamind, Penguins of Madagascar, Home, Terminator Genisys, 13 Hours: The Secret Soldiers of Benghazi, The Lego Batman Movie, Mission: Impossible – Fallout, and its sequel. Mission: Impossible – Dead Reckoning Part One, Bad Boys for Life, and its sequel Bad Boys: Ride or Die, Black Widow, Black Adam, Dungeons \u0026 Dragons: Honour Among Thieves, Gran Turismo, and Beverly Hills Cop: Axel F, as well as the video games Assassin's Creed: Revelations, Assassin's Creed III, Crysis 2, Skylanders, and Call of Duty: Modern Warfare 2. He has also scored the television series The Bible, Marcella, The Crown, and Genius, the latter for which he earned a nomination for a Primetime Emmy Award for Outstanding Original Main Title Theme Music. He also collaborates with the directors Michael Bay, Chris McKay, Christopher McQuarrie, Adil El Arbi, and Bilall Fallah, and Mikael Håfström.\n\nHe composed the new fanfare for Skydance Productions, transcribed as There’s a World, There’s a Moon. Balfe also composed the Annapurna Pictures deep note opening logo.\n\nBalfe was born in Inverness, Scotland. He went to Fettes College in Edinburgh, where he had a music scholarship.\n\nDescription above from the Wikipedia article Lorne Balfe, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1976-02-23", + "death": null, + "birthplace": "Inverness, Scotland, UK", + "homepage": "http://lornebalfe.com", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-22T08:12:50.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Jon Braver", + "ids": { + "trakt": 136969, + "slug": "jon-braver", + "imdb": "nm0106004", + "tmdb": 183933, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-11-02T10:25:06.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Matt Leonard", + "ids": { + "trakt": 136971, + "slug": "matt-leonard", + "imdb": "nm0502706", + "tmdb": 1205742, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Matt Leonard is a stunt performer and actor. He played defensive tackle for the Jacksonville Jaguars from 2003-2005.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/mattleonard", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-12-07T08:02:14.000Z" + } + }, + { + "job": "Stunt Coordinator", + "jobs": [ + "Stunt Coordinator" + ], + "person": { + "name": "Thomas Robinson Harper", + "ids": { + "trakt": 143902, + "slug": "thomas-robinson-harper", + "imdb": "nm1329724", + "tmdb": 1339453, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/tharper", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-20T08:08:08.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Luke LaFontaine", + "ids": { + "trakt": 149689, + "slug": "luke-lafontaine", + "imdb": "nm0480971", + "tmdb": 225626, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": null, + "updated_at": "2025-01-28T08:11:21.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Brian Simpson", + "ids": { + "trakt": 150454, + "slug": "brian-simpson", + "imdb": "nm0800923", + "tmdb": 180838, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Brian Simpson was born in Pittsburgh, Pennsylvania, the youngest of 6 children. He went to Upper St. Clair HS for two years, then moved to California in 1981 to finish high school at South Pasadena HS. He played Jr. college football before getting into the film industry. He has been a member of the International Stunt Association for the past 10 years. IMDb Mini Biography By: Brian Simpson", + "birthday": "1965-08-18", + "death": null, + "birthplace": "Pittsburgh, Pennsylvania, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-20T08:06:48.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Tim Trella", + "ids": { + "trakt": 168242, + "slug": "tim-trella", + "imdb": "nm0871839", + "tmdb": 200402, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/ttrella", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-10-31T08:07:04.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Theo Kypri", + "ids": { + "trakt": 178330, + "slug": "theo-kypri", + "imdb": "nm0477465", + "tmdb": 202955, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Former 3x National Champion and 8th in the World trampolinist turned stunt professional Theo Kypri has worked on some of Hollywood's biggest grossing movies and has stunt doubled some of the biggest stars in the world. Now as stunt coordinator, action designer, and second unit action director, he brings realistic, original, and creative action sequences to both the big and small screens.", + "birthday": null, + "death": null, + "birthplace": "London, England, UK", + "homepage": "https://www.istunt.com/theo-kypri", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-11T08:05:23.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Tom Elliott", + "ids": { + "trakt": 257169, + "slug": "tom-elliott", + "imdb": "nm0254657", + "tmdb": 1099845, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-19T08:09:12.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Gilbert Rosales", + "ids": { + "trakt": 299791, + "slug": "gilbert-rosales", + "imdb": "nm0740962", + "tmdb": 179887, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Gilbert Rosales was born on November 11, 1969 in El Paso, Texas, USA. He is an actor, known for Iron Man (2008), Starship Troopers (1997) and Falling Down (1993).", + "birthday": "1969-11-11", + "death": null, + "birthplace": "El Paso, Texas, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-26T08:10:15.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Daniel Arrias", + "ids": { + "trakt": 381903, + "slug": "daniel-arrias", + "imdb": "nm1765074", + "tmdb": 1340940, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Daniel Jon Arrias (born 2 April, 1967) is a stuntman, stunt actor, and stunt coordinator who performed stunts as stunt double for Zachary Quinto in Star Trek.", + "birthday": "1967-04-02", + "death": null, + "birthplace": "Los Angeles, California, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-20T08:06:47.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Ray Siegle", + "ids": { + "trakt": 381904, + "slug": "ray-siegle", + "imdb": "nm0797075", + "tmdb": 1340941, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-05-28T08:07:16.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Greg Fitzpatrick", + "ids": { + "trakt": 385354, + "slug": "greg-fitzpatrick", + "imdb": "nm0280525", + "tmdb": 1347144, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": "New Brunswick, New Jersey, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-11-25T08:08:50.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Richard Cetrone", + "ids": { + "trakt": 412263, + "slug": "richard-cetrone", + "imdb": "nm0149150", + "tmdb": 12371, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1961-07-13", + "death": null, + "birthplace": "Pittsburgh, Pennsylvania, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-20T08:06:47.000Z" + } + }, + { + "job": "Special Effects", + "jobs": [ + "Special Effects" + ], + "person": { + "name": "Stan Winston", + "ids": { + "trakt": 412281, + "slug": "stan-winston", + "imdb": "nm0935644", + "tmdb": 60261, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "From Wikipedia, the free encyclopedia.\n\nStanley Winston (April 7, 1946 – June 15, 2008) was an American visual effects supervisor, makeup artist, and film director. He was best known for his work in the Terminator series, the Jurassic Park series, Aliens, the Predator series, Iron Man, Edward Scissorhands, Avatar and Enthiran. He won four Academy Awards for his work.\n\nWinston, a frequent collaborator with director James Cameron, owned several effects studios, including Stan Winston Digital. The established areas of expertise for Winston were in makeup, puppets and practical effects, but he had recently expanded his studio to encompass digital effects as well.\n\nDescription above from the Wikipedia article Stan Winston, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1946-04-07", + "death": "2008-06-15", + "birthplace": "Arlington, Virginia, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-06T15:15:56.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Kevin Scott", + "ids": { + "trakt": 413731, + "slug": "kevin-scott-9060556d-9cf2-4858-8e0b-30b8606648b3", + "imdb": "nm0779447", + "tmdb": 1012973, + "tvrage": null + }, + "social_ids": { + "twitter": "", + "facebook": "1175322341", + "instagram": "", + "wikipedia": null + }, + "biography": "Kevin Scott is an American Stunt Coordinator, Second Unit Director and Stunts Performer.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.kevincscott.com", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-20T08:05:42.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Brian Brown", + "ids": { + "trakt": 420074, + "slug": "brian-brown-c3bc31e8-6cb0-47e4-a96d-be74095ffe8b", + "imdb": "nm0113156", + "tmdb": 1204219, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Brian Brown was born on March 15, 1963 in Waco, Texas, USA. He is an actor, known for The Ballad of Buster Scruggs (2018), Iron Man (2008) and Cowboys \u0026 Aliens (2011).", + "birthday": "1963-03-15", + "death": null, + "birthplace": "Waco, Texas, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-06-21T08:08:05.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Joe Bucaro III", + "ids": { + "trakt": 421202, + "slug": "joe-bucaro-iii", + "imdb": "nm0117851", + "tmdb": 51302, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Joe grew up in Park Ridge and Wheaton, Illinois, for 27 years. Ever since watching Evel Knievel live at the old Chicago amphitheater jump 10 buses at age 8 and seeing Hooper (1978) in the theaters as an impressionable 13-year-old boy, he knew he wanted to become a Hollywood Stuntman.\n\nAfter many odd jobs, sales was his niche, but stunts always loomed in the background. He was a successful food broker, selling steak and seafood door to door throughout the Chicagoland area. When a stunt school opened in Chicago in 1988, Joe jumped at the opportunity. From there, he honed his stunt skills and learned about the motion picture industry, hustling sets and working as an extra. After several years he hustled the sets and got his big break doubling for Steven Seagal and hasn't looked back. Since then, Joe has also doubled for Arnold Schwarzenegger, Will Ferrell, John Cusack, Jeff Goldblum, David Hasselhoff, Dan Marino, and Vince Vaughn. He has been nominated twice for the World Stunt Awards and has won two Emmy awards for best action sequences for television.", + "birthday": "1964-04-04", + "death": null, + "birthplace": "Wheaton, Illinois, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-04T08:12:21.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Chad Randall", + "ids": { + "trakt": 424290, + "slug": "chad-randall", + "imdb": "nm0709555", + "tmdb": 1391129, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-23T08:01:31.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Ben Hernandez Bray", + "ids": { + "trakt": 424311, + "slug": "ben-hernandez-bray", + "imdb": "nm0004554", + "tmdb": 59674, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Ben Hernandez Bray is a film and television director who was born in Los Angeles, California and raised in one of the toughest San Fernando Valley neighborhoods. He's the oldest of six children, raised by his Mother and Grandmother and is of Mexican and Irish descent. In the late 1980's his boxing skills led him into the stunt industry where he then became one of the very few successful latino stuntmen in Hollywood and eventually becoming one of the top action stunt coordinators and second unit directors, working specifically with \"Smoking Aces\" Joe Carnahan and \"Silver Linings Playbook\" David O Russell. After twenty five plus years in the industry and over one hundred fifty film and tv credits, Mr Bray made his television directorial debut in 2015 with Katie Heigl's tv series \"State of Affairs\" for NBC/Universal, he then went on to direct episodes for Fox/Bruckheimer, CW/Greg Berlanti and ABC/Freeform. In 2018 will be Bray's feature film debut which he co-wrote with Joe Carnahan called \"El Chicano\" a Mexican Super hero story about two brothers growing up in East L.A. The story was inspired and originally written by Bray 10 years earlier after losing his youngest brother to gang violence. The film is being produced by War Party Productions and Lorenzo di Bonaventura.", + "birthday": null, + "death": null, + "birthplace": "Los Angeles, California, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-18T08:07:21.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Brian Machleit", + "ids": { + "trakt": 425212, + "slug": "brian-machleit", + "imdb": "nm0532646", + "tmdb": 1391698, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1972-10-26", + "death": null, + "birthplace": "St. Clair Shores, Michigan, USA", + "homepage": "https://brianmachleit.com", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-26T08:08:27.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Mike Justus", + "ids": { + "trakt": 425517, + "slug": "mike-justus-425517", + "imdb": "nm0433242", + "tmdb": 1393339, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-21T08:09:57.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Oakley Lehman", + "ids": { + "trakt": 443968, + "slug": "oakley-lehman", + "imdb": "nm1172599", + "tmdb": 1391327, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Doubles: Paul Walker.\n\nAwards \u0026 Achievements:\n\n2012 Taurus Stunt Award Best High Work\n\n2012 Taurus Stunt Award Best Work With Vehicle", + "birthday": "1975-11-20", + "death": null, + "birthplace": "Los Angeles County, California, USA", + "homepage": "http://www.brandxstunts.org/members/olehman", + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-19T08:09:00.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Steve Holladay", + "ids": { + "trakt": 479987, + "slug": "steve-holladay", + "imdb": "nm0390585", + "tmdb": 1404906, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/sholladay", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-06-09T08:08:40.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "James M. Halty", + "ids": { + "trakt": 482013, + "slug": "james-m-halty", + "imdb": "nm0357127", + "tmdb": 1400371, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1949-01-01", + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-17T08:06:36.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Michael Hilow", + "ids": { + "trakt": 499949, + "slug": "michael-hilow", + "imdb": "nm0385190", + "tmdb": 1420973, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-01-20T08:09:40.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Anthony Kramme", + "ids": { + "trakt": 502737, + "slug": "anthony-kramme", + "imdb": "nm0469717", + "tmdb": 1423547, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-11-17T08:16:27.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Nito Larioza", + "ids": { + "trakt": 507173, + "slug": "nito-larioza", + "imdb": "nm1045914", + "tmdb": 1427711, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Nito Larioza is a stunt performer and an actor.", + "birthday": "1971-02-21", + "death": null, + "birthplace": "Wahiawa - Hawaii - USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-20T08:08:13.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Robert Alonzo", + "ids": { + "trakt": 525497, + "slug": "robert-alonzo", + "imdb": "nm0022315", + "tmdb": 1444239, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "dynamicactiondesign", + "wikipedia": null + }, + "biography": "Robert Alonzo’s journey through the film business began early on in the city of Angels. He started with a passion for animation at age six, when he began drawing flip pages of his favourite cartoons. He focused on this craft until he was introduced to martial arts at an early age. Martial arts would ultimately turn into a lifelong obsession for Robert. He trained incessantly and eventually earned black belts in Tae Kwon Do and Hapkido, training under Grandmaster Jun Chong.\n\nRobert’s devotion to martial arts was fueled by his hunger for knowledge of all martial arts styles and disciplines. This passion led him to broaden his training, become proficient in multiple martial arts disciplines, and compete and excel in various international martial arts competitions.\n\nAlthough Robert had a deep passion for martial arts, he never forgot his childhood dreams of filmmaking. After graduating from Loyola High School in Los Angeles, he received an academic scholarship to attend Loyola Marymount University (LMU), where he earned his Bachelor of Arts degree in Studio Arts and Communication Studies while minoring in Animation and Business Marketing. This is where Robert trained under Dan McLaughlin and made his first hand-drawn animated senior thesis film titled “The Dive of Life.\" After graduating from LMU, Robert dabbled in the commercial world as a junior art director at Envision Group, but ultimately his love for martial arts and film came calling and led him to become a stunt performer.", + "birthday": "1970-08-04", + "death": null, + "birthplace": "Manila, Philippines", + "homepage": "https://www.dynamicactiondesign.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-20T08:11:12.000Z" + } + }, + { + "job": "Stunts", + "jobs": [ + "Stunts" + ], + "person": { + "name": "Sandy Berumen", + "ids": { + "trakt": 539105, + "slug": "sandy-berumen", + "imdb": "nm0078499", + "tmdb": 1457926, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2024-11-23T08:11:06.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "J.J. Dashnaw", + "ids": { + "trakt": 539996, + "slug": "j-j-dashnaw", + "imdb": "nm7495544", + "tmdb": 1458444, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "J.J. Dashnaw is an American stunt coordinator and stunt performer. His parents are also stunt coordinators and stunt performers, Jeff Dashnaw and Tracy Keehn-Dashnaw, as well as his brothers Chad Dashnaw and Jake Dashnaw. His brother Nick Dashnaw was a stunt performer until his death in 2016.", + "birthday": "1982-10-31", + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/jjdashnaw", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-20T08:08:27.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Daniel Stevens", + "ids": { + "trakt": 563162, + "slug": "daniel-stevens", + "imdb": "nm1174894", + "tmdb": 1502956, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "danielstevens1", + "wikipedia": null + }, + "biography": "Daniel Stevens is known for Avengers: Endgame (2019), Black Panther (2018), and Iron Man (2008).", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "https://instabio.cc/danielstevens", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-11-13T18:26:51.000Z" + } + }, + { + "job": "Stunt Double", + "jobs": [ + "Stunt Double" + ], + "person": { + "name": "Clay Donahue Fontenot", + "ids": { + "trakt": 563163, + "slug": "clay-donahue-fontenot", + "imdb": "nm0285047", + "tmdb": 1291797, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Clay Donahue Fontenot is known for Blade II (2002), Iron Man (2008) and Logan (2017).", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/cfontenot", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-09T08:08:56.000Z" + } + }, + { + "job": "Military Consultant", + "jobs": [ + "Military Consultant" + ], + "person": { + "name": "Harry Humphries", + "ids": { + "trakt": 564308, + "slug": "harry-humphries-0f8aa012-ab2a-4195-973a-c0eb7c936e9a", + "imdb": "nm0402051", + "tmdb": 1554033, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Harry R. Humphries is a former United States Navy SEAL, who is a producer, military consultant and actor.\n\nAfter graduating from Admiral Farragut Academy and attending Rutgers University in New Jersey, he joined the Navy in 1958. Soon after joining, he completed UDTR (Underwater Demolition Team Replacement) Class 29 and graduated as Honor man. He was assigned to UDT 22. After working with UDT 22 for some time in 1965 to 1967, he volunteered for and was accepted into SEAL Team Two. In 1971, he left the Navy with an Honorable Discharge.\n\nAfter a career with Henkel KGaA, the German Multi National Chemical Company, he moved to California, where he started Global Study Group, Inc. (\"GSGI\"). He works full-time as a Security Consultant and Entertainment Technical Adviser/Actor/Producer.", + "birthday": "1940-11-17", + "death": null, + "birthplace": "Kearny, New Jersey, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-12T08:05:45.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Rex Reddick", + "ids": { + "trakt": 574163, + "slug": "rex-reddick", + "imdb": "nm0714699", + "tmdb": 1531529, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.rexreddick.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-16T08:07:07.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Norm Compton", + "ids": { + "trakt": 579604, + "slug": "norm-compton", + "imdb": "nm0174063", + "tmdb": 1391805, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-20T08:08:03.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Michael Runyard", + "ids": { + "trakt": 581871, + "slug": "michael-runyard", + "imdb": "nm0750354", + "tmdb": 1535124, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Michael has 39 years experience in the movie industry as Stuntman, Actor, Stunt Coordinator, 2nd Unit Director, Safety Advisor, Action Arranger, etc.\n\nDoubles: Michael Douglas, Robert Urich, Harrison Ford, Sean Connery, Roger Moore, Robin Williams, Adam Sandler, Matthew Modine, Christopher Walken, and many more.\n\nAwards:\n\nAma 500Cc National Motocross Champion, 1973 1St American\n\nCanadian (Cma) 250Cc \u0026 500Cc National Motocross Champion, 1974 \u0026 1975", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.brandxstunts.org/members/mrunyard", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-11-23T08:10:52.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Chris Carnel", + "ids": { + "trakt": 621531, + "slug": "chris-carnel", + "imdb": "nm0138691", + "tmdb": 1502542, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-26T08:11:00.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mark Aaron Wagner", + "ids": { + "trakt": 633124, + "slug": "mark-aaron-wagner", + "imdb": "nm0906010", + "tmdb": 1622657, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1975-11-03", + "death": null, + "birthplace": "Fullerton, California, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-12-06T08:09:53.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Dino Dos Santos", + "ids": { + "trakt": 640046, + "slug": "dino-dos-santos", + "imdb": "nm1326778", + "tmdb": 1627986, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1968-07-18", + "death": null, + "birthplace": "Paraná, Brazil", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-19T08:09:05.000Z" + } + }, + { + "job": "Stunt Driver", + "jobs": [ + "Stunt Driver" + ], + "person": { + "name": "Tammie Baird", + "ids": { + "trakt": 666224, + "slug": "tammie-baird", + "imdb": "nm1941679", + "tmdb": 1647187, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": "Livermore, California, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "female", + "updated_at": "2024-04-07T08:01:44.000Z" + } + }, + { + "job": "Stunts", + "jobs": [ + "Stunts" + ], + "person": { + "name": "Alvin Zalamea", + "ids": { + "trakt": 668004, + "slug": "alvin-zalamea", + "imdb": "nm1917938", + "tmdb": 1635225, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-01-15T08:09:35.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Matt Baker", + "ids": { + "trakt": 697878, + "slug": "matt-baker-638e2993-b0ad-45d2-8eb8-4fa5f1a7ad7f", + "imdb": "nm1290743", + "tmdb": 1673003, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-30T08:10:37.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "David Ott", + "ids": { + "trakt": 711852, + "slug": "david-ott", + "imdb": "nm2605709", + "tmdb": 1687703, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-03T08:10:26.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mark Chavarria", + "ids": { + "trakt": 720555, + "slug": "mark-chavarria", + "imdb": null, + "tmdb": 1680212, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1968-09-30", + "death": "2017-11-09", + "birthplace": "Houston, Texas, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-06T08:27:17.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "David Castillo", + "ids": { + "trakt": 725202, + "slug": "david-castillo-035457a0-fb2f-4dba-b6b8-b5f100e94080", + "imdb": "nm0145045", + "tmdb": 1696636, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1972-09-13", + "death": null, + "birthplace": "Bellflower, California, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-08-05T12:33:38.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Paul Eliopoulos", + "ids": { + "trakt": 782609, + "slug": "paul-eliopoulos", + "imdb": "nm0253639", + "tmdb": 1737945, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-27T08:08:35.000Z" + } + }, + { + "job": "Post Production Assistant", + "jobs": [ + "Post Production Assistant" + ], + "person": { + "name": "John Bartnicki", + "ids": { + "trakt": 808006, + "slug": "john-bartnicki", + "imdb": "nm2962362", + "tmdb": 1760516, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2024-01-02T15:06:59.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Laurence Todd Rosenthal", + "ids": { + "trakt": 814958, + "slug": "laurence-todd-rosenthal", + "imdb": "nm0742787", + "tmdb": 1766719, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-01-06T08:27:17.000Z" + } + }, + { + "job": "Stunt Driver", + "jobs": [ + "Stunt Driver" + ], + "person": { + "name": "Joost Janssen", + "ids": { + "trakt": 864304, + "slug": "joost-janssen", + "imdb": "nm7425457", + "tmdb": 1797772, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-14T08:07:22.000Z" + } + }, + { + "job": "Stunts", + "jobs": [ + "Stunts" + ], + "person": { + "name": "Victor Winters-Junco", + "ids": { + "trakt": 903701, + "slug": "victor-winters-junco", + "imdb": "nm2261724", + "tmdb": 1516873, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-03T08:05:20.000Z" + } + }, + { + "job": "Additional Music", + "jobs": [ + "Additional Music" + ], + "person": { + "name": "Clay Duncan", + "ids": { + "trakt": 921123, + "slug": "clay-duncan", + "imdb": null, + "tmdb": 1837416, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2023-03-22T08:06:42.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Hannah Kozak", + "ids": { + "trakt": 938294, + "slug": "hannah-kozak", + "imdb": null, + "tmdb": 1852746, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1960-07-26", + "death": null, + "birthplace": "Chatsworth, California, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2025-02-21T08:07:26.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Tad Griffith", + "ids": { + "trakt": 952530, + "slug": "tad-griffith", + "imdb": "nm0341498", + "tmdb": 1866579, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Screen Actors Guild Awards Nominee:\n\n2008 Outstanding Performance By A Stunt Ensemble In A Motion Picture: The Kingdom\n\n2008 Outstanding Performance By A Stunt Ensemble In A Motion Picture: 300\n\n2010 World Stunt Awards Winner — Best Work With A Vehicle: Fast And Furious 4\n\nWorld Stunt Awards Nominee:\n\n2005 Best Work With A Vehicle: Spiderman II\n\n2004 Best Stunt By A Stuntman: Seabiscuit\n\n2004 Best Specialty Stunt: Seabiscuit\n\n2004 Best Stunt By A Stuntman: Cradle 2 The Grave\n\n2004 Best Work With A Vehicle: Cradle 2 The Grave\n\n2002 Best Fire Stunt: Last Castle\n\n2002 Best Work With An Animal: American Outlaws\n\nOther Awards \u0026 Achievements:\n\n— World Champion Trick Rider\n\n— Third Generation Professional Rodeo Cowboy Association Contestant \u0026 Performer", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "http://www.atadwest.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-01T08:08:14.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Richie Gaona", + "ids": { + "trakt": 982104, + "slug": "richie-gaona", + "imdb": null, + "tmdb": 1893551, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": "Texas, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-20T08:05:42.000Z" + } + }, + { + "job": "Stunt Driver", + "jobs": [ + "Stunt Driver" + ], + "person": { + "name": "Craig Rondell", + "ids": { + "trakt": 1022435, + "slug": "craig-rondell", + "imdb": null, + "tmdb": 1931656, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": null, + "updated_at": "2023-06-18T08:07:47.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Greg Anthony", + "ids": { + "trakt": 1050216, + "slug": "greg-anthony-e17b22ce-571c-43e1-b315-89d57dc79a6b", + "imdb": null, + "tmdb": 1957590, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-07-06T08:05:59.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Annie Ellis", + "ids": { + "trakt": 1067510, + "slug": "annie-ellis", + "imdb": "nm0254695", + "tmdb": 1973515, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Stuntwoman Annie Ellis is a second generation Stuntwoman. Her father Richard Ellis was a well-known and beloved stuntman who raced motorcycles, was a well known surfer, and who went from stuntman and stunt coordinator to a feature film director.\n\nAnnie specializes in horse work, water work, fights and car work. Her resume includes thousands of TV credits and over 150 top films.", + "birthday": "1956-12-11", + "death": null, + "birthplace": "Los Angeles County, California, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2025-01-26T08:08:52.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Riley Harper", + "ids": { + "trakt": 1118940, + "slug": "riley-harper", + "imdb": "nm0363947", + "tmdb": 2020105, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "lifeof_riley", + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": "Los Angeles, California, USA", + "homepage": "http://www.lifeof-riley.com/", + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-06T08:27:17.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Anderson Martin", + "ids": { + "trakt": 1119962, + "slug": "anderson-martin", + "imdb": "nm0551906", + "tmdb": 2020611, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-03T08:08:19.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "John T. Cypert", + "ids": { + "trakt": 1120305, + "slug": "john-t-cypert", + "imdb": "nm0194229", + "tmdb": 2020623, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-05-09T08:08:25.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Keith Splinter Davis", + "ids": { + "trakt": 1122889, + "slug": "keith-splinter-davis-9acc9b96-254c-40c2-a394-e72dde8a3f5d", + "imdb": "nm0204952", + "tmdb": 1578364, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-01-26T08:10:14.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Kevin Derr", + "ids": { + "trakt": 1152289, + "slug": "kevin-derr", + "imdb": "nm1232110", + "tmdb": 2052022, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1981-04-13", + "death": null, + "birthplace": "Hollywood, California, USA", + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-12-12T08:01:38.000Z" + } + }, + { + "job": "Stunts", + "jobs": [ + "Stunts" + ], + "person": { + "name": "Sebastiano Cartier", + "ids": { + "trakt": 1412501, + "slug": "sebastiano-cartier", + "imdb": null, + "tmdb": 2224820, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-02-09T08:08:32.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Paul Sklar", + "ids": { + "trakt": 1455065, + "slug": "paul-sklar", + "imdb": null, + "tmdb": 2274724, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-01-19T08:09:10.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Krisztian Kery", + "ids": { + "trakt": 1479004, + "slug": "krisztian-kery", + "imdb": "nm1445682", + "tmdb": 2299884, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2023-06-18T08:08:45.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Todd Warren", + "ids": { + "trakt": 1485021, + "slug": "todd-warren-0c1ae5e9-8297-4b43-89b6-8fbef2f873d7", + "imdb": "nm0913060", + "tmdb": 2306648, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-12-11T08:08:28.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Glenn Goldstein", + "ids": { + "trakt": 1592203, + "slug": "glenn-goldstein", + "imdb": null, + "tmdb": 2409990, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-05-06T08:08:28.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Darryl Reeves", + "ids": { + "trakt": 1637711, + "slug": "darryl-reeves", + "imdb": null, + "tmdb": 2451620, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-02-16T08:07:07.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mike Rufino", + "ids": { + "trakt": 1675108, + "slug": "mike-rufino", + "imdb": "nm1196232", + "tmdb": 2506371, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2024-07-04T08:08:06.000Z" + } + }, + { + "job": "Stunts", + "jobs": [ + "Stunts" + ], + "person": { + "name": "Travis Fienhage", + "ids": { + "trakt": 1823374, + "slug": "travis-fienhage", + "imdb": "nm5361735", + "tmdb": 2636803, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-03T08:05:20.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Paul Crawford", + "ids": { + "trakt": 1940184, + "slug": "paul-crawford", + "imdb": null, + "tmdb": 2753653, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-11-11T08:08:10.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "J.C. Robaina", + "ids": { + "trakt": 1944253, + "slug": "j-c-robaina-f3e0d70d-5d8f-4aa3-b249-8e803641ead5", + "imdb": "nm0730154", + "tmdb": 2758531, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-02-16T08:07:00.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Danielle Wait", + "ids": { + "trakt": 1957716, + "slug": "danielle-wait-ee32e71c-7303-4f74-ad72-7046d2f35a14", + "imdb": null, + "tmdb": 2771944, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "female", + "updated_at": "2022-05-23T08:44:16.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Cain Smead", + "ids": { + "trakt": 1957717, + "slug": "cain-smead", + "imdb": null, + "tmdb": 2771948, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-08-05T08:08:30.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "John Pohmisano", + "ids": { + "trakt": 1957718, + "slug": "john-pohmisano", + "imdb": null, + "tmdb": 2771952, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-07-11T08:10:48.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Damien Moreno", + "ids": { + "trakt": 1957719, + "slug": "damien-moreno", + "imdb": "nm1862298", + "tmdb": 2771957, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2023-08-22T08:04:02.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Ross A. Jordan", + "ids": { + "trakt": 1957721, + "slug": "ross-a-jordan", + "imdb": null, + "tmdb": 2771965, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-01-20T08:06:47.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Brandon Johnson", + "ids": { + "trakt": 1957722, + "slug": "brandon-johnson-a8ca600d-31ef-48ce-a125-54a8e5d27a92", + "imdb": null, + "tmdb": 2771966, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2023-12-27T08:09:54.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Jorge Cisneros", + "ids": { + "trakt": 1957725, + "slug": "jorge-cisneros", + "imdb": null, + "tmdb": 2771989, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2023-12-09T08:07:47.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Mark Chapman", + "ids": { + "trakt": 1957726, + "slug": "mark-chapman-07be75b7-ffa5-4f85-b986-4480df155db4", + "imdb": null, + "tmdb": 2771991, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2024-11-11T08:07:16.000Z" + } + }, + { + "job": "Utility Stunts", + "jobs": [ + "Utility Stunts" + ], + "person": { + "name": "Hal Burton", + "ids": { + "trakt": 1957727, + "slug": "hal-burton-ee95ecf8-1a9e-44e7-b8c4-0d1913f81dd9", + "imdb": "nm0123592", + "tmdb": 2771993, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-08T08:07:52.000Z" + } + }, + { + "job": "Executive in Charge of Finance", + "jobs": [ + "Executive in Charge of Finance" + ], + "person": { + "name": "Matt Finick", + "ids": { + "trakt": 2588677, + "slug": "matt-finick", + "imdb": "nm2586682", + "tmdb": 3415861, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2022-05-23T08:44:16.000Z" + } + }, + { + "job": "Post Production Assistant", + "jobs": [ + "Post Production Assistant" + ], + "person": { + "name": "Patrick McMahon", + "ids": { + "trakt": 2588678, + "slug": "patrick-mcmahon-f2d5be62-36cb-4f1d-9d9c-c3c9759802a4", + "imdb": null, + "tmdb": 3415901, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2022-05-23T08:44:16.000Z" + } + }, + { + "job": "Translator", + "jobs": [ + "Translator" + ], + "person": { + "name": "Ilham Hosseini", + "ids": { + "trakt": 2588680, + "slug": "ilham-hosseini", + "imdb": null, + "tmdb": 3415904, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2022-05-23T08:44:16.000Z" + } + }, + { + "job": "Driver", + "jobs": [ + "Driver" + ], + "person": { + "name": "Steve Gentry", + "ids": { + "trakt": 3274072, + "slug": "steve-gentry-3c853b33-fcf2-4e1d-9f3d-1be79f282650", + "imdb": null, + "tmdb": 4090714, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2025-02-20T15:20:54.000Z" + } + } + ], + "editing": [ + { + "job": "Additional Editor", + "jobs": [ + "Additional Editor" + ], + "person": { + "name": "Michael Tronick", + "ids": { + "trakt": 1153, + "slug": "michael-tronick", + "imdb": "nm0873531", + "tmdb": 908, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Michael Tronick (born March 2, 1949) is an American film editor with more than 25 film credits. He has been nominated twice for American Cinema Editors \"Eddie\" Awards for Scent of a Woman (1992) and for Hairspray (2007).\n\nSince 2012, Tronick has served as a member of the board of governors of the Academy of Motion Picture Arts and Sciences Editors Branch.\n\nTronick is a member of the academy's Science and Technology Council and was previously selected for membership in the American Cinema Editors.\n\nDescription above from the Wikipedia article Michael Tronick, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1949-04-02", + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "editing", + "gender": "male", + "updated_at": "2024-09-20T08:01:49.000Z" + } + }, + { + "job": "Editor", + "jobs": [ + "Editor" + ], + "person": { + "name": "Dan Lebental", + "ids": { + "trakt": 9072, + "slug": "dan-lebental", + "imdb": "nm0495603", + "tmdb": 11455, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Dan Lebental is an American film editor who has edited many films for the Marvel Cinematic Universe.\n\nDescription above from the Wikipedia article Dan Lebental, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": null, + "death": null, + "birthplace": "USA", + "homepage": null, + "known_for_department": "editing", + "gender": "male", + "updated_at": "2024-12-07T08:02:06.000Z" + } + }, + { + "job": "Additional Editor", + "jobs": [ + "Additional Editor" + ], + "person": { + "name": "Derek Brechin", + "ids": { + "trakt": 18043, + "slug": "derek-brechin", + "imdb": "nm0106513", + "tmdb": 21351, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "editing", + "gender": "male", + "updated_at": "2024-09-23T08:01:48.000Z" + } + }, + { + "job": "Additional Editor", + "jobs": [ + "Additional Editor" + ], + "person": { + "name": "Greg Parsons", + "ids": { + "trakt": 42890, + "slug": "greg-parsons", + "imdb": "nm0663830", + "tmdb": 57344, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "editing", + "gender": "male", + "updated_at": "2024-06-09T08:08:40.000Z" + } + } + ], + "production": [ + { + "job": "Casting", + "jobs": [ + "Casting" + ], + "person": { + "name": "Sarah Halley Finn", + "ids": { + "trakt": 3223, + "slug": "sarah-halley-finn", + "imdb": "nm0278168", + "tmdb": 7232, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "sarahfinncasting", + "wikipedia": null + }, + "biography": "Sarah Halley Finn is an American casting director and producer. Finn has cast over 100 feature films and is best known for casting the Marvel Cinematic Universe films. She also cast and co-produced Oscar-winning Best Picture Everything Everywhere All at Once, the most awarded film in movie history. It won three out of four acting categories at the 95th Academy Awards: Best Actress, Best Supporting Actor, and Best Supporting Actress – a feat only achieved twice before, and not since 1976. Other works include Oscar-winning films Black Panther, Three Billboards Outside Ebbing, Missouri, and Crash, all of which earned Finn the Casting Society of America’s highest honour, the Artios Award for Outstanding Achievement in Casting. Those films also won the prestigious SAG Award for Outstanding Performance by a Cast in a Motion Picture. In 2023 Finn was nominated for the BAFTA Award for Best Casting and won the Artios Zeitgeist award for her work on Everything Everywhere All At Once. In 2022, she received two Primetime Emmy nominations for her work casting WandaVision and The Mandalorian.\n\nDescription above from the Wikipedia article Sarah Halley Finn, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1965-02-01", + "death": null, + "birthplace": "New York City, New York, USA", + "homepage": null, + "known_for_department": "production", + "gender": "female", + "updated_at": "2025-02-19T08:08:01.000Z" + } + }, + { + "job": "Executive Producer, Unit Production Manager", + "jobs": [ + "Executive Producer", + "Unit Production Manager" + ], + "person": { + "name": "Louis D'Esposito", + "ids": { + "trakt": 4197, + "slug": "louis-d-esposito", + "imdb": "nm0195669", + "tmdb": 57027, + "tvrage": null + }, + "social_ids": { + "twitter": "louisde2", + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Louis D'Esposito is an American film producer and director who is known for executive producing every Marvel Cinematic Universe film with the exception of The Incredible Hulk (2008). He also directed two Marvel One-Shots: Item 47 and Agent Carter, the latter of which inspired a spin-off TV show on ABC. D'Esposito currently serves as Marvel Studios' co-president alongside Kevin Feige.", + "birthday": null, + "death": null, + "birthplace": "The Bronx, New York City, New York, USA", + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2025-01-22T12:45:48.000Z" + } + }, + { + "job": "Producer", + "jobs": [ + "Producer" + ], + "person": { + "name": "Avi Arad", + "ids": { + "trakt": 5928, + "slug": "avi-arad", + "imdb": "nm0032696", + "tmdb": 7626, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Avi Arad (/ˈɑːvi ˈɑːrɑːd/; Hebrew: אבי ארד; born August 1, 1948) is an Israeli-American studio executive and producer of film, television, and animation. He became the CEO of Toy Biz in the 1990s, was the chief creative officer of Marvel Entertainment, and is the founder, former chairman, and former CEO of the latter's successor, Marvel Studios. Since then, he has produced and sometimes written a wide array of live-action, animated, and television comic book adaptations, including Spider-Man: Into the Spider-Verse.\n\nArad was born in 1948 in Ramat Gan, Israel, to a Jewish family. The son of Holocaust survivors from Poland, he grew up reading Superman and Spider-Man comics translated into Hebrew. In 1965, he was conscripted as a soldier into the Israel Defence Forces (IDF). He fought and was wounded in the 1967 Six-Day War and spent 15 days recuperating. Arad finished his military service in 1968.\n\nIn 1970, Arad moved to the United States and enrolled at Hofstra University to study industrial management. He worked as a truck driver and as a Hebrew teacher to put himself through college and graduated with a BBA in 1972.\n\nAlong with Israeli-American Toy Biz co-owner Isaac Perlmutter, Avi Arad came into conflict with Carl Icahn and Ron Perelman over control of Marvel Comics in the wake of its 1996 bankruptcy. In the end, Arad and Perlmutter came out on top, with Toy Biz taking over Marvel Comics in a complicated deal that included obtaining the rights to Spider-Man and other superheroes that Marvel had sold earlier. He was involved in Marvel's emergence from bankruptcy and the expansion of the company's profile through licensing and movies.\n\nOn May 31, 2006, Arad resigned from his various Marvel positions, including his leadership of Marvel Studios, to form his own production company, Arad Productions (also known as Arad Animation), a company that primarily produces Marvel-licensed films separate from the Marvel Cinematic Universe. His first non-Marvel film was 2007's Bratz. Further ventures include the manga adaptation Ghost in the Shell; an adaptation of Brandon Mull's teenage fantasy Fablehaven (which died in production); an adaptation of James Patterson's teenage novel Maximum Ride; and adaptations of video game properties Uncharted, Infamous, Metal Gear Solid, and The Legend of Zelda.\n\nIn August 2010, it was announced that Arad was given a chair with the American branch of animation studio Production I.G in Los Angeles, California.\n\nDescription above from the Wikipedia article Avi Arad, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1948-08-01", + "death": null, + "birthplace": "Givatayim, Israel", + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2025-02-20T08:07:22.000Z" + } + }, + { + "job": "Executive Producer", + "jobs": [ + "Executive Producer" + ], + "person": { + "name": "Ari Arad", + "ids": { + "trakt": 15632, + "slug": "ari-arad", + "imdb": "nm7191024", + "tmdb": 937174, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2024-03-19T08:08:26.000Z" + } + }, + { + "job": "Casting", + "jobs": [ + "Casting" + ], + "person": { + "name": "Randi Hiller", + "ids": { + "trakt": 15636, + "slug": "randi-hiller", + "imdb": "nm0384900", + "tmdb": 20540, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "female", + "updated_at": "2025-01-29T08:09:29.000Z" + } + }, + { + "job": "Co-Producer", + "jobs": [ + "Co-Producer" + ], + "person": { + "name": "Victoria Alonso", + "ids": { + "trakt": 15745, + "slug": "victoria-alonso", + "imdb": "nm0022285", + "tmdb": 113674, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Victoria Alonso (born 22 December 1965) is an Argentine film producer who formerly served as the president of physical and post-production, visual effects, and animation at Marvel Studios.\n\nVictoria Alonso was born on 22 December 1965 in La Plata, Buenos Aires Province, Argentina.\n\nAlonso moved to Seattle at the age of 19 to pursue an acting career. She relocated again to Los Angeles, where she began working in the visual effects industry, including at Digital Domain as a visual effects producer for four years, working on films such as Big Fish (2003), which was nominated for Best Special Visual Effects at the 57th British Academy Film Awards.\n\nAlonso joined Marvel Studios in 2005 as executive vice president of visual effects and post-production, working as a co-producer on Marvel Cinematic Universe films Iron Man (2008), Iron Man 2 (2010), Thor (2010), and Captain America: The First Avenger (2011), and serving as executive producer on every Marvel Studios production since The Avengers (2012), including television shows. She was promoted to executive vice president of production in 2015. In 2021, Alonso was promoted to president of physical and post-production, visual effects, and animation at Marvel Studios.\n\nIn 2016, Alonso became the first woman to win the Advanced Imaging Society's Harold Lloyd Award for her achievements in visual effects. In January 2020, she was awarded the Filmmaker Award by the Motion Picture Sound Editors at the 67th Golden Reel Awards. In October 2021, it was announced that Alonso would be the top honouree at Outfest's Visionary Award at the November ceremony at LA's Academy Museum of Motion Pictures.\n\nIn December 2022, she was named on The Hollywood Reporter's \"Women in Entertainment Power 100.\".\n\nIn 2023, Alonso was fired from her role at Marvel Studios for breach of contract after violating her noncompete clause by serving as a producer on the Amazon Studios film Argentina, 1985, despite having failed to seek permission to work on the film and continuing to promote it after being ordered by Disney to cease her involvement with the project.\n\nAt the time of her firing, criticism from VFX workers was noted, who had raised complaints of Marvel's \"demanding post-production schedules.\" Alonso was described by some as a \"kingmaker,\" with Chris Lee at Vulture reporting that Alonso was \"singularly responsible for Marvel's toxic work environment.\" However, Alonso was also described as the \"epitome of professional\" and supportive on set, with Joanna Robinson of The Ringer describing the reports as a \"gross mischaracterization\" and the opposite of Alonso's work. Alonso said that the real reason for her firing was her outspoken opposition to LGBTQ+ erasure at the company. Disney and Alonso reached a multimillion-dollar compensation settlement in April.\n\nAlonso is openly gay and is married to Australian actress Imelda Corcoran. The couple has one adopted daughter.\n\nDescription above from the Wikipedia article Victoria Alonso, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1965-12-22", + "death": null, + "birthplace": "La Plata, Argentina", + "homepage": null, + "known_for_department": "production", + "gender": "female", + "updated_at": "2024-12-13T13:12:23.000Z" + } + }, + { + "job": "Executive Producer", + "jobs": [ + "Executive Producer" + ], + "person": { + "name": "David Maisel", + "ids": { + "trakt": 15760, + "slug": "david-maisel", + "imdb": "nm2588950", + "tmdb": 113673, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2024-09-24T08:02:32.000Z" + } + }, + { + "job": "Executive Producer", + "jobs": [ + "Executive Producer" + ], + "person": { + "name": "Stan Lee", + "ids": { + "trakt": 16093, + "slug": "stan-lee", + "imdb": "nm0498278", + "tmdb": 7624, + "tvrage": null + }, + "social_ids": { + "twitter": "TheRealStanLee", + "facebook": "realstanlee", + "instagram": "therealstanlee", + "wikipedia": null + }, + "biography": "Stan Lee (born Stanley Martin Lieber /ˈliːbər/; December 28, 1922–November 12, 2018) was an American comic book writer, editor, publisher, and producer. He rose through the ranks of a family-run business called Timely Comics, which later became Marvel Comics. He was Marvel's primary creative leader for two decades, expanding it from a small publishing house division to a multimedia corporation that dominated the comics and film industries.\n\nIn collaboration with others at Marvel—particularly co-writers and artists Jack Kirby and Steve Ditko—he co-created iconic characters, including Spider-Man, the X-Men, Iron Man, Thor, the Hulk, Ant-Man, the Wasp, the Fantastic Four, Black Panther, Daredevil, Doctor Strange, the Scarlet Witch, and Black Widow. These and other characters' introductions in the 1960s pioneered a more naturalistic approach in superhero comics. In the 1970s, Lee challenged the restrictions of the Comics Code Authority, indirectly leading to changes in its policies. In the 1980s, he pursued the development of Marvel properties in other media, with mixed results.\n\nFollowing his retirement from Marvel in the 1990s, Lee remained a public figurehead for the company. He frequently made cameo appearances in films and television shows based on Marvel properties, on which he received an executive producer credit, which allowed him to become the person with the highest-grossing film total ever. He continued independent creative ventures until his death, aged 95, in 2018. Lee was inducted into the comic book industry's Will Eisner Award Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995. He received the NEA's National Medal of Arts in 2008.\n\nDescription above from the Wikipedia article Stan Lee, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1922-12-28", + "death": "2018-11-12", + "birthplace": "New York City, New York, USA", + "homepage": "https://therealstanlee.com/", + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-02-15T08:09:58.000Z" + } + }, + { + "job": "Executive Producer", + "jobs": [ + "Executive Producer" + ], + "person": { + "name": "Jon Favreau", + "ids": { + "trakt": 16183, + "slug": "jon-favreau", + "imdb": "nm0269463", + "tmdb": 15277, + "tvrage": null + }, + "social_ids": { + "twitter": "jon_favreau", + "facebook": "jonfavreau", + "instagram": "jonfavreau", + "wikipedia": null + }, + "biography": "Jonathan Kolia Favreau (/ˈfævroʊ/ FAV-roh; born October 19, 1966) is an American filmmaker and actor. As an actor, Favreau has appeared in films such as Rudy (1993), PCU (1994), Swingers (1996), Very Bad Things (1998), Deep Impact (1998), The Replacements (2000), Daredevil (2003), The Break-Up (2006), Four Christmases (2008), Couples Retreat (2009), I Love You, Man (2009), People Like Us (2012), The Wolf of Wall Street (2013), and Chef (2014).\n\nAs a filmmaker, Favreau has been significantly involved with the Marvel Cinematic Universe. He directed, produced, and appeared as Happy Hogan in the films Iron Man (2008) and Iron Man 2 (2010). He also served as an executive producer for and/or appeared as the character in the films The Avengers (2012), Iron Man 3 (2013), Avengers: Age of Ultron (2015), Spider-Man: Homecoming (2017), Avengers: Infinity War (2018), Avengers: Endgame (2019), Spider-Man: Far From Home (2019), Spider-Man: No Way Home (2021), and Deadpool \u0026 Wolverine (2024).\n\nHe has also directed the films Elf (2003), Zathura: A Space Adventure (2005), Cowboys \u0026 Aliens (2011), Chef (2014), The Jungle Book (2016), The Lion King (2019), and The Mandalorian \u0026 Grogu (2026). Recently, Favreau has been known for his work on the Star Wars franchise with Dave Filoni, creating the Disney+ original series The Mandalorian (2019–present), which Filoni helped develop, with both serving as executive producers. Alongside Filoni, he serves as an executive producer on all of the show's spin-off series, including The Book of Boba Fett, Ahsoka, and the upcoming Skeleton Crew. He produces films under his production company banner, Fairview Entertainment, and also presented the variety series Dinner for Five and the cooking series The Chef Show.\n\nDescription above from the Wikipedia article Jon Favreau, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1966-10-19", + "death": null, + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T20:04:33.000Z" + } + }, + { + "job": "Producer", + "jobs": [ + "Producer" + ], + "person": { + "name": "Kevin Feige", + "ids": { + "trakt": 16185, + "slug": "kevin-feige", + "imdb": "nm0270559", + "tmdb": 10850, + "tvrage": null + }, + "social_ids": { + "twitter": "Kevfeige", + "facebook": null, + "instagram": "kevfeige", + "wikipedia": null + }, + "biography": "Kevin Feige (/ˈfaɪɡi/ FY-ghee; born June 2, 1973) is an American film and television producer. He has been the president of Marvel Studios and the primary producer of the Marvel Cinematic Universe franchise since 2007. The films he has produced have a combined worldwide box office gross of over $30 billion, making him the highest grossing producer of all time, with Avengers: Endgame becoming the highest-grossing film at the time of its release.\n\nFeige is a member of the Producers Guild of America. In 2018, he was nominated for the Academy Award for Best Picture for producing Black Panther, the first superhero film to receive that honor and the first film in the Marvel Cinematic Universe to win an Academy Award. In October 2019, he became the chief creative officer of Marvel Entertainment.\n\nDescription above from the Wikipedia article Kevin Feige, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1973-06-02", + "death": null, + "birthplace": "Boston, Massachusetts, USA", + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2025-02-15T08:12:01.000Z" + } + }, + { + "job": "Executive Producer", + "jobs": [ + "Executive Producer" + ], + "person": { + "name": "Peter Billingsley", + "ids": { + "trakt": 16197, + "slug": "peter-billingsley", + "imdb": "nm0082526", + "tmdb": 12708, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "officialpeterb", + "wikipedia": null + }, + "biography": "Peter Billingsley (born April 16, 1971), also known as Peter Michaelsen and Peter Billingsley-Michaelsen, is an American actor, director, and producer. His acting roles include Ralphie Parker in the 1983 movie A Christmas Story, Jack Simmons in The Dirt Bike Kid, Billy in Death Valley, and as Messy Marvin in Hershey's chocolate syrup commercials during the 1980s. He began his career as an infant in television commercials.\n\nDescription above from the Wikipedia article Peter Billingsley, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1971-04-16", + "death": null, + "birthplace": "New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-12-21T08:10:50.000Z" + } + }, + { + "job": "Associate Producer", + "jobs": [ + "Associate Producer" + ], + "person": { + "name": "Jeremy Latcham", + "ids": { + "trakt": 48503, + "slug": "jeremy-latcham", + "imdb": "nm1436246", + "tmdb": 113675, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Jeremy Latcham is an American producer. Prior to launching Latcham Pictures, Latcham served as Senior Vice President of Production and Development at Marvel Studios, where he was an Executive Producer on Spider-Man: Homecoming, Avengers: Age of Ultron, Marvel's The Avengers, and Guardians of the Galaxy. Latcham was the associate producer on Iron Man and the co-producer on Iron Man 2. A graduate of Northwestern University, Latcham joined Marvel Studios in 2004 before leaving in 2017 to start Latcham Pictures.\n\nSince then, he has produced Dungeons \u0026 Dragons: Honor Among Thieves and Bad Times at the El Royale.\n\nIn 2011, Variety featured Latcham as one of Hollywood’s new leaders.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "https://www.latchampictures.com/", + "known_for_department": "production", + "gender": "male", + "updated_at": "2025-01-09T08:08:46.000Z" + } + }, + { + "job": "Executive In Charge Of Production", + "jobs": [ + "Executive In Charge Of Production" + ], + "person": { + "name": "Ross T. Fanger", + "ids": { + "trakt": 110867, + "slug": "ross-t-fanger", + "imdb": "nm0266771", + "tmdb": 11014, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Ross Fanger is a producer, known for Planet of the Apes (2001), Fantastic Four (2005) and X-Men: The Last Stand (2006).", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2024-12-03T08:01:41.000Z" + } + }, + { + "job": "Associate Producer", + "jobs": [ + "Associate Producer" + ], + "person": { + "name": "Eric Heffron", + "ids": { + "trakt": 146027, + "slug": "eric-heffron", + "imdb": "nm0373610", + "tmdb": 143894, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "directing", + "gender": "male", + "updated_at": "2024-10-21T17:06:45.000Z" + } + }, + { + "job": "Unit Production Manager", + "jobs": [ + "Unit Production Manager" + ], + "person": { + "name": "Sara E. White", + "ids": { + "trakt": 228014, + "slug": "sara-e-white", + "imdb": "nm0925436", + "tmdb": 1085294, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": null, + "updated_at": "2023-11-22T17:41:10.000Z" + } + }, + { + "job": "Production Supervisor", + "jobs": [ + "Production Supervisor" + ], + "person": { + "name": "Gary R. Wordham", + "ids": { + "trakt": 563745, + "slug": "gary-r-wordham", + "imdb": null, + "tmdb": 1552051, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": null, + "updated_at": "2022-10-30T08:04:18.000Z" + } + }, + { + "job": "Production Supervisor", + "jobs": [ + "Production Supervisor" + ], + "person": { + "name": "David J. Grant", + "ids": { + "trakt": 565409, + "slug": "david-j-grant", + "imdb": "nm0335343", + "tmdb": 1533708, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "David J. Grant is one of the Vice Presidents of Physical Production at Marvel Studios.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "production", + "gender": "male", + "updated_at": "2025-02-16T08:06:45.000Z" + } + }, + { + "job": "Executive In Charge Of Post Production", + "jobs": [ + "Executive In Charge Of Post Production" + ], + "person": { + "name": "Charlie Davis", + "ids": { + "trakt": 576959, + "slug": "charlie-davis", + "imdb": null, + "tmdb": 1562239, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "crew", + "gender": null, + "updated_at": "2022-06-05T08:06:45.000Z" + } + }, + { + "job": "Production Assistant", + "jobs": [ + "Production Assistant" + ], + "person": { + "name": "Trinh Tran", + "ids": { + "trakt": 1456711, + "slug": "trinh-tran", + "imdb": null, + "tmdb": 2275675, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Trinh T. Tran is an executive in production and development at Marvel Studios.\n\nShe was an associate producer on Captain America: Civil War and an executive producer on Avengers: Infinity War and Avengers: Endgame.\n\nShe was also an assistant to Matt Finick and Charlie Davis on Iron Man and The Incredible Hulk, an assistant to Louis D'Esposito on Iron Man 2, Thor, Captain America: The First Avenger and The Avengers and a creative executive on Captain America: The Winter Soldier and Captain America: Civil War.", + "birthday": null, + "death": null, + "birthplace": "Ho Chi Minh City, Vietnam", + "homepage": null, + "known_for_department": "production", + "gender": "female", + "updated_at": "2025-01-24T08:07:55.000Z" + } + } + ], + "camera": [ + { + "job": "Director of Photography", + "jobs": [ + "Director of Photography" + ], + "person": { + "name": "Matthew Libatique", + "ids": { + "trakt": 3739, + "slug": "matthew-libatique", + "imdb": "nm0508732", + "tmdb": 4867, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": "libatique", + "wikipedia": null + }, + "biography": "Matthew Libatique (born July 19, 1968) is an American cinematographer. He is best known for his collaborations with director Darren Aronofsky on the films Pi (1998), Requiem for a Dream (2000), The Fountain (2006), Black Swan (2010), Noah (2014), and Mother! (2017). Libatique has received three Academy Award for Best Cinematography nominations for his work on Black Swan (2010), A Star Is Born (2018), and Maestro (2023).\n\nMatthew Libatique was born in Elmhurst, Queens, New York City, to Filipino immigrant parents Georgina (née José) and Justiniáno Libatique. His father was from Dagupan, and his mother was from Lucena.\n\nLibatique studied sociology and communications at California State University, Fullerton, before earning an MFA in cinematography at AFI Conservatory.\n\nLibatique served as director of photography for music videos and teamed with fellow AFI alumnus Aronofsky for the short film Protozoa. The two collaborated on the first three of Aronofsky's feature films. Other frequent collaborators are Julie Dash (music videos including Tracy Chapman's \"Give Me One Reason\"), Spike Lee (She Hate Me, Inside Man, and Miracle at St. Anna), Joel Schumacher (Tigerland, Phone Booth, and The Number 23), and Jon Favreau (Iron Man, Iron Man 2, and Cowboys \u0026 Aliens).\n\nLibatique's notable films include blockbusters such as Iron Man and Iron Man 2. In 2010, he was nominated for an Academy Award for Best Cinematography for his work on Black Swan, for which he won his second Independent Spirit Award. He has also won best cinematography awards at the LA Film Critics Association, NY Film Critics Online, SF Film Critics, among many others.\n\nLibatique discussed the importance of working closely with a director on a Cinematographer Roundtable with The Hollywood Reporter, revealing: “The main thing is that you (both the cinematographer and director) have the same goal and are telling the same story. Going into preparation, you really need to be on the same page. Conflicts may arise when there’s a miscommunication about what’s important in a scene. So, it’s really important to listen...The director can (understandably) get pulled in a lot of different directions in prep. We, cinematographers, are sort of guarding the gate of filmmaking, amongst all the other things that are happening.”\n\nIn addition to guarding the filmmaking gate, he also says of his process, “I’d like to think each film is custom made. The director obviously dictates the approach that I have because everybody has a different working style. Some people want to talk intensely and visually about shots. Some don’t talk much at all. They concentrate more on the performances, and they give you a broad idea of what they want the film to look like. So my first approach is to evaluate them, which may start in the interview process. But you also learn in preparation, as much as you can, about the director. And that informs how I prepare in pre-production. If I’m lucky, I can shape a visual language off some kind of inspiration. But the director definitely dictates how I do it.”\n\nDescription above from the Wikipedia article Matthew Libatique, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1968-07-19", + "death": null, + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "camera", + "gender": "male", + "updated_at": "2025-01-12T08:10:56.000Z" + } + }, + { + "job": "Additional Photography", + "jobs": [ + "Additional Photography" + ], + "person": { + "name": "Gabriel Beristain", + "ids": { + "trakt": 13375, + "slug": "gabriel-beristain", + "imdb": "nm0075244", + "tmdb": 10832, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Luis Gabriel Beristáin, ASC, BSC, AMC, is a Mexican cinematographer, producer, and television director known for his work on numerous well-known films, including The Distinguished Gentleman, The Spanish Prisoner, Blade II, and Street Kings, and several entries in the Marvel Cinematic Universe, including the Agent Carter television series.\n\nHe has collaborated with filmmakers like Guillermo del Toro, Derek Jarman, David Mamet, and David Ayer. He is an active member of both the Academy of Motion Picture Arts and Sciences and the British Academy of Film and Television Arts.\n\nBeristain was born in Mexico City, the son of actor Luis Beristáin. His interest in filmmaking began with his involvement in Mexico's independent film scene in the seventies. He studied engineering at the Instituto Politécnico Nacional and later joined a new film studies program at the school while also producing audiovisual training materials for the health department.\n\nAfter filming a number of documentaries, he founded a small commercial production company before moving to Italy in 1977. At the recommendation of director Sergio Leone, he relocated to the United Kingdom, where he enrolled in the prestigious National Film and Television School, which accepted only 25 students a year. He was one of only five foreigners to be accepted into the school and studied cinematography under Oswald Morris and Billy Williams.\n\nHis first feature film as a cinematographer was the 1983 Colombian horror film Bloody Flesh (Spanish: Carne de tu carne, \"Flesh of Your Flesh\"), for which he won the Best Cinematography Award at the Bogotá Film Festival. His work on Derek Jarman's 1986 film Caravaggio earned him a Special Silver Bear Award at the Berlin International Film Festival. Beristain was one of several cinematographers on the 1987 anthology film Aria, which was nominated for a Palme d'Or at the Cannes Film Festival. Allen Daviau suggested he move to Hollywood, where he could apply his talents and unique insight into both Mexican and Anglo cultures. Beristain has been a member of the British Society of Cinematographers since 1990 and the American Society of Cinematographers since 2002.\n\nWhile working on 2003's S.W.A.T., Beristain became friends with executive producer Louis D'Esposito, who, after helping form Marvel Studios, invited Beristain to do additional photography for Iron Man. He wound up in the same function in six other Marvel Cinematic Universe films and also served as cinematographer for the D'Esposito-directed Marvel One-Shot short films Item 47 (2012) and Agent Carter (2013), as well as the television series Agent Carter. Beristein would eventually have his first feature for the studio as cinematographer in 2021's Black Widow.\n\nBeristain had a son born in 1980 who lived in Austria and died in 2000.\n\nBeristain's influences are Gregg Toland, Freddie Young, Emmanuel Lubezki, and Roger Deakins. Description above from the Wikipedia article Gabriel Beristain, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1955-05-09", + "death": null, + "birthplace": "Mexico City, Mexico", + "homepage": "https://www.gabrielberistain.com", + "known_for_department": "camera", + "gender": "male", + "updated_at": "2025-02-01T08:08:46.000Z" + } + }, + { + "job": "Second Unit Director of Photography", + "jobs": [ + "Second Unit Director of Photography" + ], + "person": { + "name": "Jonathan Taylor", + "ids": { + "trakt": 503452, + "slug": "jonathan-taylor-8d85ecf0-7012-4d1b-a711-05cdb53d2173", + "imdb": "nm0852658", + "tmdb": 1424180, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "camera", + "gender": "male", + "updated_at": "2024-06-14T08:08:00.000Z" + } + } + ], + "art": [ + { + "job": "Production Design", + "jobs": [ + "Production Design" + ], + "person": { + "name": "J. Michael Riva", + "ids": { + "trakt": 5968, + "slug": "j-michael-riva", + "imdb": "nm0728951", + "tmdb": 13304, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "John Michael Riva (June 28, 1948–June 7, 2012), better known as J. Michael Riva, was an American production designer.\n\nRiva was born in Manhattan to William Riva, a Broadway set designer, and Maria Elisabeth Sieber, a German-born actress and the daughter of Marlene Dietrich. Riva had three brothers (John Peter, John Paul, and John David). Riva attended the prep school Institute Le Rosey in Switzerland for six years before attending UCLA. Married to Wendy Mickell, he had four sons, Jean-Paul, Mikey, Daniel, and Adam.\n\nRiva had a long and prestigious career as an art director and production designer on numerous films, including the 1985 film The Colour Purple, for which he was nominated for the Academy Award for Best Art Direction. Other credits include The Goonies (1985), Lethal Weapon (1987), A Few Good Men (1992), Spider-Man 3 (2007), Iron Man (2008), and Iron Man 2 (2010).\n\nHis final films, The Amazing Spider-Man and Django Unchained, were released posthumously. He was the production designer for the opening ceremony of the 1996 Summer Olympics in Atlanta, as well as for the 74th and 79th Academy Awards in 2002 and 2007, respectively. He won a Primetime Emmy Award for his work on the latter.\n\nRiva suffered a stroke on June 1, 2012, in New Orleans, Louisiana, during production of Django Unchained. He died in a hospital there on June 7, 2012, at age 63. Django director Quentin Tarantino commented, \"Michael became a dear friend on this picture, as well as a magnificent, talented colleague.\"\n\nDescription above from the Wikipedia article J. Michael Riv, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1948-06-28", + "death": "2012-06-07", + "birthplace": "Manhattan, New York, USA", + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2024-12-06T08:01:30.000Z" + } + }, + { + "job": "Supervising Art Director", + "jobs": [ + "Supervising Art Director" + ], + "person": { + "name": "David F. Klassen", + "ids": { + "trakt": 8605, + "slug": "david-f-klassen", + "imdb": "nm0458333", + "tmdb": 14349, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2024-12-07T08:02:46.000Z" + } + }, { - "job": "Casting", + "job": "Art Direction", "jobs": [ - "Casting" + "Art Direction" ], "person": { - "name": "Sarah Finn", + "name": "Richard F. Mays", "ids": { - "trakt": 3223, - "slug": "sarah-finn", - "imdb": "nm0278168", - "tmdb": 7232, + "trakt": 12080, + "slug": "richard-f-mays", + "imdb": "nm0563081", + "tmdb": 14350, "tvrage": null }, - "biography": "Sarah Finn, sometimes credited Sarah Halley Finn is an American casting director. She has a casting director credit on every Marvel Studios film except The Incredible Hulk.", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2024-06-12T08:14:43.000Z" } }, { - "job": "Producer", + "job": "Property Master", "jobs": [ - "Producer" + "Property Master" ], "person": { - "name": "Avi Arad", + "name": "Russell Bobbitt", "ids": { - "trakt": 5928, - "slug": "avi-arad", - "imdb": "nm0032696", - "tmdb": 7626, + "trakt": 15582, + "slug": "russell-bobbitt", + "imdb": "nm0090309", + "tmdb": 1004624, "tvrage": null }, - "biography": "Avi Arad is an Israeli-American businessman. He became the CEO of the company Toy Biz in the 1990s, and soon afterward became the chief creative officer of Marvel Entertainment, a Marvel director, and the chairman, CEO, and founder of Marvel Studios.", - "birthday": "1948-08-01", + "social_ids": { + "twitter": "MarvelProps", + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Russell Bobbitt is a property master.\n\nBobbitt has worked on multiple Hollywood films. Since working as a property master on Marvel Studios' Iron Man (2008), he has worked on many of the studio's projects, for example, Captain America: Civil War (2016), Avengers: Endgame (2019), and some of their Disney+ TV shows as well: WandaVision (2021), Loki (2021), and Hawkeye (2021).\n\nBobbitt has won multiple of Hamilton's \"Behind the Camera Awards,\"  including in 2008 for his prop work on \"Iron Man\" (friend and actress Lucy Liu presented), in 2011 for his collective work as Property Master on \"Thor,\" \"Cowboys \u0026 Aliens,\" and \"Hangover 2\" (Bobbitt's \"Iron Man\" and \"Cowboys \u0026 Aliens\" director and friend Jon Favreau presented).\n\nHe is currently working on many of Marvel Studios' upcoming projects as property master.", + "birthday": null, "death": null, - "birthplace": "Givatayim, Israel", - "homepage": null + "birthplace": null, + "homepage": "https://russellbobbitt.com/", + "known_for_department": "art", + "gender": "male", + "updated_at": "2024-12-13T13:12:23.000Z" } }, { - "job": "Executive Producer", + "job": "Assistant Art Director", "jobs": [ - "Executive Producer" + "Assistant Art Director" ], "person": { - "name": "Ari Arad", + "name": "Michael E. Goldman", "ids": { - "trakt": 15632, - "slug": "ari-arad", - "imdb": "nm7191024", - "tmdb": 937174, + "trakt": 16847, + "slug": "michael-e-goldman", + "imdb": "nm0325864", + "tmdb": 60223, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "art", + "gender": null, + "updated_at": "2024-06-06T08:09:07.000Z" } }, { - "job": "Casting", + "job": "Art Direction", "jobs": [ - "Casting" + "Art Direction" ], "person": { - "name": "Randi Hiller", + "name": "Suzan Wexler", "ids": { - "trakt": 15636, - "slug": "randi-hiller", - "imdb": "nm0384900", - "tmdb": 20540, + "trakt": 16849, + "slug": "suzan-wexler", + "imdb": "nm0923328", + "tmdb": 963355, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "art", + "gender": "female", + "updated_at": "2024-12-11T08:10:11.000Z" } }, { - "job": "Co-Producer", + "job": "Graphic Designer", "jobs": [ - "Co-Producer" + "Graphic Designer" ], "person": { - "name": "Victoria Alonso", + "name": "Dianne Chadwick", "ids": { - "trakt": 15745, - "slug": "victoria-alonso", - "imdb": "nm0022285", - "tmdb": 113674, + "trakt": 179863, + "slug": "dianne-chadwick", + "imdb": "nm0149478", + "tmdb": 562696, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "art", + "gender": null, + "updated_at": "2024-11-05T08:09:41.000Z" } }, { - "job": "Executive Producer", + "job": "Storyboard Artist", "jobs": [ - "Executive Producer" + "Storyboard Artist" ], "person": { - "name": "Stan Lee", + "name": "Philip Keller", "ids": { - "trakt": 16093, - "slug": "stan-lee", - "imdb": "nm0498278", - "tmdb": 7624, - "tvrage": 37350 + "trakt": 544433, + "slug": "philip-keller", + "imdb": "nm0445745", + "tmdb": 1463323, + "tvrage": null }, - "biography": "Stan Lee (born Stanley Martin Lieber, December 28, 1922 – November 12, 2018)  was an American comic book writer, editor, actor, producer, publisher, television personality, and the former president and chairman of Marvel Comics.\n\nIn collaboration with several artists, most notably Jack Kirby and Steve Ditko, he co-created Spider-Man, the Fantastic Four, the X-Men, the Avengers, Iron Man, the Hulk, Thor, Daredevil, Doctor Strange, and many other fictional characters, introducing complex, naturalistic characters and a thoroughly shared universe into superhero comic books. In addition, he headed the first major successful challenge to the industry's censorship organization, the Comics Code Authority, and forced it to reform its policies. Lee subsequently led the expansion of Marvel Comics from a small division of a publishing house to a large multimedia corporation.\n\nHe was inducted into the comic book industry's Will Eisner Comic Book Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995.", - "birthday": "1922-12-28", - "death": "2018-11-12", - "birthplace": "New York City, New York, USA", - "homepage": "http://therealstanlee.com/" + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "art", + "gender": null, + "updated_at": "2024-02-14T03:46:38.000Z" } }, { - "job": "Producer", + "job": "Storyboard Artist", "jobs": [ - "Producer" + "Storyboard Artist" ], "person": { - "name": "Kevin Feige", + "name": "David Lowery", "ids": { - "trakt": 16185, - "slug": "kevin-feige", - "imdb": "nm0270559", - "tmdb": 10850, + "trakt": 544442, + "slug": "david-lowery-a6718b7b-fb3b-4097-99f5-271711999469", + "imdb": "nm0523204", + "tmdb": 1463332, "tvrage": null }, - "biography": "Kevin Feige was born on June 2, 1973 in Boston, Massachusetts, USA. He is a producer, known for The Avengers (2012), Guardians of the Galaxy (2014) and Iron Man (2008). He is married to Caitlin Feige.", - "birthday": "1973-06-02", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": "1959-04-10", "death": null, - "birthplace": "Boston, Massachusetts, USA", - "homepage": null + "birthplace": "Chicago, Illinois, USA", + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2025-02-03T08:10:22.000Z" } }, { - "job": "Production Assistant", + "job": "Storyboard Artist", "jobs": [ - "Production Assistant" + "Storyboard Artist" ], "person": { - "name": "Trinh Tran", + "name": "Eric Ramsey", "ids": { - "trakt": 1456711, - "slug": "trinh-tran", + "trakt": 1050223, + "slug": "eric-ramsey", "imdb": null, - "tmdb": 2275675, + "tmdb": 1957619, "tvrage": null }, - "biography": "Trinh T. Tran was an associate producer on Captain America: Civil War and an executive producer on Avengers: Infinity War and Avengers: Endgame.\n\nShe was also an assistant to Matt Finick and Charlie Davis on Iron Man and The Incredible Hulk, an assistant to Louis D'Esposito on Iron Man 2, Thor, Captain America: The First Avenger and The Avengers and a creative executive on Captain America: The Winter Soldier and Captain America: Civil War.", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", "birthday": null, "death": null, - "birthplace": "Ho Chi Minh City, Vietnam", - "homepage": null + "birthplace": null, + "homepage": null, + "known_for_department": "art", + "gender": "male", + "updated_at": "2023-12-19T08:18:51.000Z" } - } - ], - "camera": [ + }, { - "job": "Director of Photography", + "job": "Storyboard Artist", "jobs": [ - "Director of Photography" + "Storyboard Artist" ], "person": { - "name": "Matthew Libatique", + "name": "Stephen Platt", "ids": { - "trakt": 3739, - "slug": "matthew-libatique", - "imdb": "nm0508732", - "tmdb": 4867, + "trakt": 1427131, + "slug": "stephen-platt", + "imdb": null, + "tmdb": 2243003, "tvrage": null }, - "biography": "Matthew Libatique is an American cinematographer best known for his work with director Darren Aronofsky on such films as π, Requiem for a Dream, The Fountain, and Black Swan.", - "birthday": "1968-07-19", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", + "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "art", + "gender": null, + "updated_at": "2022-06-01T08:17:16.000Z" } } ], - "art": [ + "sound": [ { - "job": "Art Direction", + "job": "Sound Designer", "jobs": [ - "Art Direction" + "Sound Designer" ], "person": { - "name": "J. Michael Riva", + "name": "Shannon Mills", "ids": { - "trakt": 5968, - "slug": "j-michael-riva", - "imdb": "nm0728951", - "tmdb": 13304, + "trakt": 6848, + "slug": "shannon-mills", + "imdb": "nm0590185", + "tmdb": 8159, "tvrage": null }, - "biography": "", - "birthday": "1948-06-28", - "death": "2012-06-07", - "birthplace": "Manhattan, New York, USA", - "homepage": null + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Born and raised in Indiana, Shannon started his career in film sound, working with some of the most talented people in the business. Getting a job as the Sound Librarian at Skywalker Sound, he quickly began work for the likes of Christopher Boyes, Gary Rydstrom, Richard Hymns, and George Watters. After working as Gary Rydstrom’s right hand man for 8 years, he began branching out to do work on his own, still under the guidance of Chris Boyes and George Watters. He now remains a freelance supervising sound editor and sound designer.", + "birthday": null, + "death": null, + "birthplace": "Indiana, USA", + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2025-02-11T08:08:47.000Z" } }, { - "job": "Property Master", + "job": "Original Music Composer", "jobs": [ - "Property Master" + "Original Music Composer" ], "person": { - "name": "Russell Bobbitt", + "name": "Ramin Djawadi", "ids": { - "trakt": 15582, - "slug": "russell-bobbitt", - "imdb": "nm0090309", - "tmdb": 1004624, + "trakt": 15629, + "slug": "ramin-djawadi", + "imdb": "nm1014697", + "tmdb": 10851, + "tvrage": null + }, + "social_ids": { + "twitter": "Djawadi_Ramin", + "facebook": "RaminDjawadiOfficial", + "instagram": "ramindjawadi_official", + "wikipedia": null + }, + "biography": "Ramin Djawadi (born 19 July 1974) is an Iranian-German film score composer, conductor, and record producer. He is known for his scores for the HBO series Game of Thrones, for which he was nominated for Grammy Awards in 2018 and 2020. He is also the composer for the HBO Game of Thrones prequel series, House of the Dragon (2022–present). He has scored films such as Clash of the Titans, Pacific Rim, Warcraft, A Wrinkle in Time, Iron Man, and Eternals; television series including 3 Body Problem, Prison Break, Person of Interest, Jack Ryan, Westworld, and Fallout; and video games such as Medal of Honour, Gears of War 4, Gears 5, and System Shock 2. He won two consecutive Emmy Awards for Game of Thrones, in 2018 for the episode \"The Dragon and the Wolf\" and in 2019 for \"The Long Night.\".\n\nDescription above from the Wikipedia article Ramin Djawadi, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1974-07-19", + "death": null, + "birthplace": "Duisburg, Germany", + "homepage": "https://www.ramindjawadi.com/", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-10T08:01:18.000Z" + } + }, + { + "job": "Music Supervisor", + "jobs": [ + "Music Supervisor" + ], + "person": { + "name": "Dave Jordan", + "ids": { + "trakt": 20344, + "slug": "dave-jordan", + "imdb": "nm1044497", + "tmdb": 24192, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Dave Jordan started out in the music industry as a record label employee but quickly made the switch to become one of Hollywood's most in-demand theatrical music supervisors. Among the most popular movie series in the world under his guidance are Transformers, Iron Man, Guardians of the Galaxy, Avengers, and Black Panther. Due to his achievements, Dave is now the top-grossing music supervisor in movie history.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "https://www.formatent.com/dave-jordan", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-16T08:06:45.000Z" + } + }, + { + "job": "Music Editor", + "jobs": [ + "Music Editor" + ], + "person": { + "name": "David Klotz", + "ids": { + "trakt": 85635, + "slug": "david-klotz", + "imdb": "nm0460106", + "tmdb": 1070156, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "sound", + "gender": "male", + "updated_at": "2024-11-14T08:23:34.000Z" } - } - ], - "costume & make-up": [ + }, { - "job": "Costume Design", + "job": "Supervising Sound Editor", "jobs": [ - "Costume Design" + "Supervising Sound Editor" ], "person": { - "name": "Laura Jean Shannon", + "name": "Frank E. Eulner", "ids": { - "trakt": 7542, - "slug": "laura-jean-shannon", - "imdb": "nm0788320", - "tmdb": 9551, + "trakt": 143888, + "slug": "frank-e-eulner", + "imdb": "nm0262361", + "tmdb": 1339446, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Frank E. Eulner (born 1963) is a sound editor who is also a member of Skywalker Sound. He was nominated for Best Sound Editing for the film Iron Man at the 81st Academy Awards. His nomination was shared with Christopher Boyes.\n\nHe has over 70 sound editing credits since 1986.\n\nDescription above from the Wikipedia article Frank E. Eulner, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1963-01-01", + "death": null, + "birthplace": "Little Silver, New Jersey, USA", + "homepage": null, + "known_for_department": "sound", + "gender": "male", + "updated_at": "2024-12-08T08:02:34.000Z" + } + }, + { + "job": "Sound Re-Recording Mixer", + "jobs": [ + "Sound Re-Recording Mixer" + ], + "person": { + "name": "Lora Hirschberg", + "ids": { + "trakt": 240616, + "slug": "lora-hirschberg", + "imdb": "nm0386567", + "tmdb": 1327030, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Lora Hirschberg (born 1963) is an American sound engineer. She won the Academy Award for Best Sound Mixing for the film Inception and was nominated for the same award for the film The Dark Knight. She has worked on more than 110 films since 1990.\n\nHirschberg was born near Cleveland, Ohio, in the city of Olmsted Falls, and she attended New York University's film school. She worked in film sound in New York after graduation, having been hired by the film production company American Zoetrope in 1989. She started her career in the company's central machine room and later relocated to San Francisco, California. She is lesbian.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": "https://www.skysound.com/people/lora-hirschberg/", + "known_for_department": "sound", + "gender": "female", + "updated_at": "2025-02-17T08:06:12.000Z" + } + }, + { + "job": "Music Supervisor", + "jobs": [ + "Music Supervisor" + ], + "person": { + "name": "Richard Bernard", + "ids": { + "trakt": 297225, + "slug": "richard-bernard", + "imdb": "nm0076372", + "tmdb": 1169818, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Actor and multi-instrumentalist Richard Bernard is seen frequently in whimsical commercials playing Contra Bass Balalaika, Bouzouki, Sitar, Concertina, Didgeridoo or other exotic instruments. He is known for his work on films Indiana Jones and the Kingdom of the Crystal Skull, Iron Man, Oscar-winner The Artist, Love Affair, TV shows Gilmore Girls, Monk and many more. When not leading his Klezmer-fusion band The OY!Stars, he has performed with the Los Angeles Philharmonic and Hollywood Bowl Orchestra and been seen by millions as the Orchestra Conductor in United Airlines commercials and other TV shows and films. Born in Brooklyn, NY, Richard began playing music professionally in rural Georgia by age 13 including backing up Jerry Lee Lewis and other Sun Records Rockabilly pioneers. At age 20 after earning a BFA in Theatre from University of Georgia, he moved to Atlanta where he wrote and performed songs for theatre and bands, including a \"black consciousness raising theater\" of which he was a founding member along with Samuel L. Jackson (Richard generally acting the villainous white roles). Performing in shows by Sid \u0026 Marty Krofft led to relocating to Los Angeles where he was a regular on NBC's Barbara Mandrell and the Mandrell Sisters for two seasons while simultaneously leading his trio in downtown L.A.'s Bonaventure Hotel. Session work for films and major artists followed, as did performing in concert with Blue Man Group, Marvin Hamlisch, Theodore Bikel and many others. Along the way, Richard has written hundreds of songs, authored TV animation scripts for Hanna Barbara and Filmation, brought the creations of Jim Henson to life in major films, and played Mandolin, Tenor Banjo and Classical Guitar for Geffen Playhouse's production of Schlemiel the First. He acted the role of mysterious G-Man in Half-Life: Raise the Bar and many Mafia characters. His several international, Rock, and Folk music bands are regularly seen performing around Los Angeles and Las Vegas. Richard gives seminars in Ethnomusicology for students at California State University and University of Southern California, and is active in performing for Holocaust survivor groups.", + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2024-03-16T08:01:23.000Z" + } + }, + { + "job": "Sound Re-Recording Mixer, Sound Designer", + "jobs": [ + "Sound Re-Recording Mixer", + "Sound Designer" + ], + "person": { + "name": "Christopher Boyes", + "ids": { + "trakt": 411301, + "slug": "christopher-boyes", + "imdb": "nm0102110", + "tmdb": 900, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Christopher Boyes is an American sound engineer. He has won four Academy Awards and has been nominated for another eleven. He has worked on more than 100 films since 1991.\n\nDescription above from the Wikipedia article Christopher Boyes, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": null, + "death": null, + "birthplace": "Des Moines, Iowa, USA", + "homepage": "https://www.skysound.com/people/chris-boyes/", + "known_for_department": "sound", + "gender": "male", + "updated_at": "2025-02-07T08:10:27.000Z" + } + }, + { + "job": "Music Editor", + "jobs": [ + "Music Editor" + ], + "person": { + "name": "Shannon Erbe", + "ids": { + "trakt": 458817, + "slug": "shannon-erbe", + "imdb": "nm0258647", + "tmdb": 1396827, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2024-12-20T08:13:57.000Z" } }, { - "job": "Costume Design", + "job": "Music Coordinator", "jobs": [ - "Costume Design" + "Music Coordinator" ], "person": { - "name": "Rebecca Bentjen", + "name": "Jojo Villanueva", "ids": { - "trakt": 15637, - "slug": "rebecca-bentjen", - "imdb": null, - "tmdb": 962467, + "trakt": 564408, + "slug": "jojo-villanueva", + "imdb": "nm1173467", + "tmdb": 1527657, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2024-11-16T08:09:11.000Z" } - } - ], - "editing": [ + }, { - "job": "Editor", + "job": "Scoring Mixer", "jobs": [ - "Editor" + "Scoring Mixer" ], "person": { - "name": "Dan Lebental", + "name": "Alan Meyerson", "ids": { - "trakt": 9072, - "slug": "dan-lebental", - "imdb": "nm0495603", - "tmdb": 11455, + "trakt": 625350, + "slug": "alan-meyerson", + "imdb": null, + "tmdb": 1600114, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "sound", + "gender": null, + "updated_at": "2025-01-18T08:08:48.000Z" } - } - ], - "crew": [ + }, { - "job": "Special Effects Coordinator", + "job": "Music Coordinator", "jobs": [ - "Special Effects Coordinator" + "Music Coordinator" ], "person": { - "name": "Daniel Sudick", + "name": "Rebekah Johnson", "ids": { - "trakt": 12238, - "slug": "daniel-sudick", - "imdb": "nm0837203", - "tmdb": 15356, + "trakt": 2127642, + "slug": "rebekah-johnson-2127642", + "imdb": "nm2359435", + "tmdb": 2944425, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "sound", + "gender": "female", + "updated_at": "2024-10-04T08:09:23.000Z" } - }, + } + ], + "costume \u0026 make-up": [ { - "job": "Utility Stunts", + "job": "Costume Design", "jobs": [ - "Utility Stunts" + "Costume Design" ], "person": { - "name": "Tad Griffith", + "name": "Laura Jean Shannon", "ids": { - "trakt": 952530, - "slug": "tad-griffith", - "imdb": "nm0341498", - "tmdb": 1866579, + "trakt": 7542, + "slug": "laura-jean-shannon", + "imdb": "nm0788320", + "tmdb": 9551, "tvrage": null }, - "biography": "Screen Actors Guild Awards Nominee:\n\n2008 Outstanding Performance By A Stunt Ensemble In A Motion Picture: The Kingdom\n\n2008 Outstanding Performance By A Stunt Ensemble In A Motion Picture: 300\n\n2010 World Stunt Awards Winner — Best Work With A Vehicle: Fast And Furious 4\n\nWorld Stunt Awards Nominee:\n\n2005 Best Work With A Vehicle: Spiderman II\n\n2004 Best Stunt By A Stuntman: Seabiscuit\n\n2004 Best Specialty Stunt: Seabiscuit\n\n2004 Best Stunt By A Stuntman: Cradle 2 The Grave\n\n2004 Best Work With A Vehicle: Cradle 2 The Grave\n\n2002 Best Fire Stunt: Last Castle\n\n2002 Best Work With An Animal: American Outlaws\n\nOther Awards & Achievements:\n\n— World Champion Trick Rider\n\n— Third Generation Professional Rodeo Cowboy Association Contestant & Performer", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": "http://www.atadwest.com/" + "homepage": null, + "known_for_department": "costume \u0026 make-up", + "gender": "female", + "updated_at": "2024-09-20T08:01:51.000Z" } }, { - "job": "Utility Stunts", + "job": "Costume Design", "jobs": [ - "Utility Stunts" + "Costume Design" ], "person": { - "name": "Riley Harper", + "name": "Rebecca Gregg", "ids": { - "trakt": 1118940, - "slug": "riley-harper", - "imdb": "nm0363947", - "tmdb": 2020105, + "trakt": 503191, + "slug": "rebecca-gregg", + "imdb": "nm0191945", + "tmdb": 1423983, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, - "birthplace": "Los Angeles, California, USA", - "homepage": "http://www.lifeof-riley.com/" + "birthplace": null, + "homepage": null, + "known_for_department": "costume \u0026 make-up", + "gender": "female", + "updated_at": "2024-04-14T08:09:03.000Z" } } ], @@ -2257,11 +7053,20 @@ "tmdb": 18873, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Art Marcum is an American screenwriter, working with his screenwriting partner Matt Holloway, best known for writing the scripts of movies like Iron Man and Punisher: War Zone.In 2008, Marcum and Holloway wrote the script of Marvel Studios' superhero film Iron Man, which was directed by Jon Favreau and released on May 2, 2008, by Paramount Pictures. The duo also wrote the script for the action film Punisher: War Zone, directed by Lexi Alexander and released on December 5, 2008, by Lionsgate. They were hired by Paramount to co-write a script with John Fusco for the 2014 Teenage Mutant Ninja Turtles film, but their script was ultimately never used. In 2019, they were hired by Sony to write the script for the adventure film Uncharted, directed by Ruben Fleischer. In 2020, Deadline revealed they were working on a script for the Kraven the Hunter movie.Description above from the Wikipedia article Art Marcum and Matt Holloway, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2024-12-10T08:01:23.000Z" } }, { @@ -2278,17 +7083,26 @@ "tmdb": 18875, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Matt Holloway is an American screenwriter, working with his screenwriting partner Art Marcum, best known for writing the scripts of movies like Iron Man and Punisher: War Zone.In 2008, Marcum and Holloway wrote the script of Marvel Studios' superhero film Iron Man, which was directed by Jon Favreau and released on May 2, 2008, by Paramount Pictures. The duo also wrote the script for the action film Punisher: War Zone, directed by Lexi Alexander and released on December 5, 2008, by Lionsgate. They were hired by Paramount to co-write a script with John Fusco for the 2014 Teenage Mutant Ninja Turtles film, but their script was ultimately never used. In 2019, they were hired by Sony to write the script for the adventure film Uncharted, directed by Ruben Fleischer. In 2020, Deadline revealed they were working on a script for the Kraven the Hunter movie.Description above from the Wikipedia article Art Marcum and Matt Holloway, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2024-12-10T08:01:23.000Z" } }, { - "job": "Characters", + "job": "Comic Book", "jobs": [ - "Characters" + "Comic Book" ], "person": { "name": "Larry Lieber", @@ -2299,17 +7113,26 @@ "tmdb": 18876, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Lawrence D. Lieber (/ˈliːbər/; born October 26, 1931) is an American comic book writer and artist best known as the co-creator of the Marvel Comics superheroes Iron Man, Thor, and Ant-Man. He is also known for his long stint both writing and drawing the Marvel Western Rawhide Kid and for illustrating the newspaper comic strip The Amazing Spider-Man from 1986 to 2018. From 1974 to 1975, he was editor of Atlas/Seaboard Comics. Lieber is the younger brother of Stan Lee, who had been a writer, editor, and publisher of Marvel Comics.\n\nDescription above from the Wikipedia article Larry Lieber, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1931-10-26", "death": null, - "birthplace": "New York City, New York", - "homepage": null + "birthplace": "New York City, New York, USA", + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-02-04T15:20:27.000Z" } }, { - "job": "Characters", + "job": "Comic Book", "jobs": [ - "Characters" + "Comic Book" ], "person": { "name": "Don Heck", @@ -2320,17 +7143,26 @@ "tmdb": 18877, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Donald L. Heck (January 2, 1929 – February 23, 1995) was an American comics artist best known for co-creating the Marvel Comics characters Iron Man, the Wasp, Black Widow, Hawkeye, and Wonder Man and for his long run pencilling the Marvel superhero-team series The Avengers during the 1960s Silver Age of comic books.\n\nDescription above from the Wikipedia article Don Heck, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1929-01-02", "death": "1995-02-23", - "birthplace": null, - "homepage": null + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-01-30T08:01:24.000Z" } }, { - "job": "Characters", + "job": "Comic Book", "jobs": [ - "Characters" + "Comic Book" ], "person": { "name": "Stan Lee", @@ -2339,19 +7171,28 @@ "slug": "stan-lee", "imdb": "nm0498278", "tmdb": 7624, - "tvrage": 37350 + "tvrage": null + }, + "social_ids": { + "twitter": "TheRealStanLee", + "facebook": "realstanlee", + "instagram": "therealstanlee", + "wikipedia": null }, - "biography": "Stan Lee (born Stanley Martin Lieber, December 28, 1922 – November 12, 2018)  was an American comic book writer, editor, actor, producer, publisher, television personality, and the former president and chairman of Marvel Comics.\n\nIn collaboration with several artists, most notably Jack Kirby and Steve Ditko, he co-created Spider-Man, the Fantastic Four, the X-Men, the Avengers, Iron Man, the Hulk, Thor, Daredevil, Doctor Strange, and many other fictional characters, introducing complex, naturalistic characters and a thoroughly shared universe into superhero comic books. In addition, he headed the first major successful challenge to the industry's censorship organization, the Comics Code Authority, and forced it to reform its policies. Lee subsequently led the expansion of Marvel Comics from a small division of a publishing house to a large multimedia corporation.\n\nHe was inducted into the comic book industry's Will Eisner Comic Book Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995.", + "biography": "Stan Lee (born Stanley Martin Lieber /ˈliːbər/; December 28, 1922–November 12, 2018) was an American comic book writer, editor, publisher, and producer. He rose through the ranks of a family-run business called Timely Comics, which later became Marvel Comics. He was Marvel's primary creative leader for two decades, expanding it from a small publishing house division to a multimedia corporation that dominated the comics and film industries.\n\nIn collaboration with others at Marvel—particularly co-writers and artists Jack Kirby and Steve Ditko—he co-created iconic characters, including Spider-Man, the X-Men, Iron Man, Thor, the Hulk, Ant-Man, the Wasp, the Fantastic Four, Black Panther, Daredevil, Doctor Strange, the Scarlet Witch, and Black Widow. These and other characters' introductions in the 1960s pioneered a more naturalistic approach in superhero comics. In the 1970s, Lee challenged the restrictions of the Comics Code Authority, indirectly leading to changes in its policies. In the 1980s, he pursued the development of Marvel properties in other media, with mixed results.\n\nFollowing his retirement from Marvel in the 1990s, Lee remained a public figurehead for the company. He frequently made cameo appearances in films and television shows based on Marvel properties, on which he received an executive producer credit, which allowed him to become the person with the highest-grossing film total ever. He continued independent creative ventures until his death, aged 95, in 2018. Lee was inducted into the comic book industry's Will Eisner Award Hall of Fame in 1994 and the Jack Kirby Hall of Fame in 1995. He received the NEA's National Medal of Arts in 2008.\n\nDescription above from the Wikipedia article Stan Lee, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1922-12-28", "death": "2018-11-12", "birthplace": "New York City, New York, USA", - "homepage": "http://therealstanlee.com/" + "homepage": "https://therealstanlee.com/", + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-02-15T08:09:58.000Z" } }, { - "job": "Characters", + "job": "Comic Book", "jobs": [ - "Characters" + "Comic Book" ], "person": { "name": "Jack Kirby", @@ -2360,13 +7201,22 @@ "slug": "jack-kirby", "imdb": "nm0456158", "tmdb": 18866, - "tvrage": 73874 + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null }, - "biography": "From Wikipedia, the free encyclopedia. Jack Kirby (August 28, 1917 – February 6, 1994),  born Jacob Kurtzberg, was an American comic book artist, writer and editor. Growing up poor in New York City, Kurtzberg entered the nascent comics industry in the 1930s. He drew various comic strips under different pen names, ultimately settling on Jack Kirby. In 1941, Kirby and writer Joe Simon created the highly successful superhero character Captain America for Timely Comics. During the 1940s, Kirby would create a number of comics for various publishers, often teaming with Simon.\n\nAfter serving in World War II, Kirby returned to comics and worked in a variety of genres. He contributed to a number of publishers, including Archie Comics and DC Comics, but ultimately found himself at Timely's 1950s iteration, Atlas Comics, later to be known as Marvel Comics. In the 1960s, Kirby co-created many of Marvel Comics' major characters, including the Fantastic Four, the X-Men, and the Hulk, along with writer-editor Stan Lee. Despite the high sales and critical acclaim of the Lee-Kirby titles, Kirby felt treated unfairly, and left the company in 1970 for rival DC Comics.\n\nWhile working for DC, Kirby created his Fourth World saga, which spanned several comics titles. While these and other titles proved commercially unsuccessful and were canceled, several of their characters and the Fourth World mythos have continued as a significant part of the DC Comics universe. Kirby returned to Marvel briefly in the mid-to-late 1970s, then ventured into television animation and independent comics. In his later years, Kirby received great recognition for his career accomplishments, and is regarded by historians and fans as one of the major innovators and most influential creators in the comic book medium.\n\nIn 1987, Kirby, along with Carl Barks and Will Eisner, was one of the three inaugural inductees of the Will Eisner Comic Book Hall of Fame.\n\nDescription above from the Wikipedia article Jack Kirby, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "biography": "Jack Kirby (born Jacob Kurtzberg; August 28, 1917–February 6, 1994) was an American comic book artist, widely regarded as one of the medium's major innovators and one of its most prolific and influential creators. He grew up in New York City and learnt to draw cartoon figures by tracing characters from comic strips and editorial cartoons. He entered the nascent comics industry in the 1930s, drawing various comics features under different pen names, including Jack Curtiss, before settling on Jack Kirby. In 1940, he and writer-editor Joe Simon created the highly successful superhero character Captain America for Timely Comics, predecessor of Marvel Comics. During the 1940s, Kirby regularly teamed with Simon, creating numerous characters for that company and for National Comics Publications, later to become DC Comics.\n\nAfter serving in the European Theatre in World War II, Kirby produced work for DC Comics, Harvey Comics, Hillman Periodicals, and other publishers. At Crestwood Publications, he and Simon created the genre of romance comics and later founded their own short-lived comic company, Mainline Publications. Kirby was involved in Timely's 1950s iteration, Atlas Comics, which in the next decade became Marvel. There, in the 1960s, Kirby cocreated many of the company's major characters, including Ant-Man, the Avengers, the Black Panther, the Fantastic Four, the Hulk, Iron Man, the Silver Surfer, Thor, and the X-Men, among many others. Kirby's titles garnered high sales and critical acclaim, but in 1970, feeling he had been treated unfairly, largely in the realm of authorship credit and creators' rights, Kirby left the company for rival DC.\n\nAt DC, Kirby created his Fourth World saga, which spanned several comic titles. While these series proved commercially unsuccessful and were cancelled, the Fourth World's New Gods have continued as a significant part of the DC Universe. Kirby returned to Marvel briefly in the mid-to-late 1970s and then ventured into television animation and independent comics. In his later years, Kirby, who has been called \"the William Blake of comics,\" began receiving great recognition in the mainstream press for his career accomplishments, and in 1987 he was one of the three inaugural inductees of the Will Eisner Comic Book Hall of Fame. In 2017, Kirby was posthumously named a Disney Legend for his creations not only in the field of publishing but also because those creations formed the basis for The Walt Disney Company's financially and critically successful media franchise, the Marvel Cinematic Universe.\n\nKirby was married to Rosalind Goldstein in 1942. They had four children and remained married until his death from heart failure in 1994, at the age of 76. The Jack Kirby Awards and Jack Kirby Hall of Fame were named in his honour, and he is known as \"The King\" among comics fans for his many influential contributions to the medium.\n\nDescription above from the Wikipedia article Jack Kirby, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1917-08-28", "death": "1994-02-06", "birthplace": "New York City, New York, USA", - "homepage": null + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2025-02-12T19:42:42.000Z" } }, { @@ -2383,11 +7233,20 @@ "tmdb": 79209, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Hawk Ostby is a screenwriter, working alongside his writing partner, Mark Fergus. They are best known for their work on Children of Men (for which they were nominated for the Academy Award for Best Adapted Screenplay) and Iron Man. Their other work includes First Snow, which was also directed by Fergus, and Cowboys \u0026 Aliens.\n\nThey are the creators and executive producers of the TV series The Expanse, which debuted on Syfy in December 2015 and ended its six-season run on Amazon Prime Video on January 14, 2022.\n\nDescription above from the Wikipedia article Mark Fergus and Hawk Ostby, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": null, "death": null, - "birthplace": null, - "homepage": null + "birthplace": "India", + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2024-12-10T08:01:23.000Z" } }, { @@ -2404,76 +7263,232 @@ "tmdb": 79207, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Mark Fergus is a screenwriter, working alongside his writing partner, Hawk Ostby. They are best known for their work on Children of Men (for which they were nominated for the Academy Award for Best Adapted Screenplay) and Iron Man. Their other work includes First Snow, which was also directed by Fergus, and Cowboys \u0026 Aliens.\n\nThey are the creators and executive producers of the TV series The Expanse, which debuted on Syfy in December 2015 and ended its six-season run on Amazon Prime Video on January 14, 2022.\n\nDescription above from the Wikipedia article Mark Fergus and Hawk Ostby, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": null, "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "writing", + "gender": "male", + "updated_at": "2024-12-10T08:01:23.000Z" } } ], - "sound": [ + "visual effects": [ { - "job": "Original Music Composer", + "job": "Visual Effects Producer", "jobs": [ - "Original Music Composer" + "Visual Effects Producer" ], "person": { - "name": "Ramin Djawadi", + "name": "Victoria Alonso", "ids": { - "trakt": 15629, - "slug": "ramin-djawadi", - "imdb": "nm1014697", - "tmdb": 10851, + "trakt": 15745, + "slug": "victoria-alonso", + "imdb": "nm0022285", + "tmdb": 113674, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Victoria Alonso (born 22 December 1965) is an Argentine film producer who formerly served as the president of physical and post-production, visual effects, and animation at Marvel Studios.\n\nVictoria Alonso was born on 22 December 1965 in La Plata, Buenos Aires Province, Argentina.\n\nAlonso moved to Seattle at the age of 19 to pursue an acting career. She relocated again to Los Angeles, where she began working in the visual effects industry, including at Digital Domain as a visual effects producer for four years, working on films such as Big Fish (2003), which was nominated for Best Special Visual Effects at the 57th British Academy Film Awards.\n\nAlonso joined Marvel Studios in 2005 as executive vice president of visual effects and post-production, working as a co-producer on Marvel Cinematic Universe films Iron Man (2008), Iron Man 2 (2010), Thor (2010), and Captain America: The First Avenger (2011), and serving as executive producer on every Marvel Studios production since The Avengers (2012), including television shows. She was promoted to executive vice president of production in 2015. In 2021, Alonso was promoted to president of physical and post-production, visual effects, and animation at Marvel Studios.\n\nIn 2016, Alonso became the first woman to win the Advanced Imaging Society's Harold Lloyd Award for her achievements in visual effects. In January 2020, she was awarded the Filmmaker Award by the Motion Picture Sound Editors at the 67th Golden Reel Awards. In October 2021, it was announced that Alonso would be the top honouree at Outfest's Visionary Award at the November ceremony at LA's Academy Museum of Motion Pictures.\n\nIn December 2022, she was named on The Hollywood Reporter's \"Women in Entertainment Power 100.\".\n\nIn 2023, Alonso was fired from her role at Marvel Studios for breach of contract after violating her noncompete clause by serving as a producer on the Amazon Studios film Argentina, 1985, despite having failed to seek permission to work on the film and continuing to promote it after being ordered by Disney to cease her involvement with the project.\n\nAt the time of her firing, criticism from VFX workers was noted, who had raised complaints of Marvel's \"demanding post-production schedules.\" Alonso was described by some as a \"kingmaker,\" with Chris Lee at Vulture reporting that Alonso was \"singularly responsible for Marvel's toxic work environment.\" However, Alonso was also described as the \"epitome of professional\" and supportive on set, with Joanna Robinson of The Ringer describing the reports as a \"gross mischaracterization\" and the opposite of Alonso's work. Alonso said that the real reason for her firing was her outspoken opposition to LGBTQ+ erasure at the company. Disney and Alonso reached a multimillion-dollar compensation settlement in April.\n\nAlonso is openly gay and is married to Australian actress Imelda Corcoran. The couple has one adopted daughter.\n\nDescription above from the Wikipedia article Victoria Alonso, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1965-12-22", + "death": null, + "birthplace": "La Plata, Argentina", + "homepage": null, + "known_for_department": "production", + "gender": "female", + "updated_at": "2024-12-13T13:12:23.000Z" + } + }, + { + "job": "Visual Effects Production Manager", + "jobs": [ + "Visual Effects Production Manager" + ], + "person": { + "name": "Susan Pickett", + "ids": { + "trakt": 456991, + "slug": "susan-pickett", + "imdb": "nm0681916", + "tmdb": 1388871, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Susan Pickett is a visual effects producer.", "birthday": null, "death": null, - "birthplace": null, - "homepage": null + "birthplace": "Bronxville, New York, USA", + "homepage": null, + "known_for_department": "visual effects", + "gender": "female", + "updated_at": "2024-12-07T08:02:19.000Z" } }, { - "job": "Supervising Sound Editor", + "job": "Animation Supervisor", "jobs": [ - "Supervising Sound Editor" + "Animation Supervisor" ], "person": { - "name": "Frank E. Eulner", + "name": "Hal Hickel", "ids": { - "trakt": 143888, - "slug": "frank-e-eulner", - "imdb": "nm0262361", - "tmdb": 1339446, + "trakt": 506279, + "slug": "hal-hickel", + "imdb": "nm0382579", + "tmdb": 1426773, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Hal T. Hickel is a visual effects animator for Industrial Light \u0026 Magic.\n\nAt the age of 12, Hickel wrote a letter to Lucasfilm, outlining his ideas for a sequel to the original Star Wars movie (now known as Star Wars Episode IV: A New Hope), and received a polite rejection letter from producer Gary Kurtz. The letter now hangs on the wall of Hickel's office at ILM. Twenty years later, Hickel found himself working on Star Wars after all, as a lead animator on Star Wars: Episode I – The Phantom Menace.\n\nA native of Bailey, Colorado, Hickel joined the Film Graphics Program at CalArts in 1982. He worked at An-FX from 1982 until 1988, and then joined Will Vinton Studios, working in stop-motion and motion control.\n\nHickel began his animation career at Pixar in 1994, where he worked on Toy Story and the THXpromos, as well as some of Pixar's short films. Hearing that a new Star Wars trilogy was in pre-production, Hickel applied for a transfer to ILM on the chance that he might get to work on the prequels. He was first assigned as an animator on The Lost World: Jurassic Park, but was eventually assigned to work on The Phantom Menace, and later its sequel, Star Wars: Episode II – Attack of the Clones, where he was responsible for the unique movement of the Droideka destroyer droids.\n\nHis other credits include A.I. Artificial Intelligence, Space Cowboys, Dreamcatcher and Van Helsing. In 2007, Hickel won the BAFTA and the Academy Award for Best Visual Effects along with John Knoll, Charles Gibson and Allen Hall, for Pirates of the Caribbean: Dead Man's Chest. He also received an Academy Award nomination for his work on Rogue One: A Star Wars Story.\n\nDescription above from the Wikipedia article Hal Hickel, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": null, + "death": null, + "birthplace": "Bailey, Colorado, USA", + "homepage": null, + "known_for_department": "visual effects", + "gender": "male", + "updated_at": "2025-01-24T08:01:14.000Z" + } + }, + { + "job": "Visual Effects", + "jobs": [ + "Visual Effects" + ], + "person": { + "name": "Andy Hass", + "ids": { + "trakt": 535241, + "slug": "andy-hass", + "imdb": "nm3037885", + "tmdb": 1453929, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "visual effects", + "gender": null, + "updated_at": "2022-06-06T08:10:04.000Z" } }, { - "job": "Sound Re-Recording Mixer", + "job": "Visual Effects Supervisor", "jobs": [ - "Sound Re-Recording Mixer" + "Visual Effects Supervisor" ], "person": { - "name": "Christopher Boyes", + "name": "John Nelson", "ids": { - "trakt": 411301, - "slug": "christopher-boyes", - "imdb": "nm0102110", - "tmdb": 900, + "trakt": 537827, + "slug": "john-nelson-ce5eee21-c0df-40fe-ad63-ba2fa5b563f7", + "imdb": "nm0625471", + "tmdb": 1456374, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "John Nelson (born July 21, 1953) is an American visual effects supervisor. He has won two Academy Awards for Best Visual Effects for his work on the films Gladiator (2000) and Blade Runner 2049 (2017). He has also been nominated for I, Robot (2004) and Iron Man (2008).\n\nHe also won the 2018 British Academy of Film and Television Arts for the Special Visual Effects in Blade Runner 2049.\n\nAfter graduating with high distinction from the University of Michigan in 1976, he worked as a cameraman, technical director, and director at the pioneering computer animation and commercial production company Robert Abel and Associates, where he won two Clio awards and earned six additional Clio nominations. Early in his career while at ILM, Nelson modelled, animated, lit, and composited several shots for Terminator 2: Judgement Day (1991), including the iconic scene in which the shotgunned head of the chrome terminator splits open and reseals. He is a member of the Academy of Motion Picture Arts and Sciences, the Visual Effects Society, the International Cinematographers Guild, and the Directors Guild of America.\n\nDescription above from the Wikipedia article John Nelson, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "birthday": "1959-07-21", + "death": null, + "birthplace": "Detroit, Michigan, USA", + "homepage": "https://www.fullnelsonvfx.com/index.html", + "known_for_department": "visual effects", + "gender": "male", + "updated_at": "2024-12-22T08:07:00.000Z" + } + }, + { + "job": "Effects Supervisor", + "jobs": [ + "Effects Supervisor" + ], + "person": { + "name": "Shane Mahan", + "ids": { + "trakt": 602263, + "slug": "shane-mahan", + "imdb": "nm0536752", + "tmdb": 1600624, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Shane Patrick Mahan (born September 22, 1964) is an American special effects creator, creature designer, puppeteer, and producer known for his work at Stan Winston Studio and its successor, Legacy Effects. His film credits include The Terminator and Terminator 2: Judgement Day; Aliens; Predator and Predator 2; The Lost World: Jurassic Park; Iron Man, Iron Man 2 and Iron Man 3; Pacific Rim and The Shape of Water.\n\nMahan was born and raised in Greenville, Michigan. After graduating from Greenville High School in 1981, he left for Hollywood. His first job was with Stan Winston Studios as a crew member working on The Terminator in 1983. There he worked as a creature effects supervisor. After Stan Winston's passing, he formed Legacy Effects with three partners who were also his former colleagues at Stan Winston Studios, and this has now become the leading VFX company in Hollywood.\n\nMahan and his design team made the special suits worn by Robert Downey Jr. in the film Iron Man. They also designed the suit for Iron Man, which was ten feet tall, weighed 800 lbs, and required five operators to puppeteer. Besides several other nominations, Mahan and his team were nominated for the 2008 Oscar for 'Best Achievement in Visual Effects' for Iron Man. The team was hired as concept artists by James Cameron for his film Avatar. This was special for Mahan, as his first special effects credits were for the film The Terminator.\n\nMahan worked for Stan Winston Studios until the death of Stan Winston in 2008, when he and three other veterans of the company, Lindsay MacGowan, J. Alan Scott, and John Rosengrant, incorporated Legacy Effects, a character design, make-up, and animatronic studio so named in honour of the late Winston's legacy and lifelong achievements. He lives in Los Angeles.", + "birthday": "1964-09-22", + "death": null, + "birthplace": "Greenville, Michigan, USA", + "homepage": null, + "known_for_department": "visual effects", + "gender": "male", + "updated_at": "2025-02-22T08:05:52.000Z" + } + }, + { + "job": "Visual Effects Supervisor", + "jobs": [ + "Visual Effects Supervisor" + ], + "person": { + "name": "Ben Snow", + "ids": { + "trakt": 747447, + "slug": "ben-snow", + "imdb": "nm0811240", + "tmdb": 1715666, + "tvrage": null + }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Ben Snow is a special effects artist who has been nominated four times at the Academy Awards. He works at Industrial Light \u0026 Magic.\n\nSnow grew up in Australia, where he attended Narrabundah College and the University of Canberra. His university degree, which he completed in 1986, was a Bachelor of Arts in Computer Studies.\n\nDescription above from the Wikipedia article Ben Snow, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "visual effects", + "gender": "male", + "updated_at": "2024-11-01T08:01:18.000Z" } } ], @@ -2490,145 +7505,206 @@ "slug": "jon-favreau", "imdb": "nm0269463", "tmdb": 15277, - "tvrage": 29076 + "tvrage": null + }, + "social_ids": { + "twitter": "jon_favreau", + "facebook": "jonfavreau", + "instagram": "jonfavreau", + "wikipedia": null }, - "biography": "Jonathan Favreau (/ˈfævroʊ/; born October 19, 1966) is an American actor, director, producer, and screenwriter.\n\nAs an actor, he is known for roles in films such as \"Rudy\" (1993), \"Swingers\" (1996) (which he also wrote), \"Very Bad Things\" (1998), \"Daredevil\" (2003), \"The Break-Up\" (2006), \"Couples Retreat\" (2009), and \"Chef\" (2014) (which he also wrote and directed).\n\nHe has additionally directed the films \"Elf\" (2003), \"Zathura: A Space Adventure\" (2005), \"Iron Man\" (2008), \"Iron Man 2\" (2010), \"Cowboys & Aliens\" (2011), \"The Jungle Book\" (2016), and \"The Lion King\" (2019) and served as an executive producer on \"The Avengers\" (2012), \"Iron Man 3\" (2013), \"Avengers: Age of Ultron\" (2015), \"Avengers: Infinity War\" (2018) and \"Avengers: Endgame\" (2019). Favreau also portrays Happy Hogan in the Marvel Cinematic Universe and played Pete Becker during season three of the television sitcom \"Friends\". He produces films under his banner, Fairview Entertainment. The company has been credited as co-producers in most of Favreau's directorial ventures.\n\nDescription above from the Wikipedia article Jon Favreau, licensed under CC-BY-SA, full list of contributors on Wikipedia.", + "biography": "Jonathan Kolia Favreau (/ˈfævroʊ/ FAV-roh; born October 19, 1966) is an American filmmaker and actor. As an actor, Favreau has appeared in films such as Rudy (1993), PCU (1994), Swingers (1996), Very Bad Things (1998), Deep Impact (1998), The Replacements (2000), Daredevil (2003), The Break-Up (2006), Four Christmases (2008), Couples Retreat (2009), I Love You, Man (2009), People Like Us (2012), The Wolf of Wall Street (2013), and Chef (2014).\n\nAs a filmmaker, Favreau has been significantly involved with the Marvel Cinematic Universe. He directed, produced, and appeared as Happy Hogan in the films Iron Man (2008) and Iron Man 2 (2010). He also served as an executive producer for and/or appeared as the character in the films The Avengers (2012), Iron Man 3 (2013), Avengers: Age of Ultron (2015), Spider-Man: Homecoming (2017), Avengers: Infinity War (2018), Avengers: Endgame (2019), Spider-Man: Far From Home (2019), Spider-Man: No Way Home (2021), and Deadpool \u0026 Wolverine (2024).\n\nHe has also directed the films Elf (2003), Zathura: A Space Adventure (2005), Cowboys \u0026 Aliens (2011), Chef (2014), The Jungle Book (2016), The Lion King (2019), and The Mandalorian \u0026 Grogu (2026). Recently, Favreau has been known for his work on the Star Wars franchise with Dave Filoni, creating the Disney+ original series The Mandalorian (2019–present), which Filoni helped develop, with both serving as executive producers. Alongside Filoni, he serves as an executive producer on all of the show's spin-off series, including The Book of Boba Fett, Ahsoka, and the upcoming Skeleton Crew. He produces films under his production company banner, Fairview Entertainment, and also presented the variety series Dinner for Five and the cooking series The Chef Show.\n\nDescription above from the Wikipedia article Jon Favreau, licensed under CC-BY-SA, full list of contributors on Wikipedia.", "birthday": "1966-10-19", "death": null, - "birthplace": "Queens, New York, USA", - "homepage": null + "birthplace": "Queens, New York City, New York, USA", + "homepage": null, + "known_for_department": "acting", + "gender": "male", + "updated_at": "2025-02-13T20:04:33.000Z" } }, { - "job": "Script Supervisor", + "job": "First Assistant Director", "jobs": [ - "Script Supervisor" + "First Assistant Director" ], "person": { - "name": "Cristina Weigmann", + "name": "Eric Heffron", "ids": { - "trakt": 469606, - "slug": "cristina-weigmann", - "imdb": "nm0917881", - "tmdb": 1397300, + "trakt": 146027, + "slug": "eric-heffron", + "imdb": "nm0373610", + "tmdb": 143894, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", - "birthday": "1971-01-27", - "death": "2009-07-28", - "birthplace": "Beirut, Lebanon", - "homepage": null + "birthday": null, + "death": null, + "birthplace": null, + "homepage": null, + "known_for_department": "directing", + "gender": "male", + "updated_at": "2024-10-21T17:06:45.000Z" } - } - ], - "visual effects": [ + }, { - "job": "Animation Supervisor", + "job": "Second Unit Director", "jobs": [ - "Animation Supervisor" + "Second Unit Director" ], "person": { - "name": "Hal T. Hickel", + "name": "Phil Neilson", "ids": { - "trakt": 506279, - "slug": "hal-t-hickel", - "imdb": "nm0382579", - "tmdb": 1426773, + "trakt": 423807, + "slug": "phil-neilson", + "imdb": "nm0624827", + "tmdb": 1378239, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Philip Cody Neilson is an American stunt coordinator, stunt performer, assistant director, and actor.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "crew", + "gender": "male", + "updated_at": "2025-02-16T08:06:56.000Z" } }, { - "job": "Visual Effects", + "job": "Script Supervisor", "jobs": [ - "Visual Effects" + "Script Supervisor" ], "person": { - "name": "Andy Hass", + "name": "Cristina Weigmann", "ids": { - "trakt": 535241, - "slug": "andy-hass", - "imdb": "nm3037885", - "tmdb": 1453929, + "trakt": 469606, + "slug": "cristina-weigmann", + "imdb": "nm0917881", + "tmdb": 1397300, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", - "birthday": null, - "death": null, - "birthplace": null, - "homepage": null + "birthday": "1971-01-27", + "death": "2009-07-28", + "birthplace": "Beirut, Lebanon", + "homepage": null, + "known_for_department": "directing", + "gender": "female", + "updated_at": "2024-10-25T08:09:51.000Z" } }, { - "job": "Senior Visual Effects Supervisor", + "job": "Script Supervisor", "jobs": [ - "Senior Visual Effects Supervisor" + "Script Supervisor" ], "person": { - "name": "John Nelson", + "name": "Rebecca Robertson", "ids": { - "trakt": 537827, - "slug": "john-nelson-ce5eee21-c0df-40fe-ad63-ba2fa5b563f7", - "imdb": "nm0625471", - "tmdb": 1456374, + "trakt": 495015, + "slug": "rebecca-robertson", + "imdb": "nm0731980", + "tmdb": 1416438, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "directing", + "gender": "female", + "updated_at": "2024-07-24T08:10:22.000Z" } }, { - "job": "Effects Supervisor", + "job": "Second Assistant Director", "jobs": [ - "Effects Supervisor" + "Second Assistant Director" ], "person": { - "name": "Shane Mahan", + "name": "Michael J. Moore", "ids": { - "trakt": 602263, - "slug": "shane-mahan", - "imdb": null, - "tmdb": 1600624, + "trakt": 778856, + "slug": "michael-j-moore-f9ccb9f6-5c81-4519-ac76-de931812b9de", + "imdb": "nm0601635", + "tmdb": 1734699, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "directing", + "gender": "male", + "updated_at": "2024-06-03T08:08:05.000Z" } - }, + } + ], + "lighting": [ { - "job": "Visual Effects Supervisor", + "job": "Gaffer", "jobs": [ - "Visual Effects Supervisor" + "Gaffer" ], "person": { - "name": "Ben Snow", + "name": "Michael Bauman", "ids": { - "trakt": 747447, - "slug": "ben-snow", - "imdb": null, - "tmdb": 1715666, + "trakt": 516475, + "slug": "michael-bauman", + "imdb": "nm0062177", + "tmdb": 1435657, "tvrage": null }, - "biography": "", + "social_ids": { + "twitter": "BaumanMike", + "facebook": null, + "instagram": null, + "wikipedia": null + }, + "biography": "Michael Bauman is mostly known as chief lighting technician and gaffer. He worked with Paul Thomas Anderson on The Master (2012), Inherent Vice (2014), Radiohead's Daydreaming music video (2016), Phantom Thread (2017), and shared director of photography credits with Anderson on Licorice Pizza (2021). Other films Bauman worked on include Training Day (2001), Munich (2005), Iron Man (2008), Iron Man 2 (2010), Nightcrawler (2014), Ford v Ferrari (2019), The Tragedy of Macbeth (2021), as well as the upcoming David O. Russell film.", "birthday": null, "death": null, "birthplace": null, - "homepage": null + "homepage": null, + "known_for_department": "camera", + "gender": "male", + "updated_at": "2024-07-21T08:09:13.000Z" } - } - ], - "lighting": [ + }, { "job": "Lighting Technician", "jobs": [ @@ -2643,11 +7719,20 @@ "tmdb": 2457356, "tvrage": null }, + "social_ids": { + "twitter": null, + "facebook": null, + "instagram": null, + "wikipedia": null + }, "biography": "", "birthday": "1958-03-29", "death": "2019-11-13", "birthplace": "Los Angeles, California, USA", - "homepage": null + "homepage": null, + "known_for_department": "lighting", + "gender": "male", + "updated_at": "2022-05-23T08:32:49.000Z" } } ] diff --git a/Tests/TraktKitTests/Models/Movies/test_get_movie_comments.json b/Tests/TraktKitTests/Models/Movies/test_get_movie_comments.json index cef9fff..549fcad 100644 --- a/Tests/TraktKitTests/Models/Movies/test_get_movie_comments.json +++ b/Tests/TraktKitTests/Models/Movies/test_get_movie_comments.json @@ -1,24 +1,28 @@ [ - { - "review" : false, - "user_rating" : 8, - "replies" : 1, - "id" : 8, - "created_at" : "2011-03-25T22:35:17.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 0, - "updated_at" : "2011-03-25T22:35:17.000Z", - "comment" : "Great movie!", - "spoiler" : false, - "likes" : 0 - } + { + "id": 8, + "parent_id": 0, + "created_at": "2011-03-25T22:35:17.000Z", + "updated_at": "2011-03-25T22:35:17.000Z", + "comment": "Great movie!", + "spoiler": false, + "review": false, + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Movies/test_get_movie_studios.json b/Tests/TraktKitTests/Models/Movies/test_get_movie_studios.json new file mode 100644 index 0000000..491fa30 --- /dev/null +++ b/Tests/TraktKitTests/Models/Movies/test_get_movie_studios.json @@ -0,0 +1,47 @@ +[ + { + "name": "20th Century Fox", + "country": "us", + "ids": { + "trakt": 20, + "slug": "20th-century-fox", + "tmdb": 25 + } + }, + { + "name": "Marvel Entertainment", + "country": "us", + "ids": { + "trakt": 19, + "slug": "marvel-entertainment", + "tmdb": 7505 + } + }, + { + "name": "The Donners' Company", + "country": "us", + "ids": { + "trakt": 25, + "slug": "the-donners-company", + "tmdb": 431 + } + }, + { + "name": "TSG Entertainment", + "country": "us", + "ids": { + "trakt": 22, + "slug": "tsg-entertainment", + "tmdb": 22213 + } + }, + { + "name": "Genre Films", + "country": "us", + "ids": { + "trakt": 23, + "slug": "genre-films", + "tmdb": 28788 + } + } +] diff --git a/Tests/TraktKitTests/Models/Season/test_get_season_comments.json b/Tests/TraktKitTests/Models/Season/test_get_season_comments.json index e64b1e4..a919087 100644 --- a/Tests/TraktKitTests/Models/Season/test_get_season_comments.json +++ b/Tests/TraktKitTests/Models/Season/test_get_season_comments.json @@ -1,24 +1,28 @@ [ - { - "review" : false, - "user_rating" : 8, - "replies" : 1, - "id" : 8, - "created_at" : "2011-03-25T22:35:17.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 0, - "updated_at" : "2011-03-25T22:35:17.000Z", - "comment" : "Great season!", - "spoiler" : false, - "likes" : 0 - } + { + "id": 8, + "parent_id": 0, + "created_at": "2011-03-25T22:35:17.000Z", + "updated_at": "2011-03-25T22:35:17.000Z", + "comment": "Great season!", + "spoiler": false, + "review": false, + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Shows/ShowCastAndCrew_GuestStars.json b/Tests/TraktKitTests/Models/Shows/ShowCastAndCrew_GuestStars.json index f0f3922..a1413ba 100644 --- a/Tests/TraktKitTests/Models/Shows/ShowCastAndCrew_GuestStars.json +++ b/Tests/TraktKitTests/Models/Shows/ShowCastAndCrew_GuestStars.json @@ -10899,4 +10899,3 @@ } ] } - diff --git a/Tests/TraktKitTests/Models/Shows/Show_Images.json b/Tests/TraktKitTests/Models/Shows/Show_Images.json new file mode 100644 index 0000000..7e24673 --- /dev/null +++ b/Tests/TraktKitTests/Models/Shows/Show_Images.json @@ -0,0 +1,32 @@ +{ + "title": "Severance", + "year": 2022, + "ids": { + "trakt": 154997, + "slug": "severance", + "tvdb": 371980, + "imdb": "tt11280740", + "tmdb": 95396, + "tvrage": null + }, + "images": { + "fanart": [ + "walter-r2.trakt.tv/images/shows/000/154/997/fanarts/medium/9400ecb8e2.jpg.webp" + ], + "poster": [ + "walter-r2.trakt.tv/images/shows/000/154/997/posters/thumb/f60ddb06de.jpg.webp" + ], + "logo": [ + "walter-r2.trakt.tv/images/shows/000/154/997/logos/medium/29510e9cd0.png.webp" + ], + "clearart": [ + "walter-r2.trakt.tv/images/shows/000/154/997/cleararts/medium/d3639066f2.png.webp" + ], + "banner": [ + "walter-r2.trakt.tv/images/shows/000/154/997/banners/medium/883c234c02.jpg.webp" + ], + "thumb": [ + "walter-r2.trakt.tv/images/shows/000/154/997/thumbs/medium/1dcf1005c7.jpg.webp" + ] + } +} diff --git a/Tests/TraktKitTests/Models/Shows/test_get_most_watched_shows.json b/Tests/TraktKitTests/Models/Shows/test_get_most_watched_shows.json index 690f38e..8a3e413 100644 --- a/Tests/TraktKitTests/Models/Shows/test_get_most_watched_shows.json +++ b/Tests/TraktKitTests/Models/Shows/test_get_most_watched_shows.json @@ -1,172 +1,172 @@ [ - { - "collector_count" : 73224, - "watcher_count" : 203742, - "show" : { - "title" : "Game of Thrones", - "year" : 2011, - "ids" : { - "tmdb" : 1399, - "slug" : "game-of-thrones", - "tvdb" : 121361, - "trakt" : 1390, - "imdb" : "tt0944947" - } + { + "watcher_count": 203742, + "play_count": 8784154, + "collected_count": 2436824, + "collector_count": 73224, + "show": { + "title": "Game of Thrones", + "year": 2011, + "ids": { + "trakt": 1390, + "slug": "game-of-thrones", + "tvdb": 121361, + "imdb": "tt0944947", + "tmdb": 1399 + } + } }, - "play_count" : 8784154, - "collected_count" : 2436824 - }, - { - "collector_count" : 57617, - "watcher_count" : 163097, - "show" : { - "title" : "The Walking Dead", - "year" : 2010, - "ids" : { - "tmdb" : 1402, - "slug" : "the-walking-dead", - "tvdb" : 153021, - "trakt" : 1393, - "imdb" : "tt1520211" - } + { + "watcher_count": 163097, + "play_count": 9945882, + "collected_count": 2423833, + "collector_count": 57617, + "show": { + "title": "The Walking Dead", + "year": 2010, + "ids": { + "trakt": 1393, + "slug": "the-walking-dead", + "tvdb": 153021, + "imdb": "tt1520211", + "tmdb": 1402 + } + } }, - "play_count" : 9945882, - "collected_count" : 2423833 - }, - { - "collector_count" : 54953, - "watcher_count" : 155291, - "show" : { - "title" : "The Big Bang Theory", - "year" : 2007, - "ids" : { - "tmdb" : 1418, - "slug" : "the-big-bang-theory", - "tvdb" : 80379, - "trakt" : 1409, - "imdb" : "tt0898266" - } + { + "watcher_count": 155291, + "play_count": 23542030, + "collected_count": 6635583, + "collector_count": 54953, + "show": { + "title": "The Big Bang Theory", + "year": 2007, + "ids": { + "trakt": 1409, + "slug": "the-big-bang-theory", + "tvdb": 80379, + "imdb": "tt0898266", + "tmdb": 1418 + } + } }, - "play_count" : 23542030, - "collected_count" : 6635583 - }, - { - "collector_count" : 49385, - "watcher_count" : 140725, - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1388, - "imdb" : "tt0903747" - } + { + "watcher_count": 140725, + "play_count": 8576210, + "collected_count": 2471061, + "collector_count": 49385, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1388, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } }, - "play_count" : 8576210, - "collected_count" : 2471061 - }, - { - "collector_count" : 43351, - "watcher_count" : 123797, - "show" : { - "title" : "Arrow", - "year" : 2012, - "ids" : { - "tmdb" : 1412, - "slug" : "arrow", - "tvdb" : 257655, - "trakt" : 1403, - "imdb" : "tt2193021" - } + { + "watcher_count": 123797, + "play_count": 6173692, + "collected_count": 1856666, + "collector_count": 43351, + "show": { + "title": "Arrow", + "year": 2012, + "ids": { + "trakt": 1403, + "slug": "arrow", + "tvdb": 257655, + "imdb": "tt2193021", + "tmdb": 1412 + } + } }, - "play_count" : 6173692, - "collected_count" : 1856666 - }, - { - "collector_count" : 32335, - "watcher_count" : 100242, - "show" : { - "title" : "How I Met Your Mother", - "year" : 2005, - "ids" : { - "tmdb" : 1100, - "slug" : "how-i-met-your-mother", - "tvdb" : 75760, - "trakt" : 1095, - "imdb" : "tt0460649" - } + { + "watcher_count": 100242, + "play_count": 19787304, + "collected_count": 4529880, + "collector_count": 32335, + "show": { + "title": "How I Met Your Mother", + "year": 2005, + "ids": { + "trakt": 1095, + "slug": "how-i-met-your-mother", + "tvdb": 75760, + "imdb": "tt0460649", + "tmdb": 1100 + } + } }, - "play_count" : 19787304, - "collected_count" : 4529880 - }, - { - "collector_count" : 34077, - "watcher_count" : 94282, - "show" : { - "title" : "Sherlock", - "year" : 2010, - "ids" : { - "tmdb" : 19885, - "slug" : "sherlock", - "tvdb" : 176941, - "trakt" : 19792, - "imdb" : "tt1475582" - } + { + "watcher_count": 94282, + "play_count": 912066, + "collected_count": 260713, + "collector_count": 34077, + "show": { + "title": "Sherlock", + "year": 2010, + "ids": { + "trakt": 19792, + "slug": "sherlock", + "tvdb": 176941, + "imdb": "tt1475582", + "tmdb": 19885 + } + } }, - "play_count" : 912066, - "collected_count" : 260713 - }, - { - "collector_count" : 39724, - "watcher_count" : 89242, - "show" : { - "title" : "Homeland", - "year" : 2011, - "ids" : { - "tmdb" : 1407, - "slug" : "homeland", - "tvdb" : 247897, - "trakt" : 1398, - "imdb" : "tt1796960" - } + { + "watcher_count": 89242, + "play_count": 3375660, + "collected_count": 1299956, + "collector_count": 39724, + "show": { + "title": "Homeland", + "year": 2011, + "ids": { + "trakt": 1398, + "slug": "homeland", + "tvdb": 247897, + "imdb": "tt1796960", + "tmdb": 1407 + } + } }, - "play_count" : 3375660, - "collected_count" : 1299956 - }, - { - "collector_count" : 33424, - "watcher_count" : 87059, - "show" : { - "title" : "Dexter", - "year" : 2006, - "ids" : { - "tmdb" : 1405, - "slug" : "dexter", - "tvdb" : 79349, - "trakt" : 1396, - "imdb" : "tt0773262" - } + { + "watcher_count": 87059, + "play_count": 7799337, + "collected_count": 2239477, + "collector_count": 33424, + "show": { + "title": "Dexter", + "year": 2006, + "ids": { + "trakt": 1396, + "slug": "dexter", + "tvdb": 79349, + "imdb": "tt0773262", + "tmdb": 1405 + } + } }, - "play_count" : 7799337, - "collected_count" : 2239477 - }, - { - "collector_count" : 31982, - "watcher_count" : 83087, - "show" : { - "title" : "Marvel's Agents of S.H.I.E.L.D.", - "year" : 2013, - "ids" : { - "tmdb" : 1403, - "slug" : "marvel-s-agents-of-s-h-i-e-l-d", - "tvdb" : 263365, - "trakt" : 1394, - "imdb" : "tt2364582" - } - }, - "play_count" : 2795758, - "collected_count" : 881552 - } + { + "watcher_count": 83087, + "play_count": 2795758, + "collected_count": 881552, + "collector_count": 31982, + "show": { + "title": "Marvel's Agents of S.H.I.E.L.D.", + "year": 2013, + "ids": { + "trakt": 1394, + "slug": "marvel-s-agents-of-s-h-i-e-l-d", + "tvdb": 263365, + "imdb": "tt2364582", + "tmdb": 1403 + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Shows/test_get_show_comments.json b/Tests/TraktKitTests/Models/Shows/test_get_show_comments.json index d10f0a8..aae9ad8 100644 --- a/Tests/TraktKitTests/Models/Shows/test_get_show_comments.json +++ b/Tests/TraktKitTests/Models/Shows/test_get_show_comments.json @@ -1,24 +1,382 @@ [ - { - "review" : false, - "user_rating" : 8, - "replies" : 1, - "id" : 8, - "created_at" : "2011-03-25T22:35:17.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 0, - "updated_at" : "2011-03-25T22:35:17.000Z", - "comment" : "Great show!", - "spoiler" : false, - "likes" : 0 - } + { + "id": 755342, + "comment": "I did NOT expect that ending. Wow! What a show!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2025-01-04T02:57:47.000Z", + "updated_at": "2025-01-04T02:57:47.000Z", + "replies": 0, + "likes": 0, + "user_rating": null, + "user_stats": { + "rating": null, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "krissybdavis", + "private": false, + "deleted": false, + "name": "Krissy", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "krissybdavis" + }, + "joined_at": "2024-11-14T17:09:55.000Z", + "location": "Atlanta, Georgia", + "about": null, + "gender": "female", + "age": null, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/014/738/689/avatars/large/47431ac676.jpg" + } + } + } + }, + { + "id": 726677, + "comment": "Jake Gyllenhaal was so sketchy for real. As a mystery, it did it's job of sucking me in. It was ok.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-10-24T07:27:49.000Z", + "updated_at": "2024-10-24T07:27:49.000Z", + "replies": 0, + "likes": 0, + "user_rating": null, + "user_stats": { + "rating": null, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "caragwapa", + "private": false, + "deleted": false, + "name": "Cara", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "caragwapa" + }, + "joined_at": "2023-05-26T07:38:29.000Z", + "location": "Cebu City, Philippines", + "about": null, + "gender": "female", + "age": 42, + "images": { + "avatar": { + "full": "https://secure.gravatar.com/avatar/499d0e10eca26c2f496a842aa0b93312?d=https%3A%2F%2Fwalter-r2.trakt.tv%2Fhotlink-ok%2Fplaceholders%2Fmedium%2Fleela.png\u0026r=pg\u0026s=256" + } + } + } + }, + { + "id": 726002, + "comment": "The show was wonderful up until the end, sadly. What a massive disappointment.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-10-22T14:12:00.000Z", + "updated_at": "2024-10-22T14:12:00.000Z", + "replies": 0, + "likes": 0, + "user_rating": 7, + "user_stats": { + "rating": 7, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "LaterGator", + "private": false, + "deleted": false, + "name": "LG", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "latergator" + }, + "joined_at": "2022-10-09T20:11:55.000Z", + "location": "", + "about": "", + "gender": "male", + "age": null, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/012/259/420/avatars/large/d8fc8b9573.png" + } + } + } + }, + { + "id": 725385, + "comment": "Don't waste your time. Ultimately it's just so frustrating.\n\nDavid E Kelley seems to revel in this approach to tv shows. Plod along, end the episode with a shock, next episode ignores the shock and plods along until it ends with a shock, repeat for several episodes, pushing the viewer hard toward a reveal, then end with a left-field under-cooked twist.\n\nWaste of time.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-10-20T19:42:02.000Z", + "updated_at": "2024-10-20T19:42:02.000Z", + "replies": 0, + "likes": 1, + "user_rating": 4, + "user_stats": { + "rating": 4, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "PorterUk", + "private": false, + "deleted": false, + "name": "PorterUk", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "porteruk" + }, + "joined_at": "2017-11-16T11:51:48.000Z", + "location": "England", + "about": "", + "gender": "male", + "age": 45, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/003/416/325/avatars/large/05aa466845.jpg" + } + } + } + }, + { + "id": 714934, + "comment": "Classic case of a modern show not knowing how to write the ending of a solid series. Good family drama and mystery with some great acting.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-09-20T15:07:22.000Z", + "updated_at": "2024-09-20T15:07:22.000Z", + "replies": 0, + "likes": 0, + "user_rating": 7, + "user_stats": { + "rating": 7, + "play_count": 9, + "completed_count": 8 + }, + "user": { + "username": "kidmodo", + "private": false, + "deleted": false, + "name": "Kidmodo", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "kidmodo" + }, + "joined_at": "2023-09-22T13:01:25.000Z", + "location": "Baltimore MD", + "about": "", + "gender": "male", + "age": 29, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/013/052/697/avatars/large/9a6314d1e4.png" + } + } + } + }, + { + "id": 707456, + "comment": "This is a series I enjoyed very much! I am a big fan of Jake Gyllenhaal. But I also realy liked the performances of Fagbenle and Peter Sarsgaard. It's a good series! I definitely recommend it! There is a lot of suspense and I liked the twists!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-08-26T12:25:58.000Z", + "updated_at": "2024-08-26T12:25:58.000Z", + "replies": 0, + "likes": 0, + "user_rating": 7, + "user_stats": { + "rating": 7, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "pato22", + "private": false, + "deleted": false, + "name": "Wéjih M'zoughi", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "pato22" + }, + "joined_at": "2017-01-26T14:13:25.000Z", + "location": "Hogwarts", + "about": "", + "gender": "male", + "age": 28, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/002/650/222/avatars/large/9b32b56669.gif" + } + } + } + }, + { + "id": 704369, + "comment": "weird they said season 1 is last now they added one", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-08-16T05:43:58.000Z", + "updated_at": "2024-08-16T05:43:58.000Z", + "replies": 0, + "likes": 0, + "user_rating": 8, + "user_stats": { + "rating": 8, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "marionaise1990", + "private": false, + "deleted": false, + "name": "Mario Helmes", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "marionaise1990" + }, + "joined_at": "2024-05-25T13:50:33.000Z", + "location": "Landgraaf, Limburg", + "about": "", + "gender": "male", + "age": 34, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/013/878/832/avatars/large/a6868f2131.png" + } + } + } + }, + { + "id": 703287, + "comment": "Overall a good entertaining show!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-08-12T20:01:54.000Z", + "updated_at": "2024-08-12T20:01:54.000Z", + "replies": 0, + "likes": 0, + "user_rating": 7, + "user_stats": { + "rating": 7, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "astrix", + "private": false, + "deleted": false, + "name": "Astrix", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "astrix" + }, + "joined_at": "2014-12-25T19:04:12.000Z", + "location": "Uranus", + "about": "I was looking out at the vastness of the ocean, and it was overwhelming. I imagined a point way out there and how deep it would go. Then I get the vastness of the ocean and in that moment I can understand my pain and the size of my fear. My actual self is so tied up with it. With shame. ❖", + "gender": "male", + "age": null, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/000/782/038/avatars/large/594279c981.gif" + } + } + } + }, + { + "id": 702944, + "comment": "Started of interesting and the pacing was good, but ruined by that twist at the end. Just not believable or plausible and made the whole thing pointless to watch in the end. That twist was unnecessary and could have been done many ways better. Rest of the show and the acting is quite good but does feel dragged in the middle", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-08-11T18:19:45.000Z", + "updated_at": "2024-08-12T16:40:30.000Z", + "replies": 0, + "likes": 0, + "user_rating": 6, + "user_stats": { + "rating": 6, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "Erik7Aeiou", + "private": false, + "deleted": false, + "name": "Erik7Aeiou", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "erik7aeiou" + }, + "joined_at": "2024-08-09T09:41:55.000Z", + "location": "", + "about": "", + "gender": "", + "age": null, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/014/219/490/avatars/large/d36656b71e.jpg" + } + } + } + }, + { + "id": 702913, + "comment": "This could've been a movie.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2024-08-11T16:36:32.000Z", + "updated_at": "2024-08-11T16:36:32.000Z", + "replies": 1, + "likes": 0, + "user_rating": null, + "user_stats": { + "rating": null, + "play_count": 8, + "completed_count": 8 + }, + "user": { + "username": "sherlockedx", + "private": false, + "deleted": false, + "name": "isaías", + "vip": false, + "vip_ep": false, + "ids": { + "slug": "sherlockedx" + }, + "joined_at": "2013-10-05T21:26:21.000Z", + "location": "Brazil", + "about": "", + "gender": "male", + "age": 26, + "images": { + "avatar": { + "full": "https://walter-r2.trakt.tv/images/users/000/320/602/avatars/large/4bdd6af46a.png" + } + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Shows/test_get_show_translations.json b/Tests/TraktKitTests/Models/Shows/test_get_show_translations.json index 19fabdc..8925307 100644 --- a/Tests/TraktKitTests/Models/Shows/test_get_show_translations.json +++ b/Tests/TraktKitTests/Models/Shows/test_get_show_translations.json @@ -1,17 +1,282 @@ [ - { - "title" : "Breaking Bad", - "language" : "en", - "overview" : "Breaking Bad is an American crime drama television series created and produced by Vince Gilligan. Set and produced in Albuquerque, New Mexico, Breaking Bad is the story of Walter White, a struggling high school chemistry teacher who is diagnosed with inoperable lung cancer at the beginning of the series. He turns to a life of crime, producing and selling methamphetamine, in order to secure his family's financial future before he dies, teaming with his former student, Jesse Pinkman. Heavily serialized, the series is known for positioning its characters in seemingly inextricable corners and has been labeled a contemporary western by its creator." - }, - { - "title" : "Breaking Bad", - "language" : "tr", - "overview" : "..." - }, - { - "title" : "Perníkový tatko", - "language" : "sk", - "overview" : "" - } + { + "title": null, + "overview": "جريمة قتل مريعة تقلب مكتب النائب العام رأساً على عقب عندما يُتهم أحد أعضاءه بارتكاب الجريمة، ويبدأ المتهم بالمعاناة ليحافظ على شمل عائلته.", + "tagline": "لا تفترض شيئًا.", + "language": "ar", + "country": "ae" + }, + { + "title": null, + "overview": "جريمة قتل مريعة تقلب مكتب النائب العام رأساً على عقب عندما يُتهم أحد أعضاءه بارتكاب الجريمة، ويبدأ المتهم بالمعاناة ليحافظ على شمل عائلته.", + "tagline": "لا تفترض شيئًا.", + "language": "ar", + "country": "sa" + }, + { + "title": "Nedostatek důkazů", + "overview": "Příšerná vražda otřese základy chicagské prokuratury. Ze zločinu je totiž podezřelý někdo z jejích řad, kdo nyní musí bojovat za udržení vlastní rodiny pohromadě.", + "tagline": "Čekejte nečekané.", + "language": "cs", + "country": "cz" + }, + { + "title": null, + "overview": "Et grusomt drab vender op og ned på den offentlige anklagers kontor i Chicago, da en af kollegerne mistænkes for forbrydelsen – så den anklagede må kæmpe for at holde sammen på sin familie.", + "tagline": "Intet kan tages for givet.", + "language": "da", + "country": "dk" + }, + { + "title": "Aus Mangel an Beweisen", + "overview": "Ein brutaler Mord erschüttert die Staatsanwaltschaft von Chicago, als einer ihrer Mitarbeiter des Verbrechens beschuldigt wird und der Beschuldigte um den Zusammenhalt seiner Familie kämpfen muss.", + "tagline": "Nichts ist, wie es scheint.", + "language": "de", + "country": "de" + }, + { + "title": "Αθώος Μέχρι Αποδείξεως του Εναντίου", + "overview": "Ένας φρικτός φόνος αναστατώνει την Εισαγγελία του Σικάγο όταν ένας δικός της γίνεται ύποπτος για το έγκλημα, την ώρα που ο ίδιος παλεύει να κρατήσει την οικογένειά του ενωμένη.", + "tagline": "Μην υποθέτεις τίποτα.", + "language": "el", + "country": "gr" + }, + { + "title": "Presumed Innocent", + "overview": "A horrific murder upends the Chicago Prosecuting Attorney's Office when one of its own is suspected of the crime—leaving the accused fighting to keep his family together.", + "tagline": "Presume nothing.", + "language": "en", + "country": "us" + }, + { + "title": "Presunto inocente", + "overview": "Un asesinato horrible trastoca a la Fiscalía de Chicago cuando uno de los suyos es sospechoso del crimen. El acusado deberá luchar por mantener unida a su familia.", + "tagline": "Nada es lo que parece.", + "language": "es", + "country": "es" + }, + { + "title": "Se presume inocente", + "overview": "Un asesinato espantoso pone de cabeza la oficina de la fiscalía de Chicago cuando uno de los suyos resulta sospechoso del crimen, por lo que el acusado tendrá que luchar por mantener unida a su familia.", + "tagline": "Olvida lo que creías saber.", + "language": "es", + "country": "mx" + }, + { + "title": null, + "overview": "پس از اینکه یکی از اعضای دفتر دادستانی شیکاگو به طرز وحشتناکی به قتل می رسد، یک نفر از همین اعضا به مظنون اصلی این جنایت تبدیل می شود و...", + "tagline": null, + "language": "fa", + "country": "ir" + }, + { + "title": null, + "overview": "Kamala murha murtaa Chicagon syyttäjänviraston, kun yhtä sen omista epäillään rikoksesta. Syytetty joutuu taistelemaan pitääkseen perheensä koossa.", + "tagline": "Älä oleta mitään.", + "language": "fi", + "country": "fi" + }, + { + "title": "Présumé innocent", + "overview": "Un horrible meurtre bouleverse le bureau du procureur de Chicago quand un de ses membres est suspecté dans cette affaire, obligeant l’accusé à se battre pour garder sa famille unie.", + "tagline": "Ne présumez de rien.", + "language": "fr", + "country": "fr" + }, + { + "title": "Présumé innocent", + "overview": "Un horrible meurtre bouleverse le bureau du procureur de Chicago quand un de ses membres est suspecté dans cette affaire, obligeant l’accusé à se battre pour garder sa famille unie.", + "tagline": "Ne présumez de rien.", + "language": "fr", + "country": "ca" + }, + { + "title": null, + "overview": "רצח נורא מזעזע את משרד התובע המחוזי של שיקגו. כשאחד מעובדיו נחשד בביצוע הפשע, הוא נאלץ להילחם כדי שמשפחתו לא תתפרק.", + "tagline": "אסור להניח מראש שום דבר.", + "language": "he", + "country": "il" + }, + { + "title": "प्रिज़्यूम्ड इनोसेंट", + "overview": "एक भयावह हत्या से शिकागो अभियोजन वकील के कार्यालय में तब उथल-पुथल मच जाती है, जब उसका अपना ही एक व्यक्ति शक के घेरे में आ जाता है—और अभियुक्त को अपने परिवार को साथ बनाए रखने के लिए संघर्ष करना पड़ता है।", + "tagline": "पहले से कुछ मानकर न चलें।", + "language": "hi", + "country": "in" + }, + { + "title": "Ártatlanságra ítélve", + "overview": "Szörnyű gyilkosság forgatja fel a Chicagói Ügyészség hivatalát, amikor az egyik emberüket vádolják meg az elkövetéssel. A gyanúsítottnak azzal is meg kell küzdenie, hogy összetartsa a családját.", + "tagline": "Várd ki a végét.", + "language": "hu", + "country": "hu" + }, + { + "title": null, + "overview": "Pembunuhan yang mengerikan menggaduhkan Kantor Kejaksaan Chicago ketika salah satu dari mereka dicurigai melakukan kejahatan tersebut dan menyebabkan si tertuduh harus berjuang untuk menjaga keutuhan keluarganya.", + "tagline": "Jangan menduga apa pun.", + "language": "id", + "country": "id" + }, + { + "title": "Presunto innocente", + "overview": "Un terribile omicidio getta nel caos l’ufficio del procuratore di Chicago quando un dipendente è sospettato del crimine. L’accusato dovrà lottare per tenere insieme la propria famiglia.", + "tagline": "Niente è come te lo aspetti.", + "language": "it", + "country": "it" + }, + { + "title": "推定無罪", + "overview": "恐ろしい殺人事件が発生し、シカゴの検察官に容疑がかけられる。当局が大混乱に陥る一方で、容疑者の男は家族を守るために闘っていた。", + "tagline": "推測は、推測に過ぎない。", + "language": "ja", + "country": "jp" + }, + { + "title": "'무죄추정' - Presumed Innocent", + "overview": "끔찍한 살인 사건의 용의자로 동료 중 한 명이 지목되자 시카고 검찰청이 뒤집히고, 용의자는 가족을 지키기 위해 싸울 수밖에 없다.", + "tagline": "아무 것도 확신하지 마라.", + "language": "ko", + "country": "kr" + }, + { + "title": null, + "overview": "Serialas apie Čikagos prokurorą, apkaltintą savo kolegos nužudymu. Kai Rozat „Rusty“ Sabich nėra namuose su žmona Barbara ir vaikais Jadenu ir Kyle'u, jis dirba D.A. pagrindiniu kaltinimo advokatu. Taigi, kai jų kolegė Carolyn Polhemus randama negyva, Horganas paskiria jį į bylą. Pakankamai sunku susidoroti su kolegos netektimi, bet viskas pablogėja, kai varžovai Nico Della Guardia ir Tommy Molto, kurie artėjančiuose rinkimuose kovoja dėl Horgano ir Sabicho darbo vietų, naudojasi jos mirtimi. kaip politinė galimybė. Tuo tarpu bylą apsunkina tai, kad Polhemus kūnas buvo paliktas taip pat, kaip kalinamas žudikas Liamas Reinoldsas, vyras, kurį ji ir Sabichas nuteisė prieš kelerius metus, tariamai paliko savo auką. Tačiau kai įkalčiai pradeda krypti netikėtomis kryptimis, Sabichas atskleidžia tiesą apie savo santykių su mirusiuoju pobūdį, todėl jis tampa pagrindiniu įtariamuoju ir dėl to jo gyvenimas išsklaido.", + "tagline": null, + "language": "lt", + "country": "lt" + }, + { + "title": null, + "overview": "Na een gruwelijke moord is een advocatenkantoor in Chicago in rep en roer als een van hen verdacht wordt van de misdaad. De aangeklaagde vecht om zijn gezin bijeen te houden.", + "tagline": "Ga nergens vanuit.", + "language": "nl", + "country": "nl" + }, + { + "title": null, + "overview": "Et grusomt drap ryster statsadvokatens kontor i Chicagos når en av deres egne blir mistenkt i saken –og den mistenkte kjemper for å holde familien sammen.", + "tagline": "Antagelser er nytteløse.", + "language": "no", + "country": "no" + }, + { + "title": "Uznany za niewinnego", + "overview": "Szokujące zabójstwo stawia na nogi prokuraturę w Chicago, gdy podejrzana staje się jedna z pracujących tam osób, którą następnie czeka walka o rodzinę.", + "tagline": "Nie zakładaj niczego z góry.", + "language": "pl", + "country": "pl" + }, + { + "title": "Presumível Inocente", + "overview": "Um homicídio horrível deixa o Ministério Público de Chicago de pernas para o ar, quando um dos seus membros é suspeito do crime, fazendo com que o acusado tenha de lutar para manter a sua família unida.", + "tagline": "Presumir é um erro.", + "language": "pt", + "country": "pt" + }, + { + "title": "Acima de Qualquer Suspeita", + "overview": "Um assassinato hediondo deixa o escritório da Promotoria de Justiça de Chicago de cabeça para baixo quando um dos funcionários é suspeito do crime, o que deixa o acusado lutando para manter sua família unida.", + "tagline": "Presumir é um erro.", + "language": "pt", + "country": "br" + }, + { + "title": "Presupus nevinovat", + "overview": null, + "tagline": null, + "language": "ro", + "country": "ro" + }, + { + "title": "Презумпция невиновности", + "overview": "Ужасное убийство переворачивает с ног на голову прокуратуру Чикаго, когда становится известно, что в преступлении подозревается один из сотрудников. В результате обвиняемый борется за то, чтобы сохранить семью.", + "tagline": "Всегда требуйте доказательств.", + "language": "ru", + "country": "ru" + }, + { + "title": "Prezumpcia neviny", + "overview": "Príšerná vražda otrasie prokuratúrou v Chicagu, keď sa z jedného zo zamestnancov vykľuje podozrivý, ktorý zrazu musí bojovať, aby udržal vlastnú rodinu pokope.", + "tagline": "Nemyslite si, že niečo viete.", + "language": "sk", + "country": "sk" + }, + { + "title": null, + "overview": "Rusty Sabich je namestnik generalnega državnega tožilca v Chicagu čigar življenje se obrne na glavo, ko je obtožen umora sodelavke, ki je bila njegova ljubica. Med preiskavo se začnejo razkrivati skrivnosti iz njegovega osebnega in poklicnega življenja, ki razkrivajo zapleteno mrežo izdajstva, skritih želja in politične manipulacije. Nazorno so prikazane moralne dileme, s katerimi se sooča Sabich, ko se trudi ne le dokazati svojo nedolžnost, temveč tudi ohraniti svoj zakon in povezanost družine.", + "tagline": "Ničesar ne domnevajte.", + "language": "sl", + "country": "si" + }, + { + "title": "Претпоставља се да је невин", + "overview": "Ужасно убиство поремети Тужилаштво у Чикагу када је неко од њих осумњичен за злочин – остављајући оптуженог да се бори да одржи породицу на окупу.", + "tagline": null, + "language": "sr", + "country": "rs" + }, + { + "title": null, + "overview": "Ett hemskt mord vänder upp och ner på Chicagos åklagarmyndighet när en av deras egna misstänks för brottet, vilket tvingar den anklagade att kämpa för att hålla ihop sin familj.", + "tagline": "Inget är som det verkar.", + "language": "sv", + "country": "se" + }, + { + "title": null, + "overview": "การฆาตกรรมสุดสยองทำให้สำนักงานอัยการชิคาโกต้องตกตะลึง เมื่อหนึ่งในเจ้าหน้าที่เป็นผู้ต้องสงสัยในอาชญากรรมครั้งนี้ ทำให้ผู้ที่ถูกกล่าวหาต้องต่อสู้เพื่อทำให้ครอบครัวของเขาได้อยู่พร้อมหน้ากัน", + "tagline": "อย่าเชื่อทุกสิ่งเพราะความจริงอาจพลิกผัน", + "language": "th", + "country": "th" + }, + { + "title": null, + "overview": "Korkunç bir cinayet, kendi aralarından biri şüpheli hâline gelmesi ile Şikago Savcılığı’nı altüst eder. Sanığın ise ailesini bir arada tutmak için mücadele etmesi gerekir.", + "tagline": null, + "language": "tr", + "country": "tr" + }, + { + "title": "Презумпція невинуватості", + "overview": "Жахливе вбивство сколихує прокуратуру Чикаго, коли її співробітника підозрюють у скоєнні злочину, а сам обвинувачений змушений боротися за збереження своєї сім’ї.", + "tagline": "Жодних припущень.", + "language": "uk", + "country": "ua" + }, + { + "title": "Suy Đoán Vô Tội - Presumed Innocent", + "overview": "Một vụ án mạng kinh hoàng làm rúng động Phòng Công tố Chicago khi một nhân viên bị tình nghi là thủ phạm, khiến cho bị cáo phải đấu tranh để níu giữ gia đình mình.", + "tagline": "Kết quả khó lường.", + "language": "vi", + "country": "vn" + }, + { + "title": null, + "overview": "一桩骇人听闻的谋杀案闹得芝加哥检察官办公室天翻地覆,因为该办公室的一名成员成了这起犯罪的嫌疑人,使其不得不为维系家庭而战。\n\n该剧改编自斯考特·杜罗所著同名小说,讲述芝加哥检察官办公室发生了一起可怕的谋杀案后,检察官之一拉斯蒂·萨比奇(吉伦哈尔)成了犯罪嫌疑人。在被告为维持家庭和婚姻而奋斗的过程中,探索痴迷、性、政治以及爱情的力量和局限性。\n\n内伽饰演拉斯蒂妻子芭芭拉,是名艺术家、画廊老板和母亲,她的生活在丈夫被指控谋杀他的情妇后被颠覆了,她在处理受伤的心和破碎的婚姻时也为家人而战,并与丈夫广为人知的审判作斗争。", + "tagline": null, + "language": "zh", + "country": "cn" + }, + { + "title": "無罪的罪人", + "overview": "一宗可怕的謀殺案使芝加哥檢察官辦公室陷入混亂,因為疑犯是其中一員,被告也不得不為保護家人而奮戰。", + "tagline": "斷不能武斷。", + "language": "zh", + "country": "hk" + }, + { + "title": null, + "overview": "一桩骇人听闻的谋杀案闹得芝加哥检察官办公室天翻地覆,因为该办公室的一名成员成了这起犯罪的嫌疑人,使其不得不为维系家庭而战。", + "tagline": null, + "language": "zh", + "country": "sg" + }, + { + "title": "無罪的罪人", + "overview": "一場可怕的謀殺案顛覆了芝加哥檢察官辦公室,因為其中一名檢察官涉嫌犯罪,而被告只好奮力將家人維繫在一起。", + "tagline": "勿妄下定論。", + "language": "zh", + "country": "tw" + } ] diff --git a/Tests/TraktKitTests/Models/Shows/test_get_updated_shows_ids.json b/Tests/TraktKitTests/Models/Shows/test_get_updated_shows_ids.json new file mode 100644 index 0000000..c318b3b --- /dev/null +++ b/Tests/TraktKitTests/Models/Shows/test_get_updated_shows_ids.json @@ -0,0 +1,102 @@ +[ + 163302, + 141682, + 75637, + 137146, + 94613, + 42226, + 67906, + 81603, + 23127, + 180078, + 134744, + 78042, + 99549, + 101654, + 111152, + 76671, + 41255, + 144176, + 67900, + 111213, + 100663, + 75726, + 61260, + 80748, + 129956, + 38193, + 81906, + 106006, + 168241, + 164611, + 157261, + 116479, + 143856, + 147412, + 158323, + 130803, + 158563, + 117446, + 101639, + 69280, + 71290, + 116062, + 144753, + 67830, + 63565, + 69478, + 80053, + 96483, + 78893, + 130908, + 67773, + 116954, + 81406, + 122373, + 38484, + 124061, + 62128, + 79998, + 143775, + 148102, + 80388, + 93710, + 100429, + 77700, + 19623, + 178721, + 65855, + 100845, + 62327, + 98820, + 43377, + 166750, + 116158, + 143322, + 63922, + 103059, + 118896, + 123430, + 180780, + 124129, + 79975, + 104239, + 97100, + 81057, + 94342, + 61706, + 98265, + 69597, + 103925, + 61887, + 78471, + 163918, + 190248, + 126001, + 69694, + 82201, + 66245, + 82028, + 64187, + 123253 +] diff --git a/Tests/TraktKitTests/Models/Sync/test_add_items_to_watchlist.json b/Tests/TraktKitTests/Models/Sync/test_add_items_to_watchlist.json index e61b61a..157b83c 100644 --- a/Tests/TraktKitTests/Models/Sync/test_add_items_to_watchlist.json +++ b/Tests/TraktKitTests/Models/Sync/test_add_items_to_watchlist.json @@ -1,32 +1,30 @@ { - "added" : { - "seasons" : 1, - "movies" : 1, - "shows" : 1, - "episodes" : 2 - }, - "existing" : { - "seasons" : 0, - "movies" : 0, - "shows" : 0, - "episodes" : 0 - }, - "not_found" : { - "seasons" : [ - - ], - "movies" : [ - { - "ids" : { - "imdb" : "tt0000111" - } - } - ], - "shows" : [ - - ], - "episodes" : [ - - ] - } + "added": { + "movies": 1, + "shows": 1, + "seasons": 1, + "episodes": 2 + }, + "existing": { + "movies": 0, + "shows": 0, + "seasons": 0, + "episodes": 0 + }, + "not_found": { + "movies": [ + { + "ids": { + "imdb": "tt0000111" + } + } + ], + "shows": [], + "seasons": [], + "episodes": [] + }, + "list": { + "updated_at": "2022-04-27T21:40:41.000Z", + "item_count": 5 + } } diff --git a/Tests/TraktKitTests/Models/Sync/test_get_last_activity.json b/Tests/TraktKitTests/Models/Sync/test_get_last_activity.json index 9a711ba..dfdb88e 100644 --- a/Tests/TraktKitTests/Models/Sync/test_get_last_activity.json +++ b/Tests/TraktKitTests/Models/Sync/test_get_last_activity.json @@ -1,40 +1,63 @@ { - "movies" : { - "rated_at" : "2014-11-19T18:32:29.000Z", - "watched_at" : "2014-11-19T21:42:41.000Z", - "watchlisted_at" : "2014-11-19T21:42:41.000Z", - "collected_at" : "2014-11-20T06:51:30.000Z", - "paused_at" : "2014-11-20T06:51:30.000Z", - "hidden_at" : "2016-08-20T06:51:30.000Z", - "commented_at" : "2014-11-20T06:51:30.000Z" - }, - "seasons" : { - "watchlisted_at" : "2014-11-20T06:51:30.000Z", - "commented_at" : "2014-11-20T06:51:30.000Z", - "rated_at" : "2014-11-19T19:54:24.000Z", - "hidden_at" : "2016-08-20T06:51:30.000Z" - }, - "comments" : { - "liked_at" : "2014-11-20T03:38:09.000Z" - }, - "lists" : { - "commented_at" : "2014-11-20T06:51:30.000Z", - "updated_at" : "2014-11-20T06:52:18.000Z", - "liked_at" : "2014-11-20T00:36:48.000Z" - }, - "episodes" : { - "watchlisted_at" : "2014-11-20T06:51:30.000Z", - "commented_at" : "2014-11-20T06:51:30.000Z", - "watched_at" : "2014-11-20T06:51:30.000Z", - "rated_at" : "2014-11-20T06:51:30.000Z", - "paused_at" : "2014-11-20T06:51:30.000Z", - "collected_at" : "2014-11-19T22:02:41.000Z" - }, - "all" : "2014-11-20T07:01:32.000Z", - "shows" : { - "watchlisted_at" : "2014-11-20T06:51:30.000Z", - "commented_at" : "2014-11-20T06:51:30.000Z", - "rated_at" : "2014-11-19T19:50:58.000Z", - "hidden_at" : "2016-08-20T06:51:30.000Z" - } + "all": "2014-11-20T07:01:32.000Z", + "movies": { + "watched_at": "2014-11-19T21:42:41.000Z", + "collected_at": "2014-11-20T06:51:30.000Z", + "rated_at": "2014-11-19T18:32:29.000Z", + "watchlisted_at": "2014-11-19T21:42:41.000Z", + "favorited_at": "2014-11-19T21:42:41.000Z", + "commented_at": "2014-11-20T06:51:30.000Z", + "paused_at": "2014-11-20T06:51:30.000Z", + "hidden_at": "2016-08-20T06:51:30.000Z" + }, + "episodes": { + "watched_at": "2014-11-20T06:51:30.000Z", + "collected_at": "2014-11-19T22:02:41.000Z", + "rated_at": "2014-11-20T06:51:30.000Z", + "watchlisted_at": "2014-11-20T06:51:30.000Z", + "commented_at": "2014-11-20T06:51:30.000Z", + "paused_at": "2014-11-20T06:51:30.000Z" + }, + "shows": { + "rated_at": "2014-11-19T19:50:58.000Z", + "watchlisted_at": "2014-11-20T06:51:30.000Z", + "favorited_at": "2014-11-20T06:51:30.000Z", + "commented_at": "2014-11-20T06:51:30.000Z", + "hidden_at": "2016-08-20T06:51:30.000Z", + "dropped_at": "2025-03-13T01:23:45.000Z" + }, + "seasons": { + "rated_at": "2014-11-19T19:54:24.000Z", + "watchlisted_at": "2014-11-20T06:51:30.000Z", + "commented_at": "2014-11-20T06:51:30.000Z", + "hidden_at": "2016-08-20T06:51:30.000Z" + }, + "comments": { + "liked_at": "2014-11-20T03:38:09.000Z", + "blocked_at": "2022-02-22T03:38:09.000Z" + }, + "lists": { + "liked_at": "2014-11-20T00:36:48.000Z", + "updated_at": "2014-11-20T06:52:18.000Z", + "commented_at": "2014-11-20T06:51:30.000Z" + }, + "watchlist": { + "updated_at": "2014-11-20T06:52:18.000Z" + }, + "favorites": { + "updated_at": "2014-11-20T06:52:18.000Z" + }, + "account": { + "settings_at": "2020-03-04T03:38:09.000Z", + "followed_at": "2020-03-04T03:38:09.000Z", + "following_at": "2020-03-04T03:38:09.000Z", + "pending_at": "2020-03-04T03:38:09.000Z", + "requested_at": "2022-04-27T03:38:09.000Z" + }, + "saved_filters": { + "updated_at": "2022-06-14T06:52:18.000Z" + }, + "notes": { + "updated_at": "2023-08-31T17:18:19.000Z" + } } diff --git a/Tests/TraktKitTests/Models/Sync/test_get_watchlist.json b/Tests/TraktKitTests/Models/Sync/test_get_watchlist.json index 2569547..c4d75cf 100644 --- a/Tests/TraktKitTests/Models/Sync/test_get_watchlist.json +++ b/Tests/TraktKitTests/Models/Sync/test_get_watchlist.json @@ -1,32 +1,36 @@ [ - { - "rank" : 1, - "type" : "movie", - "movie" : { - "title" : "TRON: Legacy", - "year" : 2010, - "ids" : { - "tmdb" : 20526, - "slug" : "tron-legacy-2010", - "trakt" : 1, - "imdb" : "tt1104001" - } + { + "rank": 1, + "id": 101, + "listed_at": "2014-09-01T09:10:11.000Z", + "notes": "Need to catch up before TRON 3 is out.", + "type": "movie", + "movie": { + "title": "TRON: Legacy", + "year": 2010, + "ids": { + "trakt": 1, + "slug": "tron-legacy-2010", + "imdb": "tt1104001", + "tmdb": 20526 + } + } }, - "listed_at" : "2014-09-01T09:10:11.000Z" - }, - { - "rank" : 2, - "type" : "movie", - "movie" : { - "title" : "The Dark Knight", - "year" : 2008, - "ids" : { - "tmdb" : 155, - "slug" : "the-dark-knight-2008", - "trakt" : 6, - "imdb" : "tt0468569" - } - }, - "listed_at" : "2014-09-01T09:10:11.000Z" - } + { + "rank": 2, + "id": 102, + "listed_at": "2014-09-01T09:10:11.000Z", + "notes": "Really need to check out Heath Ledger's performance in this.", + "type": "movie", + "movie": { + "title": "The Dark Knight", + "year": 2008, + "ids": { + "trakt": 6, + "slug": "the-dark-knight-2008", + "imdb": "tt0468569", + "tmdb": 155 + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Users/test_add_hidden_item.json b/Tests/TraktKitTests/Models/Users/test_add_hidden_item.json index f103332..e51cee2 100644 --- a/Tests/TraktKitTests/Models/Users/test_add_hidden_item.json +++ b/Tests/TraktKitTests/Models/Users/test_add_hidden_item.json @@ -1,23 +1,20 @@ { - "added" : { - "movies" : 1, - "shows" : 2, - "seasons" : 2 - }, - "not_found" : { - "movies" : [ - { - "ids" : { - "imdb" : "tt0000111" - } - } - ], - "shows" : [ - - ], - "seasons" : [ - - ] - } + "added": { + "movies": 1, + "shows": 2, + "seasons": 2, + "users": 0 + }, + "not_found": { + "movies": [ + { + "ids": { + "imdb": "tt0000111" + } + } + ], + "shows": [], + "seasons": [], + "users": [] + } } - diff --git a/Tests/TraktKitTests/Models/Users/test_get_all_list_comments.json b/Tests/TraktKitTests/Models/Users/test_get_all_list_comments.json index 432b222..b4509cf 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_all_list_comments.json +++ b/Tests/TraktKitTests/Models/Users/test_get_all_list_comments.json @@ -1,24 +1,28 @@ [ - { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 8, - "created_at" : "2011-03-25T22:35:17.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" - } - }, - "parent_id" : 0, - "updated_at" : "2011-03-25T22:35:17.000Z", - "comment" : "Can't wait to watch everything on this epic list!", - "spoiler" : false, - "likes" : 0 - } + { + "id": 8, + "parent_id": 0, + "created_at": "2011-03-25T22:35:17.000Z", + "updated_at": "2011-03-25T22:35:17.000Z", + "comment": "Can't wait to watch everything on this epic list!", + "spoiler": false, + "review": false, + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } + } ] diff --git a/Tests/TraktKitTests/Models/Users/test_get_comments_likes.json b/Tests/TraktKitTests/Models/Users/test_get_comments_likes.json index a340806..4817429 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_comments_likes.json +++ b/Tests/TraktKitTests/Models/Users/test_get_comments_likes.json @@ -1,29 +1,32 @@ [ - { - "type" : "comment", - "comment" : { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 190, - "created_at" : "2014-08-04T06:46:01.000Z", - "user" : { - "username" : "sean", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Sean Rudford", - "ids" : { - "slug" : "sean" + { + "liked_at": "2015-03-30T23:18:42.000Z", + "type": "comment", + "comment": { + "id": 190, + "parent_id": 0, + "created_at": "2014-08-04T06:46:01.000Z", + "updated_at": "2014-08-04T06:46:01.000Z", + "comment": "Oh, I wasn't really listening.", + "spoiler": false, + "review": false, + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "sean", + "private": false, + "name": "Sean Rudford", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "sean" + } + } } - }, - "parent_id" : 0, - "updated_at" : "2014-08-04T06:46:01.000Z", - "comment" : "Oh, I wasn't really listening.", - "spoiler" : false, - "likes" : 0 - }, - "liked_at" : "2015-03-30T23:18:42.000Z" - } + } ] - diff --git a/Tests/TraktKitTests/Models/Users/test_get_items_on_custom_list.json b/Tests/TraktKitTests/Models/Users/test_get_items_on_custom_list.json index 8ac9402..63b4202 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_items_on_custom_list.json +++ b/Tests/TraktKitTests/Models/Users/test_get_items_on_custom_list.json @@ -1,99 +1,108 @@ [ - { - "listed_at" : "2014-06-16T06:07:12.000Z", - "movie" : { - "title" : "Star Wars: Episode IV - A New Hope", - "year" : 1977, - "ids" : { - "tmdb" : 11, - "slug" : "star-wars-episode-iv-a-new-hope-1977", - "trakt" : 12, - "imdb" : "tt0076759" - } + { + "rank": 1, + "id": 101, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": "You are part of the rebel alliance and a spy!", + "type": "movie", + "movie": { + "title": "Star Wars: Episode IV - A New Hope", + "year": 1977, + "ids": { + "trakt": 12, + "slug": "star-wars-episode-iv-a-new-hope-1977", + "imdb": "tt0076759", + "tmdb": 11 + } + } }, - "rank" : 1, - "type" : "movie" - }, - { - "listed_at" : "2014-06-16T06:07:12.000Z", - "show" : { - "title" : "The Walking Dead", - "year" : 2010, - "ids" : { - "tmdb" : 1402, - "slug" : "the-walking-dead", - "tvdb" : 153021, - "trakt" : 2, - "imdb" : "tt1520211" - } + { + "rank": 2, + "id": 102, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": null, + "type": "show", + "show": { + "title": "The Walking Dead", + "year": 2010, + "ids": { + "trakt": 2, + "slug": "the-walking-dead", + "tvdb": 153021, + "imdb": "tt1520211", + "tmdb": 1402 + } + } }, - "rank" : 2, - "type" : "show" - }, - { - "listed_at" : "2014-06-16T06:07:12.000Z", - "season" : { - "number" : 1, - "ids" : { - "tvdb" : 30272, - "tmdb" : 3572, - "trakt": 1 - } + { + "rank": 3, + "id": 103, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": null, + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 12345, + "tvdb": 30272, + "tmdb": 3572 + } + }, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } }, - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } + { + "rank": 4, + "id": 104, + "listed_at": "2014-06-17T06:52:03.000Z", + "notes": null, + "type": "episode", + "episode": { + "season": 0, + "number": 2, + "title": "Wedding Day", + "ids": { + "trakt": 2, + "tvdb": 3859791, + "imdb": null, + "tmdb": 62133 + } + }, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } }, - "rank" : 3, - "type" : "season" - }, - { - "listed_at" : "2014-06-17T06:52:03.000Z", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } - }, - "rank" : 4, - "type" : "episode", - "episode" : { - "number" : 2, - "season" : 0, - "title" : "Wedding Day", - "ids" : { - "tmdb" : 62133, - "tvdb" : 3859791, - "trakt" : 2, - "imdb" : null - } - } - }, - { - "listed_at" : "2014-06-17T06:52:03.000Z", - "rank" : 5, - "type" : "person", - "person" : { - "name" : "Garrett Hedlund", - "ids" : { - "tmdb" : 9828, - "slug" : "garrett-hedlund", - "trakt" : 1, - "imdb" : "nm1330560" - } + { + "rank": 5, + "id": 105, + "listed_at": "2014-06-17T06:52:03.000Z", + "notes": null, + "type": "person", + "person": { + "name": "Garrett Hedlund", + "ids": { + "trakt": 1, + "slug": "garrett-hedlund", + "imdb": "nm1330560", + "tmdb": 9828 + } + } } - } ] - diff --git a/Tests/TraktKitTests/Models/Users/test_get_saved_filters.json b/Tests/TraktKitTests/Models/Users/test_get_saved_filters.json new file mode 100644 index 0000000..01eaf71 --- /dev/null +++ b/Tests/TraktKitTests/Models/Users/test_get_saved_filters.json @@ -0,0 +1,38 @@ +[ + { + "rank": 1, + "id": 101, + "section": "movies", + "name": "Movies: IMDB + TMDB ratings", + "path": "/movies/favorited/weekly", + "query": "imdb_ratings=6.9-10.0&tmdb_ratings=4.2-10.0", + "updated_at": "2022-06-15T11:15:06.000Z" + }, + { + "rank": 2, + "id": 102, + "section": "shows", + "name": "Shows: US + Disney+", + "path": "/shows/popular", + "query": "watchnow=disney_plus&countries=us", + "updated_at": "2022-06-15T12:15:06.000Z" + }, + { + "rank": 3, + "id": 103, + "section": "calendars", + "name": "On Netflix", + "path": "/calendars/my/shows", + "query": "network_ids=53", + "updated_at": "2022-06-15T13:15:06.000Z" + }, + { + "rank": 4, + "id": 104, + "section": "search", + "name": "Action & Adventure", + "path": "/search/movie,show", + "query": "genres=+action,+adventure", + "updated_at": "2022-06-15T14:15:06.000Z" + } +] diff --git a/Tests/TraktKitTests/Models/Users/test_get_settings.json b/Tests/TraktKitTests/Models/Users/test_get_settings.json index 45a4330..6d945fe 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_settings.json +++ b/Tests/TraktKitTests/Models/Users/test_get_settings.json @@ -1,42 +1,69 @@ { - "connections" : { - "twitter" : true, - "facebook" : true, - "google" : true, - "tumblr" : false, - "medium" : false, - "slack" : false - }, - "account" : { - "timezone" : "America\/Los_Angeles", - "cover_image" : "https:\/\/walter.trakt.tv\/images\/movies\/000\/001\/545\/fanarts\/original\/0abb604492.jpg", - "time_24hr" : false - }, - "sharing_text" : { - "watched" : "I just watched [item]", - "watching" : "I'm watching [item]" - }, - "user" : { - "private" : false, - "ids" : { - "slug" : "justin" + "user": { + "username": "justin", + "private": false, + "name": "Justin Nemeth", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin", + "uuid": "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c" + }, + "joined_at": "2010-09-25T17:49:25.000Z", + "location": "San Diego, CA", + "about": "Co-founder of trakt.", + "gender": "male", + "age": 32, + "images": { + "avatar": { + "full": "https://secure.gravatar.com/avatar/30c2f0dfbc39e48656f40498aa871e33?r=pg&s=256" + } + }, + "vip_og": true, + "vip_years": 5 }, - "vip" : true, - "joined_at" : "2010-09-25T17:49:25.000Z", - "age" : 32, - "vip_og" : true, - "vip_years" : 5, - "about" : "Co-founder of trakt.", - "vip_ep" : false, - "location" : "San Diego, CA", - "username" : "justin", - "images" : { - "avatar" : { - "full" : "https:\/\/secure.gravatar.com\/avatar\/30c2f0dfbc39e48656f40498aa871e33?r=pg&s=256" - } + "account": { + "timezone": "America/Los_Angeles", + "date_format": "mdy", + "time_24hr": false, + "cover_image": "https://walter-r2.trakt.tv/images/movies/000/001/545/fanarts/original/0abb604492.jpg" }, - "name" : "Justin Nemeth", - "gender" : "male" - } + "connections": { + "facebook": false, + "twitter": true, + "mastodon": true, + "google": true, + "tumblr": false, + "medium": false, + "slack": false, + "apple": false, + "dropbox": false, + "microsoft": false + }, + "sharing_text": { + "watching": "I'm watching [item]", + "watched": "I just watched [item]", + "rated": "[item] [stars]" + }, + "limits": { + "list": { + "count": 2, + "item_count": 100 + }, + "watchlist": { + "item_count": 100 + }, + "favorites": { + "item_count": 100 + }, + "search": { + "recent_count": 5 + }, + "collection": { + "item_count": 100 + }, + "notes": { + "item_count": 100 + } + } } - diff --git a/Tests/TraktKitTests/Models/Users/test_get_user_comments.json b/Tests/TraktKitTests/Models/Users/test_get_user_comments.json index bb71530..464497c 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_user_comments.json +++ b/Tests/TraktKitTests/Models/Users/test_get_user_comments.json @@ -1,204 +1,225 @@ [ - { - "type" : "movie", - "movie" : { - "title" : "Batman Begins", - "year" : 2005, - "ids" : { - "tmdb" : 272, - "slug" : "batman-begins-2005", - "trakt" : 1, - "imdb" : "tt0372784" - } - }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 267, - "created_at" : "2015-04-25T00:14:57.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "movie", + "movie": { + "title": "Batman Begins", + "year": 2005, + "ids": { + "trakt": 1, + "slug": "batman-begins-2005", + "imdb": "tt0372784", + "tmdb": 272 + } + }, + "comment": { + "id": 267, + "comment": "Great kickoff to a new Batman trilogy!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-25T00:14:57.000Z", + "updated_at": "2015-04-25T00:14:57.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Great kickoff to a new Batman trilogy!", - "updated_at" : "2015-04-25T00:14:57.000Z", - "likes" : 0 - } - }, - { - "type" : "show", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } }, - "comment" : { - "review" : false, - "user_rating" : 10, - "replies" : 0, - "id" : 199, - "created_at" : "2015-02-18T06:02:30.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "show", + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + }, + "comment": { + "id": 199, + "comment": "Skyler, I AM THE DANGER.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-02-18T06:02:30.000Z", + "updated_at": "2015-02-18T06:02:30.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 10, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Skyler, I AM THE DANGER.", - "updated_at" : "2015-02-18T06:02:30.000Z", - "likes" : 0 - } - }, - { - "season" : { - "number" : 1, - "ids" : { - "trakt" : 3958, - "tvdb" : 274431, - "tmdb" : 60394 - } - }, - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "season", - "comment" : { - "review" : false, - "user_rating" : 8, - "replies" : 0, - "id" : 220, - "created_at" : "2015-04-21T06:53:25.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 3958, + "tvdb": 274431, + "tmdb": 60394 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 220, + "comment": "Kicking off season 1 for a new Batman show.", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T06:53:25.000Z", + "updated_at": "2015-04-21T06:53:25.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": 8, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Kicking off season 1 for a new Batman show.", - "updated_at" : "2015-04-21T06:53:25.000Z", - "likes" : 0 - } - }, - { - "show" : { - "title" : "Gotham", - "year" : 2014, - "ids" : { - "tmdb" : 60708, - "slug" : "gotham", - "tvdb" : 274431, - "trakt" : 869, - "imdb" : "tt3749900" - } }, - "type" : "episode", - "comment" : { - "review" : false, - "user_rating" : 7, - "replies" : 1, - "id" : 229, - "created_at" : "2015-04-21T15:42:31.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "episode", + "episode": { + "season": 1, + "number": 1, + "title": "Jim Gordon", + "ids": { + "trakt": 63958, + "tvdb": 4768720, + "imdb": "tt3216414", + "tmdb": 975968 + } + }, + "show": { + "title": "Gotham", + "year": 2014, + "ids": { + "trakt": 869, + "slug": "gotham", + "tvdb": 274431, + "imdb": "tt3749900", + "tmdb": 60708 + } + }, + "comment": { + "id": 229, + "comment": "Is this the OC?", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2015-04-21T15:42:31.000Z", + "updated_at": "2015-04-21T15:42:31.000Z", + "replies": 1, + "likes": 0, + "user_stats": { + "rating": 7, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "Is this the OC?", - "updated_at" : "2015-04-21T15:42:31.000Z", - "likes" : 0 }, - "episode" : { - "number" : 1, - "season" : 1, - "title" : "Jim Gordon", - "ids" : { - "tmdb" : 975968, - "tvdb" : 4768720, - "trakt" : 63958, - "imdb" : "tt3216414" - } - } - }, - { - "type" : "list", - "comment" : { - "review" : false, - "user_rating" : null, - "replies" : 0, - "id" : 268, - "created_at" : "2014-12-08T17:34:51.000Z", - "user" : { - "username" : "justin", - "private" : false, - "vip" : true, - "vip_ep" : false, - "name" : "Justin N.", - "ids" : { - "slug" : "justin" + { + "type": "list", + "list": { + "name": "Star Wars", + "description": "The complete Star Wars saga!", + "privacy": "public", + "share_link": "https://trakt.tv/lists/51", + "display_numbers": false, + "allow_comments": true, + "updated_at": "2015-04-22T22:01:39.000Z", + "item_count": 8, + "comment_count": 0, + "likes": 0, + "ids": { + "trakt": 51, + "slug": "star-wars" + } + }, + "comment": { + "id": 268, + "comment": "May the 4th be with you!", + "spoiler": false, + "review": false, + "parent_id": 0, + "created_at": "2014-12-08T17:34:51.000Z", + "updated_at": "2014-12-08T17:34:51.000Z", + "replies": 0, + "likes": 0, + "user_stats": { + "rating": null, + "play_count": 1, + "completed_count": 1 + }, + "user": { + "username": "justin", + "private": false, + "name": "Justin N.", + "vip": true, + "vip_ep": false, + "ids": { + "slug": "justin" + } + } } - }, - "parent_id" : 0, - "spoiler" : false, - "comment" : "May the 4th be with you!", - "updated_at" : "2014-12-08T17:34:51.000Z", - "likes" : 0 - }, - "list" : { - "ids" : { - "trakt" : 51, - "slug" : "star-wars" - }, - "display_numbers" : false, - "privacy" : "public", - "allow_comments" : true, - "comment_count" : 0, - "item_count" : 8, - "description" : "The complete Star Wars saga!", - "updated_at" : "2015-04-22T22:01:39.000Z", - "name" : "Star Wars", - "likes" : 0 } - } ] diff --git a/Tests/TraktKitTests/Models/Users/test_get_user_watched_movies.json b/Tests/TraktKitTests/Models/Users/test_get_user_watched_movies.json index d77b51c..f7dbd67 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_user_watched_movies.json +++ b/Tests/TraktKitTests/Models/Users/test_get_user_watched_movies.json @@ -1,30 +1,32 @@ [ - { - "plays" : 4, - "last_watched_at" : "2014-10-11T17:00:54.000Z", - "movie" : { - "title" : "Batman Begins", - "year" : 2005, - "ids" : { - "tmdb" : 272, - "slug" : "batman-begins-2005", - "trakt" : 6, - "imdb" : "tt0372784" - } + { + "plays": 4, + "last_watched_at": "2014-10-11T17:00:54.000Z", + "last_updated_at": "2014-10-11T17:00:54.000Z", + "movie": { + "title": "Batman Begins", + "year": 2005, + "ids": { + "trakt": 6, + "slug": "batman-begins-2005", + "imdb": "tt0372784", + "tmdb": 272 + } + } + }, + { + "plays": 2, + "last_watched_at": "2014-10-12T17:00:54.000Z", + "last_updated_at": "2014-10-12T17:00:54.000Z", + "movie": { + "title": "The Dark Knight", + "year": 2008, + "ids": { + "trakt": 4, + "slug": "the-dark-knight-2008", + "imdb": "tt0468569", + "tmdb": 155 + } + } } - }, - { - "plays" : 2, - "last_watched_at" : "2014-10-12T17:00:54.000Z", - "movie" : { - "title" : "The Dark Knight", - "year" : 2008, - "ids" : { - "tmdb" : 155, - "slug" : "the-dark-knight-2008", - "trakt" : 4, - "imdb" : "tt0468569" - } - } - } ] diff --git a/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows.json b/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows.json index ec160b1..b4cf8a4 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows.json +++ b/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows.json @@ -1,97 +1,99 @@ [ - { - "last_watched_at" : "2014-10-11T17:00:54.000Z", - "seasons" : [ - { - "number" : 1, - "episodes" : [ - { - "number" : 1, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - }, - { - "number" : 2, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - } + { + "plays": 56, + "last_watched_at": "2014-10-11T17:00:54.000Z", + "last_updated_at": "2014-10-11T17:00:54.000Z", + "reset_at": null, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + }, + "seasons": [ + { + "number": 1, + "episodes": [ + { + "number": 1, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + }, + { + "number": 2, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + } + ] + }, + { + "number": 2, + "episodes": [ + { + "number": 1, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + }, + { + "number": 2, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + } + ] + } ] - }, - { - "number" : 2, - "episodes" : [ - { - "number" : 1, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - }, - { - "number" : 2, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - } - ] - } - ], - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } }, - "plays" : 56 - }, - { - "last_watched_at" : "2014-10-12T17:00:54.000Z", - "seasons" : [ - { - "number" : 1, - "episodes" : [ - { - "number" : 1, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - }, - { - "number" : 2, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - } - ] - }, - { - "number" : 2, - "episodes" : [ - { - "number" : 1, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - }, - { - "number" : 2, - "plays" : 1, - "last_watched_at" : "2014-10-11T17:00:54.000Z" - } + { + "plays": 23, + "last_watched_at": "2014-10-12T17:00:54.000Z", + "last_updated_at": "2014-10-12T17:00:54.000Z", + "show": { + "title": "Parks and Recreation", + "year": 2009, + "ids": { + "trakt": 4, + "slug": "parks-and-recreation", + "tvdb": 84912, + "imdb": "tt1266020", + "tmdb": 8592 + } + }, + "seasons": [ + { + "number": 1, + "episodes": [ + { + "number": 1, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + }, + { + "number": 2, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + } + ] + }, + { + "number": 2, + "episodes": [ + { + "number": 1, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + }, + { + "number": 2, + "plays": 1, + "last_watched_at": "2014-10-11T17:00:54.000Z" + } + ] + } ] - } - ], - "show" : { - "title" : "Parks and Recreation", - "year" : 2009, - "ids" : { - "tmdb" : 8592, - "slug" : "parks-and-recreation", - "tvdb" : 84912, - "trakt" : 4, - "imdb" : "tt1266020" - } - }, - "plays" : 23 - } + } ] - diff --git a/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows_no_seasons.json b/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows_no_seasons.json index 4b63ce0..e3925a5 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows_no_seasons.json +++ b/Tests/TraktKitTests/Models/Users/test_get_user_watched_shows_no_seasons.json @@ -1,33 +1,36 @@ [ - { - "plays" : 56, - "last_watched_at" : "2014-10-11T17:00:54.000Z", - "show" : { - "title" : "Breaking Bad", - "year" : 2008, - "ids" : { - "tmdb" : 1396, - "slug" : "breaking-bad", - "tvdb" : 81189, - "trakt" : 1, - "imdb" : "tt0903747" - } + { + "plays": 56, + "last_watched_at": "2014-10-11T17:00:54.000Z", + "last_updated_at": "2014-10-11T17:00:54.000Z", + "reset_at": null, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } + }, + { + "plays": 23, + "last_watched_at": "2014-10-12T17:00:54.000Z", + "last_updated_at": "2014-10-12T17:00:54.000Z", + "reset_at": "2019-10-12T17:00:54.000Z", + "show": { + "title": "Parks and Recreation", + "year": 2009, + "ids": { + "trakt": 4, + "slug": "parks-and-recreation", + "tvdb": 84912, + "imdb": "tt1266020", + "tmdb": 8592 + } + } } - }, - { - "plays" : 23, - "last_watched_at" : "2014-10-12T17:00:54.000Z", - "show" : { - "title" : "Parks and Recreation", - "year" : 2009, - "ids" : { - "tmdb" : 8592, - "slug" : "parks-and-recreation", - "tvdb" : 84912, - "trakt" : 4, - "imdb" : "tt1266020" - } - } - } ] - diff --git a/Tests/TraktKitTests/Models/Users/test_get_user_watchlist.json b/Tests/TraktKitTests/Models/Users/test_get_user_watchlist.json index eb82778..b4420b5 100644 --- a/Tests/TraktKitTests/Models/Users/test_get_user_watchlist.json +++ b/Tests/TraktKitTests/Models/Users/test_get_user_watchlist.json @@ -1,33 +1,108 @@ [ - { - "rank" : 1, - "type" : "movie", - "movie" : { - "title" : "TRON: Legacy", - "year" : 2010, - "ids" : { - "tmdb" : 20526, - "slug" : "tron-legacy-2010", - "trakt" : 1, - "imdb" : "tt1104001" - } + { + "rank": 1, + "id": 101, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": "You are part of the rebel alliance and a spy!", + "type": "movie", + "movie": { + "title": "Star Wars: Episode IV - A New Hope", + "year": 1977, + "ids": { + "trakt": 12, + "slug": "star-wars-episode-iv-a-new-hope-1977", + "imdb": "tt0076759", + "tmdb": 11 + } + } }, - "listed_at" : "2014-09-01T09:10:11.000Z" - }, - { - "rank" : 2, - "type" : "movie", - "movie" : { - "title" : "The Dark Knight", - "year" : 2008, - "ids" : { - "tmdb" : 155, - "slug" : "the-dark-knight-2008", - "trakt" : 6, - "imdb" : "tt0468569" - } + { + "rank": 2, + "id": 102, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": null, + "type": "show", + "show": { + "title": "The Walking Dead", + "year": 2010, + "ids": { + "trakt": 2, + "slug": "the-walking-dead", + "tvdb": 153021, + "imdb": "tt1520211", + "tmdb": 1402 + } + } }, - "listed_at" : "2014-09-01T09:10:11.000Z" - } + { + "rank": 3, + "id": 103, + "listed_at": "2014-06-16T06:07:12.000Z", + "notes": null, + "type": "season", + "season": { + "number": 1, + "ids": { + "trakt": 3954, + "tvdb": 30272, + "tmdb": 3572 + } + }, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } + }, + { + "rank": 4, + "id": 104, + "listed_at": "2014-06-17T06:52:03.000Z", + "notes": null, + "type": "episode", + "episode": { + "season": 0, + "number": 2, + "title": "Wedding Day", + "ids": { + "trakt": 2, + "tvdb": 3859791, + "imdb": null, + "tmdb": 62133 + } + }, + "show": { + "title": "Breaking Bad", + "year": 2008, + "ids": { + "trakt": 1, + "slug": "breaking-bad", + "tvdb": 81189, + "imdb": "tt0903747", + "tmdb": 1396 + } + } + }, + { + "rank": 5, + "id": 105, + "listed_at": "2014-06-17T06:52:03.000Z", + "notes": null, + "type": "person", + "person": { + "name": "Garrett Hedlund", + "ids": { + "trakt": 1, + "slug": "garrett-hedlund", + "imdb": "nm1330560", + "tmdb": 9828 + } + } + } ] - diff --git a/Tests/TraktKitTests/Models/Users/test_post_remove_hidden_items.json b/Tests/TraktKitTests/Models/Users/test_post_remove_hidden_items.json index e211437..e627a33 100644 --- a/Tests/TraktKitTests/Models/Users/test_post_remove_hidden_items.json +++ b/Tests/TraktKitTests/Models/Users/test_post_remove_hidden_items.json @@ -1,23 +1,21 @@ -{ - "deleted" : { - "movies" : 1, - "shows" : 2, - "seasons" : 2 - }, - "not_found" : { - "movies" : [ - { - "ids" : { - "imdb" : "tt0000111" - } - } - ], - "shows" : [ - - ], - "seasons" : [ - ] - } +{ + "deleted": { + "movies": 1, + "shows": 2, + "seasons": 2, + "users": 0 + }, + "not_found": { + "movies": [ + { + "ids": { + "imdb": "tt0000111" + } + } + ], + "shows": [], + "seasons": [], + "users": [] + } } - diff --git a/Tests/TraktKitTests/MovieTests+Async.swift b/Tests/TraktKitTests/MovieTests+Async.swift new file mode 100644 index 0000000..8dd4946 --- /dev/null +++ b/Tests/TraktKitTests/MovieTests+Async.swift @@ -0,0 +1,105 @@ +// +// MovieTests+Async.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/22/25. +// + +import Testing + +extension TraktTestSuite { + @Suite(.serialized) + struct MovieTestSuite { + @Test func getTrendingMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/trending?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_trending_movies")), headers: [.page(1), .pageCount(10)]) + + let result = try await traktManager.movies + .trending() + .extend(.Min) + .page(1) + .limit(10) + .perform() + let (movies, currentPage, pageCount) = (result.object, result.currentPage, result.pageCount) + #expect(movies.count == 2) + #expect(currentPage == 1) + #expect(pageCount == 10) + } + + @Test func getPopularMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/popular?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_popular_movies"))) + + let result = try await traktManager.movies + .popular() + .extend(.Min) + .page(1) + .limit(10) + .perform() + let (movies, _, _) = (result.object, result.currentPage, result.pageCount) + #expect(movies.count == 10) + } + + @Test func getMostPlayedMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/played/all?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_played_movies"))) + + let result = try await traktManager.movies + .played(period: .all) + .extend(.Min) + .page(1) + .limit(10) + .perform() + let (movies, _, _) = (result.object, result.currentPage, result.pageCount) + #expect(movies.count == 10) + } + + @Test func getMostWatchedMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/watched/all?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_watched_movies"))) + + let result = try await traktManager.movies + .watched(period: .all) + .extend(.Min) + .page(1) + .limit(10) + .perform() + let (movies, _, _) = (result.object, result.currentPage, result.pageCount) + #expect(movies.count == 10) + } + + @Test func getPeopleInMovie() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/iron-man-2008/people?extended=min", result: .success(jsonData(named: "test_get_cast_and_crew"))) + + let castAndCrew = try await traktManager.movie(id: "iron-man-2008") + .people() + .extend(.Min) + .perform() + + #expect(castAndCrew.cast?.count == 65) + #expect(castAndCrew.crew?.count == 118) + #expect(castAndCrew.editors?.count == 4) + #expect(castAndCrew.producers?.count == 19) + #expect(castAndCrew.camera?.count == 3) + #expect(castAndCrew.art?.count == 11) + #expect(castAndCrew.sound?.count == 12) + #expect(castAndCrew.costume?.count == 2) + #expect(castAndCrew.writers?.count == 8) + #expect(castAndCrew.visualEffects?.count == 7) + #expect(castAndCrew.directors?.count == 6) + #expect(castAndCrew.lighting?.count == 2) + } + + @Test func getMovieStudios() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/studios", result: .success(jsonData(named: "test_get_movie_studios"))) + + let studios = try await traktManager.movie(id: "tron-legacy-2010") + .studios() + .perform() + + #expect(studios.count == 5) + } + } +} diff --git a/Tests/TraktKitTests/MovieTests.swift b/Tests/TraktKitTests/MovieTests.swift index 5d29e29..430fee2 100644 --- a/Tests/TraktKitTests/MovieTests.swift +++ b/Tests/TraktKitTests/MovieTests.swift @@ -10,22 +10,11 @@ import XCTest import Foundation @testable import TraktKit -class MovieTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - +final class MovieTests: TraktTestCase { // MARK: - Trending - func test_get_trending_movies() { - session.nextData = jsonData(named: "test_get_trending_movies") + func test_get_trending_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/trending?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_trending_movies"))) let expectation = XCTestExpectation(description: "Get Trending Movies") traktManager.getTrendingMovies(pagination: Pagination(page: 1, limit: 10)) { result in @@ -35,10 +24,6 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/trending") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) switch result { case .timedOut: @@ -50,8 +35,8 @@ class MovieTests: XCTestCase { // MARK: - Popular - func test_get_popular_movies() { - session.nextData = jsonData(named: "test_get_popular_movies") + func test_get_popular_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/popular?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_popular_movies"))) let expectation = XCTestExpectation(description: "Get Popular Movies") traktManager.getPopularMovies(pagination: Pagination(page: 1, limit: 10)) { result in @@ -61,10 +46,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/popular") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -75,21 +57,18 @@ class MovieTests: XCTestCase { // MARK: - Played - func test_get_most_played_movies() { - session.nextData = jsonData(named: "test_get_most_played_movies") + func test_get_most_played_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/played/all?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_played_movies"))) let expectation = XCTestExpectation(description: "Get Most Played Movies") - traktManager.getPlayedMovies(period: .All, pagination: Pagination(page: 1, limit: 10)) { result in + traktManager.getPlayedMovies(period: .all, pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let playedMovies, _, _) = result { XCTAssertEqual(playedMovies.count, 10) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/played/all") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -100,21 +79,18 @@ class MovieTests: XCTestCase { // MARK: - Watched - func test_get_most_watched_movies() { - session.nextData = jsonData(named: "test_get_most_watched_movies") + func test_get_most_watched_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/watched/all?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_watched_movies"))) let expectation = XCTestExpectation(description: "Get Most Watched Movies") - traktManager.getWatchedMovies(period: .All, pagination: Pagination(page: 1, limit: 10)) { result in + traktManager.getWatchedMovies(period: .all, pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let watchedMovies, _, _) = result { XCTAssertEqual(watchedMovies.count, 10) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/watched/all") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -125,21 +101,18 @@ class MovieTests: XCTestCase { // MARK: - Collected - func test_get_most_collected_movies() { - session.nextData = jsonData(named: "test_get_most_collected_movies") + func test_get_most_collected_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/collected/all?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_collected_movies"))) let expectation = XCTestExpectation(description: "Get Most Collected Movies") - traktManager.getCollectedMovies(period: .All, pagination: Pagination(page: 1, limit: 10)) { result in + traktManager.getCollectedMovies(period: .all, pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let collectedMovies, _, _) = result { XCTAssertEqual(collectedMovies.count, 10) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/collected/all") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -150,8 +123,8 @@ class MovieTests: XCTestCase { // MARK: - Anticipated - func test_get_most_anticipated_movies() { - session.nextData = jsonData(named: "test_get_most_anticipated_movies") + func test_get_most_anticipated_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/anticipated?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_anticipated_movies"))) let expectation = XCTestExpectation(description: "Get Most Anticipated Movies") traktManager.getAnticipatedMovies(pagination: Pagination(page: 1, limit: 10)) { result in @@ -161,10 +134,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/anticipated") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -175,8 +145,8 @@ class MovieTests: XCTestCase { // MARK: - Box Office - func test_get_weekend_box_office() { - session.nextData = jsonData(named: "test_get_weekend_box_office") + func test_get_weekend_box_office() throws { + try mock(.GET, "https://api.trakt.tv/movies/boxoffice?extended=min", result: .success(jsonData(named: "test_get_weekend_box_office"))) let expectation = XCTestExpectation(description: "Get Weekend Box Office") traktManager.getWeekendBoxOffice { result in @@ -186,8 +156,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/boxoffice?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -198,8 +167,8 @@ class MovieTests: XCTestCase { // MARK: - Updates - func test_get_recently_updated_movies() { - session.nextData = jsonData(named: "test_get_recently_updated_movies") + func test_get_recently_updated_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/updates/2014-01-10?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_recently_updated_movies"))) let expectation = XCTestExpectation(description: "Get recently updated movies") traktManager.getUpdatedMovies(startDate: try? Date.dateFromString("2014-01-10"), pagination: Pagination(page: 1, limit: 10)) { result in @@ -209,10 +178,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/movies/updates/2014-01-10") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -223,8 +189,8 @@ class MovieTests: XCTestCase { // MARK: - Summary - func test_get_min_movie() { - session.nextData = jsonData(named: "Movie_Min") + func test_get_min_movie() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010?extended=min", result: .success(jsonData(named: "Movie_Min"))) let expectation = XCTestExpectation(description: "MovieSummary") traktManager.getMovieSummary(movieID: "tron-legacy-2010") { result in @@ -239,8 +205,7 @@ class MovieTests: XCTestCase { let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -249,8 +214,8 @@ class MovieTests: XCTestCase { } } - func test_get_full_movie() { - session.nextData = jsonData(named: "Movie_Full") + func test_get_full_movie() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010?extended=full", result: .success(jsonData(named: "Movie_Full"))) let expectation = XCTestExpectation(description: "MovieSummary") traktManager.getMovieSummary(movieID: "tron-legacy-2010", extended: [.Full]) { result in @@ -276,8 +241,7 @@ class MovieTests: XCTestCase { let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010?extended=full") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -288,8 +252,8 @@ class MovieTests: XCTestCase { // MARK: - Aliases - func test_get_movie_aliases() { - session.nextData = jsonData(named: "test_get_movie_aliases") + func test_get_movie_aliases() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/aliases", result: .success(jsonData(named: "test_get_movie_aliases"))) let expectation = XCTestExpectation(description: "Get movie aliases") traktManager.getMovieAliases(movieID: "tron-legacy-2010") { result in @@ -299,8 +263,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/aliases") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -311,8 +274,8 @@ class MovieTests: XCTestCase { // MARK: - Releases - func test_get_movie_releases() { - session.nextData = jsonData(named: "test_get_movie_releases") + func test_get_movie_releases() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/releases/us", result: .success(jsonData(named: "test_get_movie_releases"))) let expectation = XCTestExpectation(description: "Get movie releases") traktManager.getMovieReleases(movieID: "tron-legacy-2010", country: "us") { result in @@ -322,8 +285,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/releases/us") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -334,8 +296,8 @@ class MovieTests: XCTestCase { // MARK: - Translations - func test_get_movie_translations() { - session.nextData = jsonData(named: "test_get_movie_translations") + func test_get_movie_translations() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/translations/us", result: .success(jsonData(named: "test_get_movie_translations"))) let expectation = XCTestExpectation(description: "Get movie translations") traktManager.getMovieTranslations(movieID: "tron-legacy-2010", language: "us") { result in @@ -345,8 +307,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/translations/us") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -357,8 +318,8 @@ class MovieTests: XCTestCase { // MARK: - Comments - func test_get_movie_comments() { - session.nextData = jsonData(named: "test_get_movie_comments") + func test_get_movie_comments() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/comments", result: .success(jsonData(named: "test_get_movie_comments"))) let expectation = XCTestExpectation(description: "Get movie comments") traktManager.getMovieComments(movieID: "tron-legacy-2010") { result in @@ -368,8 +329,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/comments") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -380,8 +340,8 @@ class MovieTests: XCTestCase { // MARK: - Lists - func test_get_lists_containing_movie() { - session.nextData = jsonData(named: "test_get_lists_containing_movie") + func test_get_lists_containing_movie() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/lists", result: .success(jsonData(named: "test_get_lists_containing_movie"))) let expectation = XCTestExpectation(description: "Get lists containing movie") traktManager.getListsContainingMovie(movieID: "tron-legacy-2010") { result in @@ -391,8 +351,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/lists") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -403,21 +362,29 @@ class MovieTests: XCTestCase { // MARK: - People - func test_get_cast_and_crew() { - session.nextData = jsonData(named: "test_get_cast_and_crew") + func test_get_cast_and_crew() throws { + try mock(.GET, "https://api.trakt.tv/movies/iron-man-2008/people?extended=min", result: .success(jsonData(named: "test_get_cast_and_crew"))) let expectation = XCTestExpectation(description: "Get movie cast and crew") - traktManager.getPeopleInMovie(movieID: "tron-legacy-2010") { result in + traktManager.getPeopleInMovie(movieID: "iron-man-2008") { result in if case .success(let castAndCrew) = result { + XCTAssertEqual(castAndCrew.cast?.count, 65) + XCTAssertEqual(castAndCrew.crew?.count, 118) + XCTAssertEqual(castAndCrew.editors?.count, 4) + XCTAssertEqual(castAndCrew.producers?.count, 19) + XCTAssertEqual(castAndCrew.camera?.count, 3) + XCTAssertEqual(castAndCrew.art?.count, 11) + XCTAssertEqual(castAndCrew.sound?.count, 12) + XCTAssertEqual(castAndCrew.costume?.count, 2) XCTAssertEqual(castAndCrew.writers?.count, 8) - XCTAssertEqual(castAndCrew.directors?.count, 2) - XCTAssertEqual(castAndCrew.cast?.count, 89) + XCTAssertEqual(castAndCrew.visualEffects?.count, 7) + XCTAssertEqual(castAndCrew.directors?.count, 6) + XCTAssertEqual(castAndCrew.lighting?.count, 2) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/people?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -428,8 +395,8 @@ class MovieTests: XCTestCase { // MARK: - Ratings - func test_get_movie_ratings() { - session.nextData = jsonData(named: "test_get_movie_ratings") + func test_get_movie_ratings() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/ratings", result: .success(jsonData(named: "test_get_movie_ratings"))) let expectation = XCTestExpectation(description: "Get movie ratings") traktManager.getMovieRatings(movieID: "tron-legacy-2010") { result in @@ -439,8 +406,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/ratings") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -451,8 +417,8 @@ class MovieTests: XCTestCase { // MARK: - Related - func test_get_related_movies() { - session.nextData = jsonData(named: "test_get_related_movies") + func test_get_related_movies() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/related?extended=min", result: .success(jsonData(named: "test_get_related_movies"))) let expectation = XCTestExpectation(description: "Get related movies") traktManager.getRelatedMovies(movieID: "tron-legacy-2010") { result in @@ -462,8 +428,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/related?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -474,8 +439,8 @@ class MovieTests: XCTestCase { // MARK: - stats - func test_get_movie_stats() { - session.nextData = jsonData(named: "test_get_movie_stats") + func test_get_movie_stats() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/stats", result: .success(jsonData(named: "test_get_movie_stats"))) let expectation = XCTestExpectation(description: "Get movies stats") traktManager.getMovieStatistics(movieID: "tron-legacy-2010") { result in @@ -490,8 +455,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/stats") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -502,8 +466,8 @@ class MovieTests: XCTestCase { // MARK: - Watching - func test_get_users_watching_movie_now() { - session.nextData = jsonData(named: "test_get_users_watching_movie_now") + func test_get_users_watching_movie_now() throws { + try mock(.GET, "https://api.trakt.tv/movies/tron-legacy-2010/watching", result: .success(jsonData(named: "test_get_users_watching_movie_now"))) let expectation = XCTestExpectation(description: "Get users watching a movie") traktManager.getUsersWatchingMovie(movieID: "tron-legacy-2010") { result in @@ -513,8 +477,7 @@ class MovieTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/movies/tron-legacy-2010/watching") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/NetworkMocking/MockedResponse.swift b/Tests/TraktKitTests/NetworkMocking/MockedResponse.swift new file mode 100644 index 0000000..1ce3d10 --- /dev/null +++ b/Tests/TraktKitTests/NetworkMocking/MockedResponse.swift @@ -0,0 +1,36 @@ +// +// MockedResponse.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/16/25. +// + +import Foundation +@testable import TraktKit + +extension RequestMocking { + struct MockedResponse { + let url: URL + let result: Result + let httpCode: Int + let headers: [String: String] + let loadingTime: TimeInterval + let customResponse: URLResponse? + } +} + +extension RequestMocking.MockedResponse { + enum Error: Swift.Error { + case failedMockCreation + } + + init(urlString: String, result: Result, httpCode: Int = 200, headers: [HTTPHeader] = [.contentType, .apiVersion, .apiKey("")], loadingTime: TimeInterval = .zero) throws { + guard let url = URL(string: urlString) else { throw Error.failedMockCreation } + self.url = url + self.result = result + self.httpCode = httpCode + self.headers = Dictionary(headers.map { ($0.key, $0.value) }) { _, last in last } + self.loadingTime = loadingTime + self.customResponse = nil + } +} diff --git a/Tests/TraktKitTests/NetworkMocking/RequestMocking.swift b/Tests/TraktKitTests/NetworkMocking/RequestMocking.swift new file mode 100644 index 0000000..f0dcdc6 --- /dev/null +++ b/Tests/TraktKitTests/NetworkMocking/RequestMocking.swift @@ -0,0 +1,121 @@ +// +// RequestMocking.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/16/25. +// +import Foundation +import os +@testable import TraktKit + +extension URLSession { + static var mockedResponsesOnly: URLSession { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [RequestMocking.self, RequestBlocking.self] + configuration.timeoutIntervalForRequest = 2 + configuration.timeoutIntervalForResource = 2 + return URLSession(configuration: configuration) + } +} + +extension RequestMocking { + private final class MocksContainer: @unchecked Sendable { + var mocks: [MockedResponse] = [] + } + + static private let container = MocksContainer() + static private let lock = NSLock() + + static func add(mock: MockedResponse) { + lock.withLock { + container.mocks.append(mock) + } + } + + static func replace(mock: MockedResponse) { + lock.withLock { + container.mocks.removeAll(where: { $0.url == mock.url }) + container.mocks.append(mock) + } + } + + static func removeAllMocks() { + lock.withLock { + container.mocks.removeAll() + } + } + + static private func mock(for request: URLRequest) -> MockedResponse? { + return lock.withLock { + container.mocks.first { mock in + guard let url = request.url else { return false } + return mock.url.compareComponents(url) + } + } + } +} + +// MARK: - RequestMocking + +final class RequestMocking: URLProtocol, @unchecked Sendable { + override class func canInit(with request: URLRequest) -> Bool { + return mock(for: request) != nil + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool { + return false + } + + override func startLoading() { + guard + let mock = RequestMocking.mock(for: request), + let url = request.url, + let response = mock.customResponse ?? + HTTPURLResponse(url: url, statusCode: mock.httpCode, httpVersion: "HTTP/1.1", headerFields: mock.headers) + else { return } + + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + + switch mock.result { + case let .success(data): + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + case let .failure(error): + client?.urlProtocol(self, didFailWithError: error) + } + } + + + override func stopLoading() { } +} + +// MARK: - RequestBlocking + +/// Block all outgoing requests not caught by `RequestMocking` protocol +private class RequestBlocking: URLProtocol, @unchecked Sendable { + + static let logger = Logger(subsystem: "TraktKit", category: "RequestBlocking") + + enum Error: Swift.Error { + case requestBlocked + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + Self.logger.warning("Blocking request to \(self.request.url?.absoluteString ?? "Unknown URL.")") + self.client?.urlProtocol(self, didFailWithError: Error.requestBlocked) + } + + override func stopLoading() { } +} diff --git a/Tests/TraktKitTests/OAuthTests.swift b/Tests/TraktKitTests/OAuthTests.swift new file mode 100644 index 0000000..923cef1 --- /dev/null +++ b/Tests/TraktKitTests/OAuthTests.swift @@ -0,0 +1,183 @@ +// +// OAuthTests.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/5/25. +// + +import Foundation +import Testing +@testable import TraktKit + +@Suite(.serialized) +struct OAuthTests { + + @Test func decodeOAuthTokenBody() throws { + let json: [String: String] = [ + "code": "fd0847dbb559752d932dd3c1ac34ff98d27b11fe2fea5a864f44740cd7919ad0", + "client_id": "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f", + "client_secret": "d6ea27703957b69939b8104ed4524595e210cd2e79af587744a7eb6e58f5b3d2", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "grant_type": "authorization_code" + ] + let data = try JSONSerialization.data(withJSONObject: json) + + let oauthBody = try JSONDecoder().decode(OAuthBody.self, from: data) + #expect(oauthBody.code == "fd0847dbb559752d932dd3c1ac34ff98d27b11fe2fea5a864f44740cd7919ad0") + #expect(oauthBody.refreshToken == nil) + #expect(oauthBody.clientId == "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f") + #expect(oauthBody.clientSecret == "d6ea27703957b69939b8104ed4524595e210cd2e79af587744a7eb6e58f5b3d2") + #expect(oauthBody.redirectURI == "urn:ietf:wg:oauth:2.0:oob") + #expect(oauthBody.grantType == "authorization_code") + + // To ensure `nil` keys are not encoded as empty or `null` + let backToData = try JSONEncoder().encode(oauthBody) + let backToJSON = try #require(JSONSerialization.jsonObject(with: backToData) as? [String: String]) + #expect(backToJSON == json) + } + + @Test func decodeRefreshTokenBody() throws { + let json: [String: String] = [ + "refresh_token": "fd0847dbb559752d932dd3c1ac34ff98d27b11fe2fea5a864f44740cd7919ad0", + "client_id": "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f", + "client_secret": "d6ea27703957b69939b8104ed4524595e210cd2e79af587744a7eb6e58f5b3d2", + "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "grant_type": "refresh_token" + ] + let data = try JSONSerialization.data(withJSONObject: json) + + let oauthBody = try JSONDecoder().decode(OAuthBody.self, from: data) + #expect(oauthBody.code == nil) + #expect(oauthBody.refreshToken == "fd0847dbb559752d932dd3c1ac34ff98d27b11fe2fea5a864f44740cd7919ad0") + #expect(oauthBody.clientId == "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f") + #expect(oauthBody.clientSecret == "d6ea27703957b69939b8104ed4524595e210cd2e79af587744a7eb6e58f5b3d2") + #expect(oauthBody.redirectURI == "urn:ietf:wg:oauth:2.0:oob") + #expect(oauthBody.grantType == "refresh_token") + + // To ensure `nil` keys are not encoded as empty or `null` + let backToData = try JSONEncoder().encode(oauthBody) + let backToJSON = try #require(JSONSerialization.jsonObject(with: backToData) as? [String: String]) + #expect(backToJSON == json) + } + + @Test func decodeDeviceTokenBody() throws { + let json: [String: String] = [ + "client_id": "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f" + ] + let data = try JSONSerialization.data(withJSONObject: json) + + let oauthBody = try JSONDecoder().decode(OAuthBody.self, from: data) + #expect(oauthBody.code == nil) + #expect(oauthBody.refreshToken == nil) + #expect(oauthBody.clientId == "9b36d8c0db59eff5038aea7a417d73e69aea75b41aac771816d2ef1b3109cc2f") + #expect(oauthBody.clientSecret == nil) + #expect(oauthBody.redirectURI == nil) + #expect(oauthBody.grantType == nil) + + // To ensure `nil` keys are not encoded as empty or `null` + let backToData = try JSONEncoder().encode(oauthBody) + let backToJSON = try #require(JSONSerialization.jsonObject(with: backToData) as? [String: String]) + #expect(backToJSON == json) + } + + // MARK: - Networking + + @Test func exchangeCodeForAccessCode() async throws { + let json: [String: Any] = [ + "access_token": "dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781", + "token_type": "bearer", + "expires_in": 86400, + "refresh_token": "76ba4c5c75c96f6087f58a4de10be6c00b29ea1ddc3b2022ee2016d1363e3a7c", + "scope": "public", + "created_at": Date.now.timeIntervalSince1970 + ] + let data = try JSONSerialization.data(withJSONObject: json) + try OAuthTests.mock(.POST, "https://api.trakt.tv/oauth/token", result: .success(data), replace: true) + + let authStorage = TraktMockAuthStorage() + + let traktManager = await TraktManager( + session: URLSession.mockedResponsesOnly, + clientId: "", + clientSecret: "", + redirectURI: "", + authStorage: authStorage + ) + await #expect(throws: AuthenticationError.noStoredCredentials, performing: { + try await authStorage.getCurrentState() + }) + + let code = "..." + let authInfo = try await traktManager.getToken(authorizationCode: code) + + #expect(authInfo.accessToken == "dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781") + #expect(authInfo.refreshToken == "76ba4c5c75c96f6087f58a4de10be6c00b29ea1ddc3b2022ee2016d1363e3a7c") + #expect(authInfo.scope == "public") + + let currentAuthState = try await authStorage.getCurrentState() + #expect(currentAuthState.accessToken == authInfo.accessToken) + #expect(traktManager.isSignedIn == true) + } + + @Test func exchangeRefreshTokenForAccessCode() async throws { + let refreshToken = "abcdefgh" + // Setup Trakt Manager with expired token + let authStorage = TraktMockAuthStorage(accessToken: "123456789", refreshToken: refreshToken, expirationDate: .distantPast) + + let traktManager = await TraktManager( + session: URLSession.mockedResponsesOnly, + clientId: "", + clientSecret: "", + redirectURI: "", + authStorage: authStorage + ) + await #expect(throws: AuthenticationError.tokenExpired(refreshToken: refreshToken), performing: { + try await authStorage.getCurrentState() + }) + + // Mock + let json: [String: Any] = [ + "access_token": "dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781", + "token_type": "bearer", + "expires_in": 86400, + "refresh_token": "76ba4c5c75c96f6087f58a4de10be6c00b29ea1ddc3b2022ee2016d1363e3a7c", + "scope": "public", + "created_at": Date.now.timeIntervalSince1970 + ] + let data = try JSONSerialization.data(withJSONObject: json) + try OAuthTests.mock(.POST, "https://api.trakt.tv/oauth/token", result: .success(data), replace: true) + + // Refresh token + try await traktManager.checkToRefresh() + + let currentAuthState = try await authStorage.getCurrentState() + #expect(currentAuthState.accessToken == "dbaf9757982a9e738f05d249b7b5b4a266b3a139049317c4909f2f263572c781") + #expect(traktManager.isSignedIn == true) + } + + @Test func signOut() async throws { + let authStorage = TraktMockAuthStorage(accessToken: "123456789", refreshToken: "abcdefgh", expirationDate: .distantFuture) + + let traktManager = await TraktManager( + session: URLSession.mockedResponsesOnly, + clientId: "", + clientSecret: "", + redirectURI: "", + authStorage: authStorage + ) + #expect(traktManager.isSignedIn == true) + await traktManager.signOut() + #expect(traktManager.isSignedIn == false) + } + + // MARK: - Helper + + static func mock(_ method: TraktKit.Method, _ urlString: String, result: Result, httpCode: Int? = nil, headers: [HTTPHeader] = [.contentType, .apiVersion, .apiKey("")], replace: Bool = false) throws { + let mock = try RequestMocking.MockedResponse(urlString: urlString, result: result, httpCode: httpCode ?? method.expectedResult, headers: headers) + if replace { + RequestMocking.replace(mock: mock) + } else { + RequestMocking.add(mock: mock) + } + } +} diff --git a/Tests/TraktKitTests/PeopleTests.swift b/Tests/TraktKitTests/PeopleTests.swift index cc091d1..6d89d09 100644 --- a/Tests/TraktKitTests/PeopleTests.swift +++ b/Tests/TraktKitTests/PeopleTests.swift @@ -9,22 +9,11 @@ import XCTest @testable import TraktKit -class PeopleTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - +final class PeopleTests: TraktTestCase { // MARK: - Summary - func testParsePersonMin() { - session.nextData = jsonData(named: "Person_Min") + func test_parse_person_min() throws { + try mock(.GET, "https://api.trakt.tv/people/bryan-cranston?extended=min", result: .success(jsonData(named: "Person_Min"))) let expectation = XCTestExpectation(description: "Get minimal details on a person") traktManager.getPersonDetails(personID: "bryan-cranston", extended: [.Min]) { result in @@ -34,7 +23,6 @@ class PeopleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/people/bryan-cranston?extended=min") switch result { case .timedOut: @@ -44,8 +32,8 @@ class PeopleTests: XCTestCase { } } - func testParsePersonFull() { - session.nextData = jsonData(named: "Person_Full") + func test_parse_person_full() throws { + try mock(.GET, "https://api.trakt.tv/people/bryan-cranston?extended=full", result: .success(jsonData(named: "Person_Full"))) let expectation = XCTestExpectation(description: "Get full details on a person") traktManager.getPersonDetails(personID: "bryan-cranston", extended: [.Full]) { result in @@ -55,7 +43,6 @@ class PeopleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/people/bryan-cranston?extended=full") switch result { case .timedOut: @@ -68,8 +55,8 @@ class PeopleTests: XCTestCase { // MARK: - Movies - func test_get_movie_credits() { - session.nextData = jsonData(named: "test_get_movie_credits") + func test_get_movie_credits() throws { + try mock(.GET, "https://api.trakt.tv/people/bryan-cranston/movies?extended=min", result: .success(jsonData(named: "test_get_movie_credits"))) let expectation = XCTestExpectation(description: "Get movie credits for person") traktManager.getMovieCredits(personID: "bryan-cranston", extended: [.Min]) { result in @@ -81,8 +68,7 @@ class PeopleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/people/bryan-cranston/movies?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -93,8 +79,8 @@ class PeopleTests: XCTestCase { // MARK: - Shows - func test_get_show_credits() { - session.nextData = jsonData(named: "test_get_show_credits") + func test_get_show_credits() throws { + try mock(.GET, "https://api.trakt.tv/people/bryan-cranston/shows?extended=min", result: .success(jsonData(named: "test_get_show_credits"))) let expectation = XCTestExpectation(description: "Get show credits for person") traktManager.getShowCredits(personID: "bryan-cranston", extended: [.Min]) { result in @@ -105,8 +91,7 @@ class PeopleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/people/bryan-cranston/shows?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -117,8 +102,8 @@ class PeopleTests: XCTestCase { // MARK: - Lists - func test_get_lists_containing_this_person() { - session.nextData = jsonData(named: "test_get_lists_containing_this_person") + func test_get_lists_containing_this_person() throws { + try mock(.GET, "https://api.trakt.tv/people/bryan-cranston/lists", result: .success(jsonData(named: "test_get_lists_containing_this_person"))) let expectation = XCTestExpectation(description: "Get lists containing person") traktManager.getListsContainingPerson(personId: "bryan-cranston") { result in @@ -128,8 +113,7 @@ class PeopleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/people/bryan-cranston/lists") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/RecommendationsTests.swift b/Tests/TraktKitTests/RecommendationsTests.swift index 65268e2..1d7dc5f 100644 --- a/Tests/TraktKitTests/RecommendationsTests.swift +++ b/Tests/TraktKitTests/RecommendationsTests.swift @@ -9,22 +9,11 @@ import XCTest @testable import TraktKit -class RecommendationsTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - +final class RecommendationsTests: TraktTestCase { // MARK: - Movies - func test_get_movie_recommendations() { - session.nextData = jsonData(named: "test_get_movie_recommendations") + func test_get_movie_recommendations() throws { + try mock(.GET, "https://api.trakt.tv/recommendations/movies", result: .success(jsonData(named: "test_get_movie_recommendations"))) let expectation = XCTestExpectation(description: "Get movie recommendations") traktManager.getRecommendedMovies { result in @@ -34,8 +23,7 @@ class RecommendationsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/recommendations/movies") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -46,8 +34,8 @@ class RecommendationsTests: XCTestCase { // MARK: - Hide Movie - func test_hide_movie_recommendation() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_hide_movie_recommendation() throws { + try mock(.POST, "https://api.trakt.tv/recommendations/movies/922", result: .success(.init())) let expectation = XCTestExpectation(description: "Hide movie recommendation") traktManager.hideRecommendedMovie(movieID: 922) { result in @@ -56,7 +44,6 @@ class RecommendationsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/recommendations/movies/922") switch result { case .timedOut: @@ -68,8 +55,8 @@ class RecommendationsTests: XCTestCase { // MARK: - Shows - func test_get_show_recommendations() { - session.nextData = jsonData(named: "test_get_show_recommendations") + func test_get_show_recommendations() throws { + try mock(.GET, "https://api.trakt.tv/recommendations/shows", result: .success(jsonData(named: "test_get_show_recommendations"))) let expectation = XCTestExpectation(description: "Get show recommendations") traktManager.getRecommendedShows { result in @@ -79,8 +66,7 @@ class RecommendationsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/recommendations/shows") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -91,8 +77,8 @@ class RecommendationsTests: XCTestCase { // MARK: - Hide Show - func test_hide_show_recommendation() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_hide_show_recommendation() throws { + try mock(.POST, "https://api.trakt.tv/recommendations/shows/922", result: .success(.init())) let expectation = XCTestExpectation(description: "Hide show recommendation") traktManager.hideRecommendedShow(showID: 922) { result in @@ -101,7 +87,6 @@ class RecommendationsTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/recommendations/shows/922") switch result { case .timedOut: diff --git a/Tests/TraktKitTests/ScrobbleTests.swift b/Tests/TraktKitTests/ScrobbleTests.swift index 769e005..c7fd8bd 100644 --- a/Tests/TraktKitTests/ScrobbleTests.swift +++ b/Tests/TraktKitTests/ScrobbleTests.swift @@ -9,27 +9,15 @@ import XCTest @testable import TraktKit -class ScrobbleTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } - +final class ScrobbleTests: TraktTestCase { // MARK: - Start - func test_start_watching_in_media_center() { - session.nextData = jsonData(named: "test_start_watching_in_media_center") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_start_watching_in_media_center() throws { + try mock(.POST, "https://api.trakt.tv/scrobble/start", result: .success(jsonData(named: "test_start_watching_in_media_center"))) let expectation = XCTestExpectation(description: "Start watching in media center") let scrobble = TraktScrobble(movie: SyncId(trakt: 12345), progress: 1.25) - try! traktManager.scrobbleStart(scrobble) { result in + try traktManager.scrobbleStart(scrobble) { result in if case .success(let response) = result { XCTAssertEqual(response.action, "start") XCTAssertEqual(response.progress, 1.25) @@ -38,8 +26,7 @@ class ScrobbleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/scrobble/start") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -50,13 +37,12 @@ class ScrobbleTests: XCTestCase { // MARK: - Pause - func test_pause_watching_in_media_center() { - session.nextData = jsonData(named: "test_pause_watching_in_media_center") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_pause_watching_in_media_center() throws { + try mock(.POST, "https://api.trakt.tv/scrobble/pause", result: .success(jsonData(named: "test_pause_watching_in_media_center"))) let expectation = XCTestExpectation(description: "Pause watching in media center") let scrobble = TraktScrobble(movie: SyncId(trakt: 12345), progress: 75) - try! traktManager.scrobblePause(scrobble) { result in + try traktManager.scrobblePause(scrobble) { result in if case .success(let response) = result { XCTAssertEqual(response.action, "pause") XCTAssertEqual(response.progress, 75) @@ -65,8 +51,7 @@ class ScrobbleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/scrobble/pause") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -77,13 +62,12 @@ class ScrobbleTests: XCTestCase { // MARK: - Stop - func test_stop_watching_in_media_center() { - session.nextData = jsonData(named: "test_stop_watching_in_media_center") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_stop_watching_in_media_center() throws { + try mock(.POST, "https://api.trakt.tv/scrobble/stop", result: .success(jsonData(named: "test_stop_watching_in_media_center"))) let expectation = XCTestExpectation(description: "Stop watching in media center") let scrobble = TraktScrobble(movie: SyncId(trakt: 12345), progress: 99.9) - try! traktManager.scrobbleStop(scrobble) { result in + try traktManager.scrobbleStop(scrobble) { result in if case .success(let response) = result { XCTAssertEqual(response.action, "scrobble") XCTAssertEqual(response.progress, 99.9) @@ -92,8 +76,7 @@ class ScrobbleTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/scrobble/stop") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/SearchTests.swift b/Tests/TraktKitTests/SearchTests.swift index 82b7633..49b2c29 100644 --- a/Tests/TraktKitTests/SearchTests.swift +++ b/Tests/TraktKitTests/SearchTests.swift @@ -9,22 +9,12 @@ import XCTest @testable import TraktKit -class SearchTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class SearchTests: TraktTestCase { // MARK: - Text query - func test_search_query() { - session.nextData = jsonData(named: "test_search_query") + func test_search_query() throws { + try mock(.GET, "https://api.trakt.tv/search/movie,show,episode,person,list?query=tron&extended=min", result: .success(jsonData(named: "test_search_query"))) let expectation = XCTestExpectation(description: "Search") traktManager.search(query: "tron", types: [.movie, .show, .episode, .person, .list]) { result in @@ -34,7 +24,6 @@ class SearchTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/search/movie,show,episode,person,list") switch result { case .timedOut: @@ -46,8 +35,8 @@ class SearchTests: XCTestCase { // MARK: - ID Lookup - func test_id_lookup() { - session.nextData = jsonData(named: "test_id_lookup") + func test_id_lookup() throws { + try mock(.GET, "https://api.trakt.tv/search/imdb/tt0848228?type=movie&extended=min", result: .success(jsonData(named: "test_id_lookup"))) let expectation = XCTestExpectation(description: "Lookup Id") traktManager.lookup(id: .IMDB(id: "tt0848228"), type: .movie) { result in @@ -57,9 +46,7 @@ class SearchTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/search/imdb/tt0848228") - XCTAssertTrue(session.lastURL?.query?.contains("type=movie") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/SeasonTests.swift b/Tests/TraktKitTests/SeasonTests.swift index 4784314..f2edb4b 100644 --- a/Tests/TraktKitTests/SeasonTests.swift +++ b/Tests/TraktKitTests/SeasonTests.swift @@ -9,22 +9,12 @@ import XCTest @testable import TraktKit -class SeasonTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class SeasonTests: TraktTestCase { // MARK: - Summary - func test_get_all_seasons() { - session.nextData = jsonData(named: "test_get_all_seasons") + func test_get_all_seasons() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons?extended=min", result: .success(jsonData(named: "test_get_all_seasons"))) let expectation = XCTestExpectation(description: "Get all seasons") traktManager.getSeasons(showID: "game-of-thrones") { result in @@ -34,8 +24,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -44,8 +33,8 @@ class SeasonTests: XCTestCase { } } - func test_get_all_seasons_and_episodes() { - session.nextData = jsonData(named: "test_get_all_seasons_and_episodes") + func test_get_all_seasons_and_episodes() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons?extended=episodes", result: .success(jsonData(named: "test_get_all_seasons_and_episodes"))) let expectation = XCTestExpectation(description: "Get all seasons and episodes") traktManager.getSeasons(showID: "game-of-thrones", extended: [.Episodes]) { result in @@ -55,8 +44,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons?extended=episodes") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -67,8 +55,8 @@ class SeasonTests: XCTestCase { // MARK: - Season - func test_get_season() { - session.nextData = jsonData(named: "test_get_season") + func test_get_season() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1?extended=min", result: .success(jsonData(named: "test_get_season"))) let expectation = XCTestExpectation(description: "Get all seasons and episodes") traktManager.getEpisodesForSeason(showID: "game-of-thrones", season: 1) { result in @@ -78,8 +66,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -88,8 +75,8 @@ class SeasonTests: XCTestCase { } } - func test_get_translated_season() { - session.nextData = jsonData(named: "test_get_season") + func test_get_translated_season() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1?extended=min&translations=es", result: .success(jsonData(named: "test_get_season"))) let expectation = XCTestExpectation(description: "Get translated episodes") traktManager.getEpisodesForSeason(showID: "game-of-thrones", season: 1, translatedInto: "es") { result in @@ -99,9 +86,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/shows/game-of-thrones/seasons/1") - XCTAssertTrue(session.lastURL?.query?.contains("translations=es") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) + switch result { case .timedOut: XCTFail("Something isn't working") @@ -112,8 +97,8 @@ class SeasonTests: XCTestCase { // MARK: - Comments - func test_get_season_comments() { - session.nextData = jsonData(named: "test_get_season_comments") + func test_get_season_comments() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/comments", result: .success(jsonData(named: "test_get_season_comments"))) let expectation = XCTestExpectation(description: "Get season comments") traktManager.getAllSeasonComments(showID: "game-of-thrones", season: 1) { result in @@ -123,8 +108,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/comments") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -135,8 +119,8 @@ class SeasonTests: XCTestCase { // MARK: - Lists - func test_get_lists_containing_season() { - session.nextData = jsonData(named: "test_get_lists_containing_season") + func test_get_lists_containing_season() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/lists/personal/added", result: .success(jsonData(named: "test_get_lists_containing_season"))) let expectation = XCTestExpectation(description: "Get lists containing season") traktManager.getListsContainingSeason(showID: "game-of-thrones", season: 1, listType: ListType.personal, sortBy: .added) { result in @@ -146,8 +130,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/lists/personal/added") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -158,8 +141,8 @@ class SeasonTests: XCTestCase { // MARK: - Ratings - func test_get_season_rating() { - session.nextData = jsonData(named: "test_get_season_rating") + func test_get_season_rating() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/ratings", result: .success(jsonData(named: "test_get_season_rating"))) let expectation = XCTestExpectation(description: "Get season ratings") traktManager.getSeasonRatings(showID: "game-of-thrones", season: 1) { result in @@ -171,8 +154,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/ratings") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -183,8 +165,8 @@ class SeasonTests: XCTestCase { // MARK: - Stats - func test_get_season_stats() { - session.nextData = jsonData(named: "test_get_season_stats") + func test_get_season_stats() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/stats", result: .success(jsonData(named: "test_get_season_stats"))) let expectation = XCTestExpectation(description: "Get season stats") traktManager.getSeasonStatistics(showID: "game-of-thrones", season: 1) { result in @@ -200,8 +182,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/stats") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -212,8 +193,8 @@ class SeasonTests: XCTestCase { // MARK: - Watching - func test_get_users_watching_season() { - session.nextData = jsonData(named: "test_get_users_watching_season") + func test_get_users_watching_season() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/watching", result: .success(jsonData(named: "test_get_users_watching_season"))) let expectation = XCTestExpectation(description: "Get users watching season") traktManager.getUsersWatchingSeasons(showID: "game-of-thrones", season: 1) { result in @@ -223,8 +204,7 @@ class SeasonTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/watching") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -235,8 +215,8 @@ class SeasonTests: XCTestCase { // MARK: - People - func test_get_show_people_min() { - session.nextData = jsonData(named: "test_get_season_cast") + func test_get_show_people_min() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/people?extended=min", result: .success(jsonData(named: "test_get_season_cast"))) let expectation = XCTestExpectation(description: "ShowCastAndCrew") traktManager.getPeopleInSeason(showID: "game-of-thrones", season: 1) { result in @@ -253,8 +233,7 @@ class SeasonTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/seasons/1/people?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/ShowTests.swift b/Tests/TraktKitTests/ShowTests.swift deleted file mode 100644 index 676f5b0..0000000 --- a/Tests/TraktKitTests/ShowTests.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// ShowTests.swift -// TraktKit -// -// Created by Maximilian Litteral on 7/20/15. -// Copyright © 2015 Maximilian Litteral. All rights reserved. -// - -import XCTest -@testable import TraktKit -/* -class ShowTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testParseShow() { - let expectation = self.expectation(description: "High Expectations") - let numberOfTrendingShows = 100 - var count = 0 - - TraktManager.sharedManager.getTrendingShows(page: 1, limit: numberOfTrendingShows) { (objects, error) -> Void in - guard error == nil else { - print("Error getting trending shows: \(error)") - XCTAssert(false, "Error getting trending shows") - return - } - - guard let trendingShows = objects else { - XCTAssert(false, "objects is nil") - return - } - - for trendingObject in trendingShows { - if let showDict = trendingObject["show"] as? [String: AnyObject] { - if let showIDs = showDict["ids"] as? [String: AnyObject] { - if let traktID = showIDs["trakt"] as? NSNumber { - - TraktManager.sharedManager.getShowSummary(showID: traktID, extended: [ExtendedType.Full]) { (dictionary, error) -> Void in - count = count + 1 - - guard error == nil else { - print("Error getting Show Summary: \(error)") - XCTAssert(false, "Error getting show summary") - return - } - - guard let summary = dictionary else { - XCTAssert(false, "Error getting show summary") - return - } - - let showTitle = summary["title"] as? String - let _ = summary["certification"] as? String ?? "Unknown" - let showRuntime = summary["runtime"] as? NSNumber - let showOverview = summary["overview"] as? String - let _ = summary["country"] as? String ?? "us" - let showNetwork = summary["network"] as? String - let showStatus = summary["status"] as? String - let showYear = summary["year"] as? NSNumber - let _ = summary["language"] as? String ?? "en" - - if let title = showTitle, let _ = showRuntime, let _ = showOverview, let _ = showNetwork, let _ = showStatus, let _ = showYear { - print("Parsed \(title) succesfully!") - XCTAssert(true, "JSON was parsed correctly") - } - else { - print("JSON: \(summary)") - XCTAssert(false, "JSON was not parsed correctly") - } - - if count == numberOfTrendingShows { - expectation.fulfill() - } - } - } - } - } - } - } - - self.waitForExpectations(timeout: 30.0, handler: { (error) -> Void in - if let error = error { - print("Timeout error: \(error)") - } - }) - } - - func testEpisodeRatings() { - let expectation = self.expectation(description: "High Expectations") - - TraktManager.sharedManager.getEpisodeRatings(showID: 77686, seasonNumber: 1, episodeNumber: 1) { (dictionary, error) -> Void in - guard error == nil else { - XCTAssert(false, "Error getting episode ratings") - return - } - - XCTAssert(true, "Passed!") - expectation.fulfill() - } - - self.waitForExpectations(timeout: 10) { (error) -> Void in - if let error = error { - print("Timeout error: \(error)") - } - } - } -}*/ diff --git a/Tests/TraktKitTests/ShowsTests.swift b/Tests/TraktKitTests/ShowsTests.swift index c2b8929..197f1a3 100644 --- a/Tests/TraktKitTests/ShowsTests.swift +++ b/Tests/TraktKitTests/ShowsTests.swift @@ -10,52 +10,48 @@ import XCTest import Foundation @testable import TraktKit -class ShowsTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class ShowsTests: TraktTestCase { // MARK: - Trending func test_get_min_trending_shows_await() async throws { - session.nextData = jsonData(named: "TrendingShows_Min") + try mock(.GET, "https://api.trakt.tv/shows/trending?extended=min&page=1&limit=10", result: .success(jsonData(named: "TrendingShows_Min")), headers: [.page(1), .pageCount(100)]) - let trendingShows = try await traktManager.explore.trending.shows() + let response = try await traktManager.shows.trending() .extend(.Min) .page(1) .limit(10) .perform() + let trendingShows = response.object XCTAssertEqual(trendingShows.count, 10) - XCTAssertEqual(session.lastURL?.path, "/shows/trending") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + XCTAssertEqual(response.currentPage, 1) + XCTAssertEqual(response.pageCount, 100) + let firstTrendingShow = try XCTUnwrap(trendingShows.first) + XCTAssertEqual(firstTrendingShow.watchers, 252) + XCTAssertEqual(firstTrendingShow.show.title, "Marvel's Jessica Jones") + XCTAssertNil(firstTrendingShow.show.overview) } func test_get_min_trending_shows() { - session.nextData = jsonData(named: "TrendingShows_Min") + try? mock(.GET, "https://api.trakt.tv/shows/trending?extended=min&page=1&limit=10", result: .success(jsonData(named: "TrendingShows_Min"))) let expectation = XCTestExpectation(description: "TrendingShows") traktManager.getTrendingShows(pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let trendingShows, _, _) = result { - XCTAssertEqual(trendingShows.count, 10) + do { + XCTAssertEqual(trendingShows.count, 10) + let firstTrendingShow = try XCTUnwrap(trendingShows.first) + XCTAssertEqual(firstTrendingShow.watchers, 252) + XCTAssertEqual(firstTrendingShow.show.title, "Marvel's Jessica Jones") + XCTAssertNil(firstTrendingShow.show.overview) + } catch { + XCTFail("Something isn't working") + } expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.path, "/shows/trending") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -65,22 +61,25 @@ class ShowsTests: XCTestCase { } func test_get_full_trending_shows() { - session.nextData = jsonData(named: "TrendingShows_Full") + try? mock(.GET, "https://api.trakt.tv/shows/trending?extended=full&page=1&limit=10", result: .success(jsonData(named: "TrendingShows_Full"))) let expectation = XCTestExpectation(description: "TrendingShows") traktManager.getTrendingShows(pagination: Pagination(page: 1, limit: 10), extended: [.Full]) { result in if case .success(let trendingShows, _, _) = result { - XCTAssertEqual(trendingShows.count, 10) + do { + XCTAssertEqual(trendingShows.count, 10) + let firstTrendingShow = try XCTUnwrap(trendingShows.first) + XCTAssertEqual(firstTrendingShow.watchers, 252) + XCTAssertEqual(firstTrendingShow.show.title, "Marvel's Jessica Jones") + XCTAssertEqual(firstTrendingShow.show.overview, "A former superhero decides to reboot her life by becoming a private investigator.") + } catch { + XCTFail("Something isn't working") + } expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.path, "/shows/trending") - XCTAssertTrue(session.lastURL?.query?.contains("extended=full") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -92,7 +91,7 @@ class ShowsTests: XCTestCase { // MARK: - Popular func test_get_popular_shows() { - session.nextData = jsonData(named: "test_get_popular_shows") + try? mock(.GET, "https://api.trakt.tv/shows/popular?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_popular_shows"))) let expectation = XCTestExpectation(description: "Get popular shows") traktManager.getPopularShows(pagination: Pagination(page: 1, limit: 10)) { result in @@ -101,11 +100,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/popular") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -117,7 +112,7 @@ class ShowsTests: XCTestCase { // MARK: - Played func test_get_most_played_shows() { - session.nextData = jsonData(named: "test_get_most_played_shows") + try? mock(.GET, "https://api.trakt.tv/shows/played/weekly?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_played_shows"))) let expectation = XCTestExpectation(description: "Get most played shows") traktManager.getPlayedShows(pagination: Pagination(page: 1, limit: 10)) { result in @@ -126,11 +121,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/played/weekly") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -142,7 +133,7 @@ class ShowsTests: XCTestCase { // MARK: - Watched func test_get_most_watched_shows() { - session.nextData = jsonData(named: "test_get_most_watched_shows") + try? mock(.GET, "https://api.trakt.tv/shows/watched/weekly?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_watched_shows"))) let expectation = XCTestExpectation(description: "Get most watched shows") traktManager.getWatchedShows(pagination: Pagination(page: 1, limit: 10)) { result in @@ -151,11 +142,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/watched/weekly") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -164,10 +151,28 @@ class ShowsTests: XCTestCase { } } + func test_get_most_watched_shows_async() async throws { + try? mock(.GET, "https://api.trakt.tv/shows/watched/weekly?page=1&limit=10", result: .success(jsonData(named: "test_get_most_watched_shows")), headers: [.page(1), .pageCount(8)]) + + let pagedObject = try await traktManager.shows + .watched(period: .weekly) + .page(1) + .limit(10) + .perform() + let (watchedShows, page, total) = (pagedObject.object, pagedObject.currentPage, pagedObject.pageCount) + + XCTAssertEqual(watchedShows.count, 10) + XCTAssertEqual(page, 1) + XCTAssertEqual(total, 8) + let firstWatchedShow = try XCTUnwrap(watchedShows.first) + XCTAssertEqual(firstWatchedShow.playCount, 8784154) + XCTAssertEqual(firstWatchedShow.watcherCount, 203742) + } + // MARK: - Collected func test_get_most_collected_shows() { - session.nextData = jsonData(named: "test_get_most_collected_shows") + try? mock(.GET, "https://api.trakt.tv/shows/collected/weekly?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_collected_shows"))) let expectation = XCTestExpectation(description: "Get most collected shows") traktManager.getCollectedShows(pagination: Pagination(page: 1, limit: 10)) { result in @@ -176,11 +181,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/collected/weekly") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -192,7 +193,7 @@ class ShowsTests: XCTestCase { // MARK: - Anticipated func test_get_most_anticipated_shows() { - session.nextData = jsonData(named: "test_get_most_anticipated_shows") + try? mock(.GET, "https://api.trakt.tv/shows/anticipated?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_most_anticipated_shows"))) let expectation = XCTestExpectation(description: "Get anticipated shows") traktManager.getAnticipatedShows(pagination: Pagination(page: 1, limit: 10)) { result in @@ -201,11 +202,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/anticipated") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -216,21 +213,17 @@ class ShowsTests: XCTestCase { // MARK: - Updates - func test_get_updated_shows() { - session.nextData = jsonData(named: "test_get_updated_shows") + func test_get_updated_shows() throws { + try mock(.GET, "https://api.trakt.tv/shows/updates/2014-09-22?extended=min&page=1&limit=10", result: .success(jsonData(named: "test_get_updated_shows"))) let expectation = XCTestExpectation(description: "Get updated shows") - traktManager.getUpdatedShows(startDate: try! Date.dateFromString("2014-09-22"), pagination: Pagination(page: 1, limit: 10)) { result in + traktManager.getUpdatedShows(startDate: try Date.dateFromString("2014-09-22"), pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let shows, _, _) = result { XCTAssertEqual(shows.count, 2) expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/updates/2014-09-22") - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -239,10 +232,20 @@ class ShowsTests: XCTestCase { } } + func test_get_updated_show_ids() async throws { + try mock(.GET, "https://api.trakt.tv/shows/updates/id/2025-01-01T00:00:00", result: .success(jsonData(named: "test_get_updated_shows_ids"))) + + let updatedIds = try await traktManager.shows + .recentlyUpdatedIds(since: try Date.dateFromString("2025-01-01")) + .perform() + XCTAssertEqual(updatedIds.object.count, 100) + XCTAssertEqual(updatedIds.object.first, 163302) + } + // MARK: - Summary func test_get_min_show() { - session.nextData = jsonData(named: "Show_Min") + try? mock(.GET, "https://api.trakt.tv/shows/game-of-thrones?extended=min", result: .success(jsonData(named: "Show_Min"))) let expectation = XCTestExpectation(description: "ShowSummary") traktManager.getShowSummary(showID: "game-of-thrones") { result in @@ -272,10 +275,7 @@ class ShowsTests: XCTestCase { } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones?extended=min") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -285,7 +285,7 @@ class ShowsTests: XCTestCase { } func test_get_full_show() { - session.nextData = jsonData(named: "Show_Full") + try? mock(.GET, "https://api.trakt.tv/shows/game-of-thrones?extended=full", result: .success(jsonData(named: "Show_Full"))) let expectation = XCTestExpectation(description: "ShowSummary") traktManager.getShowSummary(showID: "game-of-thrones", extended: [.Full]) { result in @@ -315,10 +315,7 @@ class ShowsTests: XCTestCase { } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones?extended=full") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -328,7 +325,7 @@ class ShowsTests: XCTestCase { } func test_get_full_show_await() async throws { - session.nextData = jsonData(named: "Show_Full") + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones?extended=full", result: .success(jsonData(named: "Show_Full"))) let show = try await traktManager.show(id: "game-of-thrones").summary() .extend(.Full) @@ -357,10 +354,39 @@ class ShowsTests: XCTestCase { XCTAssertEqual(show.airedEpisodes, 50) } + func test_get_show_images() async throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones?extended=images", result: .success(jsonData(named: "Show_Images"))) + + let show = try await traktManager.show(id: "game-of-thrones").summary().extend(.images).perform() + XCTAssertEqual(show.title, "Severance") + XCTAssertEqual(show.year, 2022) + XCTAssertEqual(show.ids.trakt, 154997) + XCTAssertEqual(show.ids.slug, "severance") + XCTAssertNotNil(show.images) + XCTAssertEqual(show.images?.poster.count, 1) + XCTAssertNil(show.overview) + XCTAssertNil(show.firstAired) + XCTAssertNil(show.airs) + XCTAssertNil(show.runtime) + XCTAssertNil(show.certification) + XCTAssertNil(show.network) + XCTAssertNil(show.country) + XCTAssertNil(show.trailer) + XCTAssertNil(show.homepage) + XCTAssertNil(show.status) + XCTAssertNil(show.rating) + XCTAssertNil(show.votes) + XCTAssertNil(show.updatedAt) + XCTAssertNil(show.language) + XCTAssertNil(show.availableTranslations) + XCTAssertNil(show.genres) + XCTAssertNil(show.airedEpisodes) + } + // MARK: - Aliases func test_get_show_aliases() { - session.nextData = jsonData(named: "ShowAliases") + try? mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/aliases", result: .success(jsonData(named: "ShowAliases"))) let expectation = XCTestExpectation(description: "ShowAliases") traktManager.getShowAliases(showID: "game-of-thrones") { result in @@ -370,10 +396,7 @@ class ShowsTests: XCTestCase { } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/aliases") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -383,30 +406,26 @@ class ShowsTests: XCTestCase { } func test_get_show_aliases_await() async throws { - session.nextData = jsonData(named: "ShowAliases") + try? mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/aliases", result: .success(jsonData(named: "ShowAliases"))) let aliases = try await traktManager.show(id: "game-of-thrones").aliases().perform() XCTAssertEqual(aliases.count, 32) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/aliases") } // MARK: - Translations func test_get_show_translations() { - session.nextData = jsonData(named: "test_get_show_translations") + try? mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/translations/es", result: .success(jsonData(named: "test_get_show_translations"))) let expectation = XCTestExpectation(description: "Get show translations") traktManager.getShowTranslations(showID: "game-of-thrones", language: "es") { result in if case .success(let translations) = result { - XCTAssertEqual(translations.count, 3) + XCTAssertEqual(translations.count, 40) expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/translations/es") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -417,19 +436,17 @@ class ShowsTests: XCTestCase { // MARK: - Comments - func test_get_show_comments() { - session.nextData = jsonData(named: "test_get_show_comments") + func test_get_show_comments() throws { + try mock(.GET, "https://api.trakt.tv/shows/presumed-innocent/comments", result: .success(jsonData(named: "test_get_show_comments"))) let expectation = XCTestExpectation(description: "Get show comments") - traktManager.getShowComments(showID: "game-of-thrones") { result in + traktManager.getShowComments(showID: "presumed-innocent") { result in if case .success(let comments, _, _) = result { - XCTAssertEqual(comments.count, 1) + XCTAssertEqual(comments.count, 10) expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/comments") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -438,10 +455,27 @@ class ShowsTests: XCTestCase { } } + func test_get_show_comments_async() async throws { + try mock(.GET, "https://api.trakt.tv/shows/presumed-innocent/comments/newest", result: .success(jsonData(named: "test_get_show_comments")), headers: [.page(1), .pageCount(4)]) + + let pagedObject = try await traktManager.show(id: "presumed-innocent") + .comments(sort: "newest") + .perform() + let (watchedShows, page, total) = (pagedObject.object, pagedObject.currentPage, pagedObject.pageCount) + + XCTAssertEqual(watchedShows.count, 10) + XCTAssertEqual(page, 1) + XCTAssertEqual(total, 4) + + let firstComment = try XCTUnwrap(watchedShows.first) + XCTAssertEqual(firstComment.comment, "I did NOT expect that ending. Wow! What a show!") + XCTAssertEqual(firstComment.parentId, 0) + } + // MARK: - Lists - func test_get_lists_containing_show() { - session.nextData = jsonData(named: "test_get_lists_containing_show") + func test_get_lists_containing_show() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/lists", result: .success(jsonData(named: "test_get_lists_containing_show"))) let expectation = XCTestExpectation(description: "Get lists containing shows") traktManager.getListsContainingShow(showID: "game-of-thrones") { result in @@ -450,9 +484,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/lists") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -463,8 +495,8 @@ class ShowsTests: XCTestCase { // MARK: - Collection Progress - func testParseShowCollectionProgress() { - session.nextData = jsonData(named: "ShowCollectionProgress") + func testParseShowCollectionProgress() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/progress/collection?hidden=false&specials=false", result: .success(jsonData(named: "ShowCollectionProgress"))) let expectation = XCTestExpectation(description: "Get collected progress") traktManager.getShowCollectionProgress(showID: "game-of-thrones") { result in @@ -480,10 +512,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/game-of-thrones/progress/collection") - XCTAssertTrue(session.lastURL?.query?.contains("hidden=false") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("specials=false") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -494,8 +523,8 @@ class ShowsTests: XCTestCase { // MARK: - Watched Progress - func test_get_wathced_progress() { - session.nextData = jsonData(named: "test_get_wathced_progress") + func test_get_wathced_progress() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/progress/watched?hidden=false&specials=false", result: .success(jsonData(named: "test_get_wathced_progress"))) let expectation = XCTestExpectation(description: "Get watched progress") traktManager.getShowWatchedProgress(showID: "game-of-thrones") { result in @@ -505,10 +534,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.path, "/shows/game-of-thrones/progress/watched") - XCTAssertTrue(session.lastURL?.query?.contains("hidden=false") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("specials=false") ?? false) + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -519,8 +545,8 @@ class ShowsTests: XCTestCase { // MARK: - People - func test_get_show_people_min() { - session.nextData = jsonData(named: "ShowCastAndCrew_Min") + func test_get_show_people_min() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/people?extended=min", result: .success(jsonData(named: "ShowCastAndCrew_Min"))) let expectation = XCTestExpectation(description: "ShowCastAndCrew") traktManager.getPeopleInShow(showID: "game-of-thrones") { result in @@ -536,9 +562,7 @@ class ShowsTests: XCTestCase { } expectation.fulfill() } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/people?extended=min") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -547,9 +571,9 @@ class ShowsTests: XCTestCase { } } - func test_get_show_people_full() { - session.nextData = jsonData(named: "ShowCastAndCrew_Full") - + func test_get_show_people_full() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/people?extended=full", result: .success(jsonData(named: "ShowCastAndCrew_Full"))) + let expectation = XCTestExpectation(description: "ShowCastAndCrew") traktManager.getPeopleInShow(showID: "game-of-thrones", extended: [.Full]) { result in if case .success(let castAndCrew) = result { @@ -560,9 +584,7 @@ class ShowsTests: XCTestCase { } expectation.fulfill() } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/people?extended=full") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -571,9 +593,9 @@ class ShowsTests: XCTestCase { } } - func test_get_show_people_guest_stars() { - session.nextData = jsonData(named: "ShowCastAndCrew_GuestStars") - + func test_get_show_people_guest_stars() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/people?extended=guest_stars", result: .success(jsonData(named: "ShowCastAndCrew_GuestStars"))) + let expectation = XCTestExpectation(description: "ShowCastAndCrew") traktManager.getPeopleInShow(showID: "game-of-thrones", extended: [.guestStars]) { result in if case .success(let castAndCrew) = result { @@ -586,9 +608,7 @@ class ShowsTests: XCTestCase { } expectation.fulfill() } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/people?extended=guest_stars") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -599,8 +619,8 @@ class ShowsTests: XCTestCase { // MARK: - Ratings - func test_get_show_ratings() { - session.nextData = jsonData(named: "test_get_show_ratings") + func test_get_show_ratings() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/ratings", result: .success(jsonData(named: "test_get_show_ratings"))) let expectation = XCTestExpectation(description: "Get show ratings") traktManager.getShowRatings(showID: "game-of-thrones") { result in @@ -610,9 +630,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/ratings") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -623,8 +641,8 @@ class ShowsTests: XCTestCase { // MARK: - Related - func test_get_related_shows() { - session.nextData = jsonData(named: "test_get_related_shows") + func test_get_related_shows() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/related?extended=min", result: .success(jsonData(named: "test_get_related_shows"))) let expectation = XCTestExpectation(description: "Get related shows") traktManager.getRelatedShows(showID: "game-of-thrones") { result in @@ -633,9 +651,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/related?extended=min") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -646,8 +662,8 @@ class ShowsTests: XCTestCase { // MARK: - Stats - func test_get_stats() { - session.nextData = jsonData(named: "ShowStats") + func test_get_stats() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/stats", result: .success(jsonData(named: "ShowStats"))) let expectation = XCTestExpectation(description: "Stats") traktManager.getShowStatistics(showID: "game-of-thrones") { result in @@ -661,9 +677,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/stats") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -674,8 +688,8 @@ class ShowsTests: XCTestCase { // MARK: - Watching - func test_get_users_watching_show() { - session.nextData = jsonData(named: "test_get_users_watching_show") + func test_get_users_watching_show() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/watching", result: .success(jsonData(named: "test_get_users_watching_show"))) let expectation = XCTestExpectation(description: "Get users watching the show") traktManager.getUsersWatchingShow(showID: "game-of-thrones") { result in @@ -684,9 +698,7 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/watching") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: XCTFail("Something isn't working") @@ -697,20 +709,23 @@ class ShowsTests: XCTestCase { // MARK: - Next episode - func test_get_nextEpisode() { - // TODO: Add success with status code, and move `createErrorWithStatusCode` outside TraktManager - session.nextError = traktManager.createErrorWithStatusCode(204) + func test_get_nextEpisode() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/next_episode?extended=min", result: .success(.init()), httpCode: 204) let expectation = XCTestExpectation(description: "NextEpisode") traktManager.getNextEpisode(showID: "game-of-thrones") { result in if case .error(let error) = result { XCTAssertNotNil(error) - XCTAssertEqual(error!._code, 204) - expectation.fulfill() + do { + let traktError = try XCTUnwrap(error as? TraktManager.TraktError) + XCTAssertEqual(traktError, TraktManager.TraktError.noContent) + expectation.fulfill() + } catch { + XCTFail(error.localizedDescription) + } } else { XCTFail("Unexpected result") } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/next_episode?extended=min") + let result = XCTWaiter().wait(for: [expectation], timeout: 1) switch result { case .timedOut: @@ -722,8 +737,8 @@ class ShowsTests: XCTestCase { // MARK: - Last episode - func test_get_lastEpisode() { - session.nextData = jsonData(named: "LastEpisodeAired_min") + func test_get_lastEpisode() throws { + try mock(.GET, "https://api.trakt.tv/shows/game-of-thrones/last_episode?extended=min", result: .success(jsonData(named: "LastEpisodeAired_min"))) let expectation = XCTestExpectation(description: "LastEpisode") traktManager.getLastEpisode(showID: "game-of-thrones") { result in @@ -734,9 +749,8 @@ class ShowsTests: XCTestCase { expectation.fulfill() } } - let result = XCTWaiter().wait(for: [expectation], timeout: 5) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/shows/game-of-thrones/last_episode?extended=min") - + let result = XCTWaiter().wait(for: [expectation], timeout: 1) + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/SyncTests+Async.swift b/Tests/TraktKitTests/SyncTests+Async.swift new file mode 100644 index 0000000..472a2cb --- /dev/null +++ b/Tests/TraktKitTests/SyncTests+Async.swift @@ -0,0 +1,277 @@ +// +// SyncTests+Async.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/1/25. +// + +import Foundation +import Testing +@testable import TraktKit + +extension TraktTestSuite { + @Suite(.serialized) + struct SyncTestSuite { + @Test func getLastActivities() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/last_activities", result: .success(jsonData(named: "test_get_last_activity"))) + + let lastActivities = try await traktManager.sync() + .lastActivities() + .perform() + + // Date formatter to convert date back into String + let dateFormatter = DateFormatter() + dateFormatter.timeZone = .gmt + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + + #expect(dateFormatter.string(from: lastActivities.all) == "2014-11-20T07:01:32.000Z") + + #expect(dateFormatter.string(from: lastActivities.movies.watchedAt) == "2014-11-19T21:42:41.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.collectedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.ratedAt) == "2014-11-19T18:32:29.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.watchlistedAt) == "2014-11-19T21:42:41.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.favoritesAt) == "2014-11-19T21:42:41.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.commentedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.pausedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.movies.hiddenAt) == "2016-08-20T06:51:30.000Z") + + #expect(dateFormatter.string(from: lastActivities.episodes.watchedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.episodes.collectedAt) == "2014-11-19T22:02:41.000Z") + #expect(dateFormatter.string(from: lastActivities.episodes.ratedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.episodes.watchlistedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.episodes.commentedAt) == "2014-11-20T06:51:30.000Z") + #expect(dateFormatter.string(from: lastActivities.episodes.pausedAt) == "2014-11-20T06:51:30.000Z") + } + + // MARK: - Playback + + @Test func getMoviePlaybackProgress() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/playback/movies", result: .success(jsonData(named: "test_get_playback_progress"))) + + let progress = try await traktManager.sync() + .playback(type: "movies") + .perform() + + #expect(progress.count == 2) + let firstItem = try #require(progress.first) + #expect(firstItem.id == 13) + } + + @Test func removePlaybackProgress() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.DELETE, "https://api.trakt.tv/sync/playback/13", result: .success(.init()), httpCode: StatusCodes.SuccessNoContentToReturn) + + try await traktManager.sync().removePlayback(id: 13).perform() + } + + // MARK: - Collection + + @Test func getCollectedMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/collection/movies?extended=min", result: .success(jsonData(named: "test_get_collection"))) + + let movies = try await traktManager.sync() + .collection(type: "movies") + .extend(.Min) + .perform() + #expect(movies.count == 2) + } + + @Test func getCollectedShows() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/collection/shows?extended=min", result: .success(jsonData(named: "test_get_collection_shows"))) + + let movies = try await traktManager.sync() + .collection(type: "shows") + .extend(.Min) + .perform() + #expect(movies.count == 2) + } + + @Test func addItemsToCollection() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/sync/collection", result: .success(jsonData(named: "test_add_items_to_collection"))) + + let result = try await traktManager.sync() + .addToCollection(movies: []) // For mock we don't have to pass anything in. + .perform() + #expect(result.added.movies == 1) + #expect(result.added.episodes == 12) + } + + @Test func removeItemsFromCollection() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/collection/remove", result: .success(jsonData(named: "test_remove_items_from_collection"))) + + let result = try await traktManager.sync() + .removeFromCollection(movies: []) // For mock we don't have to pass anything in. + .perform() + + #expect(result.deleted.movies == 1) + #expect(result.deleted.episodes == 12) + #expect(result.notFound.episodes.count == 0) + #expect(result.notFound.movies.count == 1) + } + + // MARK: - Watched + + @Test func getWatchedMovies() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/watched/movies?extended=min", result: .success(jsonData(named: "test_get_watched"))) + + let watchedMovies = try await traktManager.sync() + .watchedMovies() + .extend(.Min) + .perform() + #expect(watchedMovies.count == 2) + } + + @Test func getWatchedShows() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/watched/shows?extended=min", result: .success(jsonData(named: "test_get_watched_shows"))) + + let watchedShows = try await traktManager.sync() + .watchedShows() + .extend(.Min) + .perform() + #expect(watchedShows.count == 2) + #expect(watchedShows.allSatisfy { $0.seasons != nil }) + } + + @Test func getWatchedShowsWithoutSeasons() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/watched/shows?extended=noseasons", result: .success(jsonData(named: "test_get_watched_shows_noseasons"))) + + let watchedShows = try await traktManager.sync() + .watchedShows() + .extend(.noSeasons) + .perform() + #expect(watchedShows.count == 2) + #expect(watchedShows.allSatisfy { $0.seasons == nil }) + } + + // MARK: - History + + @Test func getHistory() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/history/movies?extended=min", result: .success(jsonData(named: "test_get_watched_history"))) + + let history = try await traktManager.sync() + .history(type: "movies") + .extend(.Min) + .perform() + .object + + #expect(history.count == 3) + } + + @Test func addItemsToHistory() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/sync/history", result: .success(jsonData(named: "test_add_items_to_watched_history"))) + + let result = try await traktManager.sync() + .addToHistory() + .perform() + + #expect(result.added.movies == 2) + #expect(result.added.episodes == 72) + } + + @Test func removeItemsFromHistory() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/history/remove", result: .success(jsonData(named: "test_remove_items_from_history"))) + + let result = try await traktManager.sync() + .removeFromHistory() + .perform() + + #expect(result.deleted.movies == 2) + #expect(result.deleted.episodes == 72) + } + + // MARK: - Ratings + + @Test func getRatings() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/ratings/movies/9", result: .success(jsonData(named: "test_get_ratings"))) + + let ratings = try await traktManager.sync() + .ratings(type: "movies", rating: 9) + .perform() + .object + + #expect(ratings.count == 2) + } + + @Test func addRating() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/sync/ratings", result: .success(jsonData(named: "test_add_new_ratings"))) + + let result = try await traktManager.sync() + .addRatings() + .perform() + + #expect(result.added.movies == 1) + #expect(result.added.shows == 1) + #expect(result.added.seasons == 1) + #expect(result.added.episodes == 2) + #expect(result.notFound.movies.count == 1) + } + + @Test func removeRating() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/ratings/remove", result: .success(jsonData(named: "test_remove_ratings"))) + + let result = try await traktManager.sync() + .removeRatings() + .perform() + + #expect(result.deleted.movies == 1) + #expect(result.deleted.shows == 1) + #expect(result.deleted.seasons == 1) + #expect(result.deleted.episodes == 2) + } + + // MARK: - Watchlist + + @Test func getWatchlist() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/sync/watchlist/movies?extended=min", result: .success(jsonData(named: "test_get_watchlist"))) + + let watchlist = try await traktManager.sync() + .watchlist(type: "movies") + .extend(.Min) + .perform() + .object + + #expect(watchlist.count == 2) + + let firstItem = try #require(watchlist.first) + #expect(firstItem.rank == 1) + #expect(firstItem.id == 101) + #expect(firstItem.type == "movie") + #expect(firstItem.movie?.title == "TRON: Legacy") + #expect(firstItem.notes == "Need to catch up before TRON 3 is out.") + } + + @Test func addToWatchlist() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/sync/watchlist", result: .success(jsonData(named: "test_add_items_to_watchlist"))) + + let result = try await traktManager.sync() + .addToWatchlist() + .perform() + + #expect(result.added.movies == 1) + #expect(result.added.shows == 1) + #expect(result.added.seasons == 1) + #expect(result.added.episodes == 2) + + #expect(result.notFound.movies.count == 1) + + #expect(result.list.itemCount == 5) + } + } +} diff --git a/Tests/TraktKitTests/SyncTests.swift b/Tests/TraktKitTests/SyncTests.swift index 13fbf83..e45be33 100644 --- a/Tests/TraktKitTests/SyncTests.swift +++ b/Tests/TraktKitTests/SyncTests.swift @@ -10,22 +10,12 @@ import XCTest import Foundation @testable import TraktKit -class SyncTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class SyncTests: TraktTestCase { // MARK: - Last Activities - func test_get_last_activity() { - session.nextData = jsonData(named: "test_get_last_activity") + func test_get_last_activity() throws { + try mock(.GET, "https://api.trakt.tv/sync/last_activities", result: .success(jsonData(named: "test_get_last_activity"))) let expectation = XCTestExpectation(description: "Get Last Activity") traktManager.lastActivities { result in @@ -34,8 +24,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/last_activities") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -46,23 +35,26 @@ class SyncTests: XCTestCase { // MARK: - Playback - func test_get_playback_progress() { - session.nextData = jsonData(named: "test_get_playback_progress") + func test_get_playback_progress() throws { + try mock(.GET, "https://api.trakt.tv/sync/playback/movies", result: .success(jsonData(named: "test_get_playback_progress"))) let expectation = XCTestExpectation(description: "Get Playback progress") traktManager.getPlaybackProgress(type: .Movies) { result in if case .success(let progress) = result { XCTAssertEqual(progress.count, 2) - let first = progress.first - XCTAssertEqual(first?.progress, 10) - XCTAssertNotNil(first?.movie) - XCTAssertEqual(first?.id, 13) + do { + let first = try XCTUnwrap(progress.first) + XCTAssertEqual(first.progress, 10) + XCTAssertNotNil(first.movie) + XCTAssertEqual(first.id, 13) + } catch { + XCTFail("Failed to unwrap") + } expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/playback/movies") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -73,8 +65,8 @@ class SyncTests: XCTestCase { // MARK: - Remove Playback - func test_remove_a_playback_item() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_remove_a_playback_item() throws { + try mock(.DELETE, "https://api.trakt.tv/sync/playback/13", result: .success(.init()), httpCode: StatusCodes.SuccessNoContentToReturn) let expectation = XCTestExpectation(description: "Remove playback item") traktManager.removePlaybackItem(id: 13) { result in @@ -83,7 +75,6 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/playback/13") switch result { case .timedOut: @@ -95,8 +86,8 @@ class SyncTests: XCTestCase { // MARK: - Get Collection - func test_get_collection() { - session.nextData = jsonData(named: "test_get_collection") + func test_get_collection() throws { + try mock(.GET, "https://api.trakt.tv/sync/collection/movies?extended=min", result: .success(jsonData(named: "test_get_collection"))) let expectation = XCTestExpectation(description: "Get collection") traktManager.getCollection(type: .Movies) { result in @@ -106,8 +97,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/collection/movies?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -116,8 +106,8 @@ class SyncTests: XCTestCase { } } - func test_get_collection_shows() { - session.nextData = jsonData(named: "test_get_collection_shows") + func test_get_collection_shows() throws { + try mock(.GET, "https://api.trakt.tv/sync/collection/shows?extended=min", result: .success(jsonData(named: "test_get_collection_shows"))) let expectation = XCTestExpectation(description: "Get shows collection") traktManager.getCollection(type: .Shows) { result in @@ -131,8 +121,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/collection/shows?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -143,12 +132,11 @@ class SyncTests: XCTestCase { // MARK: - Add to Collection - func test_add_items_to_collection() { - session.nextData = jsonData(named: "test_add_items_to_collection") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_items_to_collection() throws { + try mock(.POST, "https://api.trakt.tv/sync/collection", result: .success(jsonData(named: "test_add_items_to_collection"))) let expectation = XCTestExpectation(description: "Add items to collection") - try! traktManager.addToCollection(movies: [], shows: [], episodes: []) { result in + try traktManager.addToCollection(movies: [], shows: [], episodes: []) { result in if case .success(let result) = result { XCTAssertEqual(result.added.movies, 1) XCTAssertEqual(result.added.episodes, 12) @@ -156,8 +144,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/collection") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -168,11 +155,11 @@ class SyncTests: XCTestCase { // MARK: - Remove from Collection - func test_remove_items_from_collection() { - session.nextData = jsonData(named: "test_remove_items_from_collection") + func test_remove_items_from_collection() throws { + try mock(.GET, "https://api.trakt.tv/sync/collection/remove", result: .success(jsonData(named: "test_remove_items_from_collection"))) let expectation = XCTestExpectation(description: "Remove items from collection") - try! traktManager.removeFromCollection(movies: [], shows: [], episodes: []) { result in + try traktManager.removeFromCollection(movies: [], shows: [], episodes: []) { result in if case .success(let result) = result { XCTAssertEqual(result.deleted.movies, 1) XCTAssertEqual(result.deleted.episodes, 12) @@ -182,8 +169,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/collection/remove") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -194,8 +180,8 @@ class SyncTests: XCTestCase { // MARK: - Get Watched - func test_get_watched() { - session.nextData = jsonData(named: "test_get_watched") + func test_get_watched() throws { + try mock(.GET, "https://api.trakt.tv/sync/watched/movies?extended=min", result: .success(jsonData(named: "test_get_watched"))) let expectation = XCTestExpectation(description: "Get Watched") traktManager.getWatchedMovies { result in @@ -205,8 +191,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watched/movies?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -215,8 +200,8 @@ class SyncTests: XCTestCase { } } - func test_get_watched_shows_noseasons() { - session.nextData = jsonData(named: "test_get_watched_shows_noseasons") + func test_get_watched_shows_noseasons() throws { + try mock(.GET, "https://api.trakt.tv/sync/watched/shows?extended=noseasons", result: .success(jsonData(named: "test_get_watched_shows_noseasons"))) let expectation = XCTestExpectation(description: "Get Watched - noSeasons") traktManager.getWatchedShows(extended: [.noSeasons]) { result in @@ -230,8 +215,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watched/shows?extended=noseasons") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -240,8 +224,8 @@ class SyncTests: XCTestCase { } } - func test_get_watched_shows() { - session.nextData = jsonData(named: "test_get_watched_shows") + func test_get_watched_shows() throws { + try mock(.GET, "https://api.trakt.tv/sync/watched/shows?extended=min", result: .success(jsonData(named: "test_get_watched_shows"))) let expectation = XCTestExpectation(description: "Get Watched") traktManager.getWatchedShows { result in @@ -255,8 +239,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watched/shows?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -267,8 +250,8 @@ class SyncTests: XCTestCase { // MARK: - Get History - func test_get_watched_history() { - session.nextData = jsonData(named: "test_get_watched_history") + func test_get_watched_history() throws { + try mock(.GET, "https://api.trakt.tv/sync/history/movies?extended=min", result: .success(jsonData(named: "test_get_watched_history"))) let expectation = XCTestExpectation(description: "Get Watched history") traktManager.getHistory(type: .Movies) { result in @@ -278,8 +261,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/history/movies?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -290,12 +272,11 @@ class SyncTests: XCTestCase { // MARK: - Add to History - func test_add_items_to_watched_history() { - session.nextData = jsonData(named: "test_add_items_to_watched_history") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_items_to_watched_history() throws { + try mock(.POST, "https://api.trakt.tv/sync/history", result: .success(jsonData(named: "test_add_items_to_watched_history"))) let expectation = XCTestExpectation(description: "Add items to history") - try! traktManager.addToHistory(movies: [], shows: [], episodes: []) { result in + try traktManager.addToHistory(movies: [], shows: [], episodes: []) { result in switch result { case .success(let ids): XCTAssertEqual(ids.added.movies, 2) @@ -306,8 +287,7 @@ class SyncTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/history") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -318,11 +298,11 @@ class SyncTests: XCTestCase { // MARK: - Remove from History - func test_remove_items_from_history() { - session.nextData = jsonData(named: "test_remove_items_from_history") + func test_remove_items_from_history() throws { + try mock(.GET, "https://api.trakt.tv/sync/history/remove", result: .success(jsonData(named: "test_remove_items_from_history"))) let expectation = XCTestExpectation(description: "Remove items from history") - try! traktManager.removeFromHistory(movies: [], shows: [], episodes: []) { result in + try traktManager.removeFromHistory(movies: [], shows: [], episodes: []) { result in switch result { case .success(let ids): XCTAssertEqual(ids.deleted.movies, 2) @@ -333,8 +313,7 @@ class SyncTests: XCTestCase { expectation.fulfill() } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/history/remove") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -345,8 +324,8 @@ class SyncTests: XCTestCase { // MARK: - Get Ratings - func test_get_ratings() { - session.nextData = jsonData(named: "test_get_ratings") + func test_get_ratings() throws { + try mock(.GET, "https://api.trakt.tv/sync/ratings/movies/9", result: .success(jsonData(named: "test_get_ratings"))) let expectation = XCTestExpectation(description: "Get ratings") traktManager.getRatings(type: .Movies, rating: 9) { result in @@ -356,8 +335,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/ratings/movies/9") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -368,12 +346,11 @@ class SyncTests: XCTestCase { // MARK: - Add Ratings - func test_add_new_ratings() { - session.nextData = jsonData(named: "test_add_new_ratings") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_new_ratings() throws { + try mock(.POST, "https://api.trakt.tv/sync/ratings", result: .success(jsonData(named: "test_add_new_ratings"))) let expectation = XCTestExpectation(description: "Add rating") - try! traktManager.addRatings(movies: [RatingId(trakt: 12345, rating: 10, ratedAt: Date())]) { result in + try traktManager.addRatings(movies: [RatingId(trakt: 12345, rating: 10, ratedAt: Date())]) { result in if case .success(let result) = result { XCTAssertEqual(result.added.movies, 1) XCTAssertEqual(result.added.shows, 1) @@ -384,8 +361,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/ratings") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -396,11 +372,11 @@ class SyncTests: XCTestCase { // MARK: - Remove Ratings - func test_remove_ratings() { - session.nextData = jsonData(named: "test_remove_ratings") + func test_remove_ratings() throws { + try mock(.GET, "https://api.trakt.tv/sync/ratings/remove", result: .success(jsonData(named: "test_remove_ratings"))) let expectation = XCTestExpectation(description: "Remove rating") - try! traktManager.removeRatings(movies: [], shows: [], episodes: []) { result in + try traktManager.removeRatings(movies: [], shows: [], episodes: []) { result in if case .success(let result) = result { XCTAssertEqual(result.deleted.movies, 1) XCTAssertEqual(result.deleted.shows, 1) @@ -410,8 +386,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/ratings/remove") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -422,8 +397,8 @@ class SyncTests: XCTestCase { // MARK: - Get Watchlist - func test_get_watchlist() { - session.nextData = jsonData(named: "test_get_watchlist") + func test_get_watchlist() throws { + try mock(.GET, "https://api.trakt.tv/sync/watchlist/movies?extended=min", result: .success(jsonData(named: "test_get_watchlist"))) let expectation = XCTestExpectation(description: "Get watchlist") traktManager.getWatchlist(watchType: .Movies) { result in @@ -433,8 +408,7 @@ class SyncTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watchlist/movies?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -445,19 +419,20 @@ class SyncTests: XCTestCase { // MARK: - Add to Watchlist - func test_add_items_to_watchlist() { - session.nextData = jsonData(named: "test_add_items_to_watchlist") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_items_to_watchlist() throws { + try mock(.POST, "https://api.trakt.tv/sync/watchlist", result: .success(jsonData(named: "test_add_items_to_watchlist"))) let expectation = XCTestExpectation(description: "Add items to watchlist") - try! traktManager.addToWatchlist(movies: [], shows: [], episodes: []) { result in - if case .success = result { + try traktManager.addToWatchlist(movies: [], shows: [], episodes: []) { result in + switch result { + case .success: expectation.fulfill() + case .error(let error): + XCTFail("Failed to add to watchlist: \(String(describing: error))") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watchlist") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -468,18 +443,17 @@ class SyncTests: XCTestCase { // MARK: - Remove from Watchlist - func test_remove_items_from_watchlist() { - session.nextData = jsonData(named: "test_remove_items_from_watchlist") + func test_remove_items_from_watchlist() throws { + try mock(.GET, "https://api.trakt.tv/sync/watchlist/remove", result: .success(jsonData(named: "test_remove_items_from_watchlist"))) let expectation = XCTestExpectation(description: "Remove items from watchlist") - try! traktManager.removeFromWatchlist(movies: [], shows: [], episodes: []) { result in + try traktManager.removeFromWatchlist(movies: [], shows: [], episodes: []) { result in if case .success = result { expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/sync/watchlist/remove") - + switch result { case .timedOut: XCTFail("Something isn't working") diff --git a/Tests/TraktKitTests/TestTraktManager.swift b/Tests/TraktKitTests/TestTraktManager.swift deleted file mode 100644 index fc878ae..0000000 --- a/Tests/TraktKitTests/TestTraktManager.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TestTraktManager.swift -// TraktKitTests -// -// Created by Maximilian Litteral on 1/12/19. -// Copyright © 2019 Maximilian Litteral. All rights reserved. -// - -@testable import TraktKit - -final class TestTraktManager: TraktManager { - override init(session: URLSessionProtocol) { - super.init(session: session) - self.set(clientID: "", clientSecret: "", redirectURI: "", staging: false) - } -} diff --git a/Tests/TraktKitTests/TraktManagerTests.swift b/Tests/TraktKitTests/TraktManagerTests.swift new file mode 100644 index 0000000..3353954 --- /dev/null +++ b/Tests/TraktKitTests/TraktManagerTests.swift @@ -0,0 +1,137 @@ +// +// TraktManagerTests.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/8/25. +// + +import Foundation +import Testing +@testable import TraktKit + +extension TraktTestSuite { + @Suite + struct TraktManagerTests { + @Test + func pollForAccessTokenInvalidDeviceCode() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/oauth/device/token", result: .success(.init()), httpCode: 404) + + let deviceCodeJSON: [String: Any] = [ + "device_code": "d9c126a7706328d808914cfd1e40274b6e009f684b1aca271b9b3f90b3630d64", + "user_code": "5055CC52", + "verification_url": "https://trakt.tv/activate", + "expires_in": 600, + "interval": 5 + ] + let deviceCode = try JSONDecoder().decode(DeviceCode.self, from: try JSONSerialization.data(withJSONObject: deviceCodeJSON)) + + await #expect(throws: TraktManager.TraktTokenError.invalidDeviceCode, performing: { + try await traktManager.pollForAccessToken(deviceCode: deviceCode) + }) + } + + @Test + func retryRequestSuccess() async throws { + let traktManager = await authenticatedTraktManager() + let urlString = "https://api.trakt.tv/retry_test" + let url = try #require(URL(string: urlString)) + try mock(.GET, urlString, result: .success(Data()), httpCode: 429, headers: [.retry(5)]) + + // Update mock in 2 seconds + Task { + try await Task.sleep(for: .seconds(2)) + try mock(.GET, urlString, result: .success(Data()), httpCode: 201, replace: true) + } + + let request = URLRequest(url: url) + let (_, response) = try await traktManager.fetchData(request: request, retryLimit: 2) + let httpHeader = try #require(response as? HTTPURLResponse) + #expect(httpHeader.statusCode == 201) + } + + @Test func retryRequestFailed() async throws { + let traktManager = await authenticatedTraktManager() + let urlString = "https://api.trakt.tv/retry_test_failed" + let url = try #require(URL(string: urlString)) + try mock(.GET, urlString, result: .success(Data()), httpCode: 429, headers: [.retry(3)]) + + // Update mock in 2 seconds + Task { + try await Task.sleep(for: .seconds(1)) + try mock(.GET, urlString, result: .success(Data()), httpCode: 405, replace: true) + } + + let request = URLRequest(url: url) + + await #expect(throws: TraktManager.TraktError.noMethodFound, performing: { + try await traktManager.fetchData(request: request, retryLimit: 2) + }) + } + + @Test func fetchPaginatedResults() async throws { + let traktManager = await authenticatedTraktManager() + + let path = "shows/paginated-results/trending" + for p in 1...2 { + let urlString = "https://api.trakt.tv/\(path)?page=\(p)&limit=10" + let json: [[String: Any]] = (0..<10).map { i in + [ + "watchers": Int.random(in: 0...100), + "show": [ + "title": "Random show \(i)", + "year": 2025, + "ids": [ + "trakt": Int.random(in: 0...1000), + "slug": "Random_show_\(i)" + ] + ] + ] + } + let data = try JSONSerialization.data(withJSONObject: json) + try mock(.GET, urlString, result: .success(data), headers: [.page(p), .pageCount(2)], replace: true) + } + + // Note: Creating a mock route to test pagination logic due to running into conflicts with other tests setting up mock data for the same endpoint. + let mockTrendingRoute: Route> = Route(path: path, method: .GET, traktManager: traktManager) + + let trending = try await mockTrendingRoute.limit(10).fetchAllPages() + #expect(trending.count == 20) + } + + @Test func streamPaginatedResults() async throws { + let traktManager = await authenticatedTraktManager() + + var watchersByPage = [[Int]]() + let path = "shows/paged-results/trending" + for p in 1...2 { + let urlString = "https://api.trakt.tv/\(path)?page=\(p)&limit=10" + let json: [[String: Any]] = (0..<10).map { i in + [ + "watchers": Int.random(in: 0...100), + "show": [ + "title": "Random show \(i)", + "year": 2025, + "ids": [ + "trakt": Int.random(in: 0...1000), + "slug": "Random_show_\(i)" + ] + ] + ] + } + watchersByPage.append(json.map { $0["watchers"] as? Int ?? 0 }) + let data = try JSONSerialization.data(withJSONObject: json) + try mock(.GET, urlString, result: .success(data), headers: [.page(p), .pageCount(2)], replace: true) + } + + // Note: Creating a mock route to test pagination logic due to running into conflicts with other tests setting up mock data for the same endpoint. + let mockTrendingRoute: Route> = Route(path: path, method: .GET, traktManager: traktManager) + + for try await page in mockTrendingRoute.limit(10).pagedResults() { + let watchersForPage = watchersByPage.removeFirst() + #expect(page.map { $0.watchers } == watchersForPage) + #expect(page.count == 10) + } + } + } +} diff --git a/Tests/TraktKitTests/TraktTestCase.swift b/Tests/TraktKitTests/TraktTestCase.swift new file mode 100644 index 0000000..31a7a06 --- /dev/null +++ b/Tests/TraktKitTests/TraktTestCase.swift @@ -0,0 +1,32 @@ +// +// TraktTestCase.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/16/25. +// + +import XCTest +@testable import TraktKit + +class TraktTestCase: XCTestCase { + lazy var traktManager = TraktManager( + session: URLSession.mockedResponsesOnly, + clientId: "", + clientSecret: "", + redirectURI: "", + authStorage: TraktMockAuthStorage(accessToken: "", refreshToken: "", expirationDate: .distantFuture) + ) + + override func setUp() async throws { + try await traktManager.refreshCurrentAuthState() + } + + override func tearDown() { + RequestMocking.removeAllMocks() + } + + func mock(_ method: TraktKit.Method, _ urlString: String, result: Result, httpCode: Int? = nil, headers: [HTTPHeader] = [.contentType, .apiVersion, .apiKey("")]) throws { + let mock = try RequestMocking.MockedResponse(urlString: urlString, result: result, httpCode: httpCode ?? method.expectedResult, headers: headers) + RequestMocking.add(mock: mock) + } +} diff --git a/Tests/TraktKitTests/TraktTestSuite.swift b/Tests/TraktKitTests/TraktTestSuite.swift new file mode 100644 index 0000000..d9610d6 --- /dev/null +++ b/Tests/TraktKitTests/TraktTestSuite.swift @@ -0,0 +1,36 @@ +// +// TraktTestSuite.swift +// TraktKit +// +// Created by Maximilian Litteral on 2/22/25. +// + +import Testing +import Foundation +@testable import TraktKit + +@Suite(.serialized) +final class TraktTestSuite { + deinit { + RequestMocking.removeAllMocks() + } + + static func authenticatedTraktManager() async -> TraktManager { + await TraktManager( + session: URLSession.mockedResponsesOnly, + clientId: "", + clientSecret: "", + redirectURI: "", + authStorage: TraktMockAuthStorage(accessToken: "", refreshToken: "", expirationDate: .distantFuture) + ) + } + + static func mock(_ method: TraktKit.Method, _ urlString: String, result: Result, httpCode: Int? = nil, headers: [HTTPHeader] = [.contentType, .apiVersion, .apiKey("")], replace: Bool = false) throws { + let mock = try RequestMocking.MockedResponse(urlString: urlString, result: result, httpCode: httpCode ?? method.expectedResult, headers: headers) + if replace { + RequestMocking.replace(mock: mock) + } else { + RequestMocking.add(mock: mock) + } + } +} diff --git a/Tests/TraktKitTests/UserTests+Async.swift b/Tests/TraktKitTests/UserTests+Async.swift new file mode 100644 index 0000000..2b67912 --- /dev/null +++ b/Tests/TraktKitTests/UserTests+Async.swift @@ -0,0 +1,308 @@ +// +// UserTests+Async.swift +// TraktKit +// +// Created by Maximilian Litteral on 3/8/25. +// + +import Testing +@testable import TraktKit + +extension TraktTestSuite { + @Suite(.serialized) + struct UserTestSuite { + @Test func getSettings() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/settings", result: .success(jsonData(named: "test_get_settings"))) + + let settings = try await traktManager.currentUser().settings().perform() + #expect(settings.user.name == "Justin Nemeth") + #expect(settings.user.gender == "male") + #expect(settings.connections.twitter == true) + #expect(settings.connections.slack == false) + #expect(settings.limits.list.count == 2) + #expect(settings.limits.list.itemCount == 100) + } + + // MARK: - Follow requests + + @Test func getFollowingRequests() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/requests", result: .success(jsonData(named: "test_get_follow_request"))) + + let followRequests = try await traktManager.currentUser() + .getFollowerRequests() + .perform() + + #expect(followRequests.count == 1) + } + + @Test func approveFollowRequest() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/requests/123", result: .success(jsonData(named: "test_approve_follow_request"))) + + let result = try await traktManager.currentUser() + .approveFollowRequest(id: 123) + .perform() + + #expect(result.user.username == "sean") + } + + @Test func denyFollowRequest() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.DELETE, "https://api.trakt.tv/users/requests/123", result: .success(.init())) + + try await traktManager.currentUser() + .denyFollowRequest(id: 123) + .perform() + } + + @Test func getSavedFilters() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/saved_filters", result: .success(jsonData(named: "test_get_saved_filters"))) + + let filters = try await traktManager.currentUser().savedFilters().perform().object + #expect(filters.count == 4) + + let firstFilter = try #require(filters.first) + #expect(firstFilter.id == 101) + } + + // MARK: - Hidden items + + @Test func getHiddenItems() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/hidden/progress_watched?page=1&limit=10&type=show&extended=min", result: .success(jsonData(named: "test_get_hidden_items"))) + + let result = try await traktManager.currentUser() + .hiddenItems(for: HiddenItemSection.progressWatched, type: "show") + .page(1) + .limit(10) + .extend(.Min) + .perform() + .object + + #expect(result.count == 2) + } + + @Test func hideItem() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/users/hidden/calendar", result: .success(jsonData(named: "test_add_hidden_item"))) + + let result = try await traktManager.currentUser() + .hide(movies: [.init(trakt: 1234)], in: HiddenItemSection.calendar) + .perform() + + #expect(result.added.movies == 1) + #expect(result.added.shows == 2) + #expect(result.added.seasons == 2) + } + + @Test func unhideItem() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/hidden/calendar/remove", result: .success(jsonData(named: "test_post_remove_hidden_items"))) + + let result = try await traktManager.currentUser() + .unhide(movies: [.init(trakt: 1234)], in: HiddenItemSection.calendar) + .perform() + #expect(result.deleted.movies == 1) + #expect(result.deleted.shows == 2) + #expect(result.deleted.seasons == 2) + } + + // MARK: - Profile + + @Test func getMinProfile() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/me?extended=min", result: .success(jsonData(named: "test_get_min_profile"))) + + let profile = try await traktManager.currentUser() + .profile() + .extend(.Min) + .perform() + + #expect(profile.username == "sean") + #expect(profile.isPrivate == false) + #expect(profile.isVIP == true) + #expect(profile.isVIPEP == true) + #expect(profile.name == "Sean Rudford") + } + + @Test func getFullProfile() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/me?extended=full", result: .success(jsonData(named: "test_get_full_profile")), replace: true) + + let profile = try await traktManager.currentUser() + .profile() + .extend(.Full) + .perform() + + #expect(profile.username == "sean") + #expect(profile.isPrivate == false) + #expect(profile.isVIP == true) + #expect(profile.isVIPEP == true) + #expect(profile.name == "Sean Rudford") + #expect(profile.joinedAt != nil) + #expect(profile.age == 35) + #expect(profile.about == "I have all your cassette tapes.") + #expect(profile.location == "SF") + #expect(profile.gender == "male") + } + + @Test func getVIPProfile() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/me?extended=full", result: .success(jsonData(named: "test_get_VIP_profile")), replace: true) + + let profile = try await traktManager.currentUser() + .profile() + .extend(.Full) + .perform() + + #expect(profile.username == "sean") + #expect(profile.isPrivate == false) + #expect(profile.isVIP == true) + #expect(profile.isVIPEP == true) + #expect(profile.name == "Sean Rudford") + #expect(profile.vipYears == 5) + #expect(profile.vipOG == true) + } + + // MARK: - Collection + + @Test func getCollection() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/me/collection/shows", result: .success(jsonData(named: "test_get_user_collection"))) + + let collection = try await traktManager.user("me") + .collection(type: "shows") + .perform() + + let shows = collection.map { $0.show } + let seasons = collection.map { $0.seasons } + #expect(shows.count == 2) + #expect(seasons.count == 2) + + let firstItemMetadata = try #require(collection.first(where: { $0.show?.ids.trakt == 245 })?.seasons?.first?.episodes.first?.metadata) + + #expect(firstItemMetadata.mediaType == .bluray) + #expect(firstItemMetadata.resolution == nil) + #expect(firstItemMetadata.hdr == nil) + #expect(firstItemMetadata.audio == .dtsHDMA) + #expect(firstItemMetadata.audioChannels == nil) + #expect(firstItemMetadata.is3D == false) + } + + // MARK: - Comments + + @Test func getComments() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/sean/comments", result: .success(jsonData(named: "test_get_user_comments"))) + + let comments = try await traktManager.user("sean") + .comments() + .perform() + .object + + #expect(comments.count == 5) + } + + // MARK: - List + + @Test func getCustomList() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order", result: .success(jsonData(named: "test_get_custom_list"))) + + let customList = try await traktManager.currentUser().personalList("star-wars-in-machete-order").perform() + #expect(customList.name == "Star Wars in machete order") + #expect(customList.description != nil) + } + + @Test func getListItems() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items", result: .success(jsonData(named: "test_get_items_on_custom_list"))) + + let listItems = try await traktManager.user("sean").itemsOnList("star-wars-in-machete-order").perform().object + #expect(listItems.count == 5) + } + + @Test func addItemsToList() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.POST, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order/items", result: .success(jsonData(named: "test_add_item_to_custom_list"))) + + // For the test we don't need to add any specific Ids. + let response = try await traktManager.currentUser().addItemsToList("star-wars-in-machete-order", movies: []).perform() + #expect(response.added.seasons == 1) + #expect(response.added.people == 1) + #expect(response.added.movies == 1) + #expect(response.added.shows == 1) + #expect(response.added.episodes == 2) + + #expect(response.existing.seasons == 0) + #expect(response.existing.episodes == 0) + #expect(response.existing.movies == 0) + #expect(response.existing.shows == 0) + #expect(response.existing.episodes == 0) + + #expect(response.notFound.movies.count == 1) + } + + @Test func removeItemsFromList() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.DELETE, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order/items/remove", result: .success(jsonData(named: "test_remove_item_from_custom_list"))) + + // For the test we don't need to add any specific Ids. + let response = try await traktManager.currentUser().removeItemsFromList("star-wars-in-machete-order", movies: []).perform() + #expect(response.deleted.seasons == 1) + #expect(response.deleted.people == 1) + #expect(response.deleted.movies == 1) + #expect(response.deleted.shows == 1) + #expect(response.deleted.episodes == 2) + + #expect(response.notFound.movies.count == 1) + } + + // MARK: - History + + @Test func getWatchedHistory() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/sean/history?extended=min", result: .success(jsonData(named: "test_get_user_watched_history"))) + + let history = try await traktManager.user("sean") + .watchedHistory() + .extend(.Min) + .perform() + .object + + #expect(history.count == 3) + } + + // MARK: - Ratings + + @Test func getRatings() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/sean/ratings", result: .success(jsonData(named: "test_get_user_ratings"))) + + let ratings = try await traktManager.user("sean") + .ratings() + .perform() + .object + + #expect(ratings.count == 2) + } + + // MARK: - Watchlist + + @Test func getWatchlist() async throws { + let traktManager = await authenticatedTraktManager() + try mock(.GET, "https://api.trakt.tv/users/sean/watchlist/movies?extended=min", result: .success(jsonData(named: "test_get_user_watchlist"))) + + let watchlist = try await traktManager.user("sean") + .watchlist(type: "movies") + .extend(.Min) + .perform() + .object + + #expect(watchlist.count == 5) + } + } +} diff --git a/Tests/TraktKitTests/UserTests.swift b/Tests/TraktKitTests/UserTests.swift index 0ec355c..d69fde2 100644 --- a/Tests/TraktKitTests/UserTests.swift +++ b/Tests/TraktKitTests/UserTests.swift @@ -10,50 +10,26 @@ import XCTest import Foundation @testable import TraktKit -class UserTests: XCTestCase { - - let session = MockURLSession() - lazy var traktManager = TestTraktManager(session: session) - - override func tearDown() { - super.tearDown() - session.nextData = nil - session.nextStatusCode = StatusCodes.Success - session.nextError = nil - } +final class UserTests: TraktTestCase { // MARK: - Settings - func test_get_settings() { - session.nextData = jsonData(named: "test_get_settings") - - let expectation = XCTestExpectation(description: "settings") - traktManager.getSettings { result in - if case .success(let accountSessings) = result { - XCTAssertEqual(accountSessings.user.name, "Justin Nemeth") - XCTAssertEqual(accountSessings.user.gender, "male") - XCTAssertEqual(accountSessings.connections.twitter, true) - XCTAssertEqual(accountSessings.connections.slack, false) - expectation.fulfill() - } - } - - let result = XCTWaiter().wait(for: [expectation], timeout: 1) + func test_get_settings() async throws { + try mock(.GET, "https://api.trakt.tv/users/settings", result: .success(jsonData(named: "test_get_settings"))) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/settings") - - switch result { - case .timedOut: - XCTFail("Something isn't working") - default: - break - } + let settings = try await traktManager.currentUser().settings().perform() + XCTAssertEqual(settings.user.name, "Justin Nemeth") + XCTAssertEqual(settings.user.gender, "male") + XCTAssertEqual(settings.connections.twitter, true) + XCTAssertEqual(settings.connections.slack, false) + XCTAssertEqual(settings.limits.list.count, 2) + XCTAssertEqual(settings.limits.list.itemCount, 100) } // MARK: - Follower requests - func test_get_follow_request() { - session.nextData = jsonData(named: "test_get_follow_request") + func test_get_follow_request() throws { + try mock(.GET, "https://api.trakt.tv/users/requests", result: .success(jsonData(named: "test_get_follow_request"))) let expectation = XCTestExpectation(description: "FollowRequest") traktManager.getFollowRequests { result in @@ -64,8 +40,7 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/requests") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -76,8 +51,8 @@ class UserTests: XCTestCase { // MARK: - Approve or deny follower requests - func test_approve_follow_request() { - session.nextData = jsonData(named: "test_approve_follow_request") + func test_approve_follow_request() throws { + try mock(.GET, "https://api.trakt.tv/users/requests/123", result: .success(jsonData(named: "test_approve_follow_request"))) let expectation = XCTestExpectation(description: "Approve follow request") traktManager.approveFollowRequest(requestID: 123) { result in @@ -88,8 +63,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/requests/123") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -98,9 +71,8 @@ class UserTests: XCTestCase { } } - func test_deny_follow_request() { - session.nextData = nil - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_deny_follow_request() throws { + try mock(.DELETE, "https://api.trakt.tv/users/requests/123", result: .success(.init())) let expectation = XCTestExpectation(description: "Deny follow request") traktManager.denyFollowRequest(requestID: 123) { result in @@ -110,8 +82,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/requests/123") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -120,25 +90,37 @@ class UserTests: XCTestCase { } } + func test_deny_follow_request_async() async throws { + try mock(.DELETE, "https://api.trakt.tv/users/requests/123", result: .success(.init())) + + try await traktManager.currentUser().denyFollowRequest(id: 123).perform() + } + + // MARK: - Saved filters + + func test_get_saved_filters() async throws { + try mock(.GET, "https://api.trakt.tv/users/saved_filters", result: .success(jsonData(named: "test_get_saved_filters"))) + + let filters = try await traktManager.currentUser().savedFilters().perform().object + XCTAssertEqual(filters.count, 4) + + let firstFilter = try XCTUnwrap(filters.first) + XCTAssertEqual(firstFilter.id, 101) + } + // MARK: - Hidden items - func test_get_hidden_items() { - session.nextData = jsonData(named: "test_get_hidden_items") - session.nextStatusCode = StatusCodes.Success + func test_get_hidden_items() throws { + try mock(.GET, "https://api.trakt.tv/users/hidden/progress_watched?page=1&limit=10&type=show&extended=min", result: .success(jsonData(named: "test_get_hidden_items"))) let expectation = XCTestExpectation(description: "HiddenItems") - traktManager.hiddenItems(section: .ProgressWatched, type: .Show, pagination: Pagination(page: 1, limit: 10)) { result in + traktManager.hiddenItems(section: HiddenItemSection.progressWatched, type: .Show, pagination: Pagination(page: 1, limit: 10)) { result in if case .success(let hiddenShows, _, _) = result { XCTAssertEqual(hiddenShows.count, 2) expectation.fulfill() } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.path, "/users/hidden/progress_watched") - XCTAssertTrue(session.lastURL?.query?.contains("page=1") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("limit=10") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("type=show") ?? false) - XCTAssertTrue(session.lastURL?.query?.contains("extended=min") ?? false) switch result { case .timedOut: XCTFail("Something isn't working") @@ -149,12 +131,11 @@ class UserTests: XCTestCase { // MARK: - Add hidden item - func test_add_hidden_item() { - session.nextData = jsonData(named: "test_add_hidden_item") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_hidden_item() throws { + try mock(.POST, "https://api.trakt.tv/users/hidden/calendar", result: .success(jsonData(named: "test_add_hidden_item"))) let expectation = XCTestExpectation(description: "Add hidden item") - try! traktManager.hide(from: .Calendar) { result in + try traktManager.hide(from: HiddenItemSection.calendar) { result in if case .success(let result) = result { XCTAssertEqual(result.added.movies, 1) XCTAssertEqual(result.added.shows, 2) @@ -163,8 +144,7 @@ class UserTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/hidden/calendar") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -175,12 +155,11 @@ class UserTests: XCTestCase { // MARK: - Remove hidden item - func test_post_remove_hidden_items() { - session.nextData = jsonData(named: "test_post_remove_hidden_items") - session.nextStatusCode = StatusCodes.Success + func test_post_remove_hidden_items() throws { + try mock(.GET, "https://api.trakt.tv/users/hidden/calendar/remove", result: .success(jsonData(named: "test_post_remove_hidden_items"))) let expectation = XCTestExpectation(description: "Remove hidden items") - try! traktManager.unhide(from: .Calendar) { result in + try traktManager.unhide(from: HiddenItemSection.calendar) { result in if case .success(let result) = result { XCTAssertEqual(result.deleted.movies, 1) XCTAssertEqual(result.deleted.shows, 2) @@ -189,7 +168,6 @@ class UserTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/hidden/calendar/remove") switch result { case .timedOut: @@ -201,8 +179,8 @@ class UserTests: XCTestCase { // MARK: - Likes - func test_get_comments_likes() { - session.nextData = jsonData(named: "test_get_comments_likes") + func test_get_comments_likes() throws { + try mock(.GET, "https://api.trakt.tv/users/likes/comments", result: .success(jsonData(named: "test_get_comments_likes"))) let expectation = XCTestExpectation(description: "Comments likes") traktManager.getLikes(type: .Comments) { result in @@ -217,8 +195,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/likes/comments") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -227,8 +203,8 @@ class UserTests: XCTestCase { } } - func test_get_lists_likes() { - session.nextData = jsonData(named: "test_get_lists_likes") + func test_get_lists_likes() throws { + try mock(.GET, "https://api.trakt.tv/users/likes/lists", result: .success(jsonData(named: "test_get_lists_likes"))) let expectation = XCTestExpectation(description: "Lists likes") traktManager.getLikes(type: .Lists) { result in @@ -243,8 +219,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/likes/lists") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -255,8 +229,8 @@ class UserTests: XCTestCase { // MARK: - Profile - func test_get_min_profile() { - session.nextData = jsonData(named: "test_get_min_profile") + func test_get_min_profile() throws { + try mock(.GET, "https://api.trakt.tv/users/me?extended=min", result: .success(jsonData(named: "test_get_min_profile"))) let expectation = XCTestExpectation(description: "User Profile") traktManager.getUserProfile { result in @@ -271,8 +245,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me?extended=min") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -281,8 +253,8 @@ class UserTests: XCTestCase { } } - func test_get_full_profile() { - session.nextData = jsonData(named: "test_get_full_profile") + func test_get_full_profile() throws { + try mock(.GET, "https://api.trakt.tv/users/me?extended=full", result: .success(jsonData(named: "test_get_full_profile"))) let expectation = XCTestExpectation(description: "User Profile") traktManager.getUserProfile(extended: [.Full]) { result in @@ -302,8 +274,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me?extended=full") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -312,8 +282,8 @@ class UserTests: XCTestCase { } } - func test_get_VIP_profile() { - session.nextData = jsonData(named: "test_get_VIP_profile") + func test_get_VIP_profile() throws { + try mock(.GET, "https://api.trakt.tv/users/me?extended=full", result: .success(jsonData(named: "test_get_VIP_profile"))) let expectation = XCTestExpectation(description: "User Profile") traktManager.getUserProfile(extended: [.Full]) { result in @@ -330,8 +300,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me?extended=full") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -342,8 +310,8 @@ class UserTests: XCTestCase { // MARK: - Collection - func test_get_user_collection() { - session.nextData = jsonData(named: "test_get_user_collection") + func test_get_user_collection() throws { + try mock(.GET, "https://api.trakt.tv/users/me/collection/shows", result: .success(jsonData(named: "test_get_user_collection"))) let expectation = XCTestExpectation(description: "Get User Collection") traktManager.getUserCollection(type: .shows) { result in @@ -369,8 +337,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/collection/shows") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -381,8 +347,8 @@ class UserTests: XCTestCase { // MARK: - Comments - func test_get_user_comments() { - session.nextData = jsonData(named: "test_get_user_comments") + func test_get_user_comments() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/comments", result: .success(jsonData(named: "test_get_user_comments"))) let expectation = XCTestExpectation(description: "User Commets") traktManager.getUserComments(username: "sean") { result in @@ -393,8 +359,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/comments") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -405,8 +369,8 @@ class UserTests: XCTestCase { // MARK: - Lists - func test_get_custom_lists() { - session.nextData = jsonData(named: "test_get_custom_lists") + func test_get_custom_lists() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/lists", result: .success(jsonData(named: "test_get_custom_lists"))) let expectation = XCTestExpectation(description: "User Custom Lists") traktManager.getCustomLists(username: "sean") { result in @@ -417,8 +381,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -427,15 +389,14 @@ class UserTests: XCTestCase { } } - func test_post_custom_list() { - session.nextData = jsonData(named: "test_post_custom_list") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_post_custom_list() throws { + try mock(.POST, "https://api.trakt.tv/users/me/lists", result: .success(jsonData(named: "test_post_custom_list"))) let expectation = XCTestExpectation(description: "User create custom lists") let listName = "Star Wars in machete order" let listDescription = "Next time you want to introduce someone to Star Wars for the first time, watch the films with them in this order: IV, V, II, III, VI." - try! traktManager.createCustomList(listName: "listName", listDescription: listDescription) { result in + try traktManager.createCustomList(listName: "listName", listDescription: listDescription) { result in if case .success(let newList) = result { XCTAssertEqual(newList.name, listName) XCTAssertEqual(newList.description, listDescription) @@ -444,8 +405,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/lists") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -456,11 +415,10 @@ class UserTests: XCTestCase { // MARK: - List - func test_get_custom_list() { - session.nextData = jsonData(named: "test_get_custom_list") + func test_get_custom_list() throws { + try mock(.GET, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order", result: .success(jsonData(named: "test_get_custom_list"))) let expectation = XCTestExpectation(description: "User create custom list") - traktManager.getCustomList(listID: "star-wars-in-machete-order") { result in if case .success(let list) = result { XCTAssertEqual(list.name, "Star Wars in machete order") @@ -470,8 +428,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -480,11 +436,11 @@ class UserTests: XCTestCase { } } - func test_update_custom_list() { - session.nextData = jsonData(named: "test_update_custom_list") + func test_update_custom_list() throws { + try mock(.GET, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order", result: .success(jsonData(named: "test_update_custom_list"))) let expectation = XCTestExpectation(description: "User update custom list") - try! traktManager.updateCustomList(listID: "star-wars-in-machete-order", listName: "Star Wars in NEW machete order", privacy: "private", displayNumbers: false) { result in + try traktManager.updateCustomList(listID: "star-wars-in-machete-order", listName: "Star Wars in NEW machete order", privacy: "private", displayNumbers: false) { result in if case .success(let list) = result { XCTAssertEqual(list.name, "Star Wars in NEW machete order") XCTAssertEqual(list.privacy, .private) @@ -493,8 +449,7 @@ class UserTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -503,8 +458,8 @@ class UserTests: XCTestCase { } } - func test_delete_custom_list() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_delete_custom_list() throws { + try mock(.DELETE, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order", result: .success(.init())) let expectation = XCTestExpectation(description: "User delete custom list") traktManager.deleteCustomList(listID: "star-wars-in-machete-order") { result in @@ -513,7 +468,6 @@ class UserTests: XCTestCase { } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/lists/star-wars-in-machete-order") switch result { case .timedOut: @@ -525,11 +479,10 @@ class UserTests: XCTestCase { // MARK: - List like - func test_like_list() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_like_list() throws { + try mock(.POST, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/like", result: .success(.init())) let expectation = XCTestExpectation(description: "Like a list") - traktManager.likeList(username: "sean", listID: "star-wars-in-machete-order") { result in if case .success = result { expectation.fulfill() @@ -537,8 +490,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/like") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -547,11 +498,10 @@ class UserTests: XCTestCase { } } - func test_remove_like_from_list() { - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_remove_like_from_list() throws { + try mock(.DELETE, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/like", result: .success(.init())) let expectation = XCTestExpectation(description: "Like a list") - traktManager.removeListLike(username: "sean", listID: "star-wars-in-machete-order") { result in if case .success = result { expectation.fulfill() @@ -559,8 +509,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/like") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -571,20 +519,22 @@ class UserTests: XCTestCase { // MARK: - List items - func test_get_items_on_custom_list() { - session.nextData = jsonData(named: "test_get_items_on_custom_list") + func test_get_items_on_custom_list() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items?extended=min", result: .success(jsonData(named: "test_get_items_on_custom_list"))) let expectation = XCTestExpectation(description: "Get custom list items") traktManager.getItemsForCustomList(username: "sean", listID: "star-wars-in-machete-order") { result in - if case .success(let listItems) = result { + switch result { + case .success(let listItems): XCTAssertEqual(listItems.count, 5) expectation.fulfill() + case .error(let error): + XCTFail("Failed to get items on custom list: \(String(describing: error))") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items?extended=min") - + switch result { case .timedOut: XCTFail("Something isn't working") @@ -595,12 +545,11 @@ class UserTests: XCTestCase { // MARK: - Add list items - func test_add_item_to_custom_list() { - session.nextData = jsonData(named: "test_add_item_to_custom_list") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_add_item_to_custom_list() throws { + try mock(.POST, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items", result: .success(jsonData(named: "test_add_item_to_custom_list"))) let expectation = XCTestExpectation(description: "Add item to custom list") - try! traktManager.addItemToCustomList(username: "sean", listID: "star-wars-in-machete-order", movies: [], shows: [], episodes: []) { result in + try traktManager.addItemToCustomList(username: "sean", listID: "star-wars-in-machete-order", movies: [], shows: [], episodes: []) { result in if case .success(let response) = result { XCTAssertEqual(response.added.seasons, 1) XCTAssertEqual(response.added.people, 1) @@ -620,8 +569,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -632,12 +579,11 @@ class UserTests: XCTestCase { // MARK: - Remove list items - func test_remove_item_from_custom_list() { - session.nextData = jsonData(named: "test_remove_item_from_custom_list") - session.nextStatusCode = StatusCodes.Success + func test_remove_item_from_custom_list() throws { + try mock(.DELETE, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items/remove", result: .success(jsonData(named: "test_remove_item_from_custom_list"))) let expectation = XCTestExpectation(description: "Remove item to custom list") - try! traktManager.removeItemFromCustomList(username: "sean", listID: "star-wars-in-machete-order", movies: [], shows: [], episodes: []) { result in + try traktManager.removeItemFromCustomList(username: "sean", listID: "star-wars-in-machete-order", movies: [], shows: [], episodes: []) { result in if case .success(let response) = result { XCTAssertEqual(response.deleted.seasons, 1) XCTAssertEqual(response.deleted.people, 1) @@ -651,8 +597,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/items/remove") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -663,9 +607,8 @@ class UserTests: XCTestCase { // MARK: - List comments - func test_get_all_list_comments() { - session.nextData = jsonData(named: "test_get_all_list_comments") - session.nextStatusCode = StatusCodes.Success + func test_get_all_list_comments() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/comments", result: .success(jsonData(named: "test_get_all_list_comments"))) let expectation = XCTestExpectation(description: "List comments") traktManager.getUserAllListComments(username: "sean", listID: "star-wars-in-machete-order") { result in @@ -682,8 +625,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/lists/star-wars-in-machete-order/comments") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -694,9 +635,8 @@ class UserTests: XCTestCase { // MARK: - Follow - func test_follow_user() { - session.nextData = jsonData(named: "test_follow_user") - session.nextStatusCode = StatusCodes.SuccessNewResourceCreated + func test_follow_user() throws { + try mock(.POST, "https://api.trakt.tv/users/sean/follow", result: .success(jsonData(named: "test_follow_user"))) let expectation = XCTestExpectation(description: "Follow user") traktManager.followUser(username: "sean") { result in @@ -707,8 +647,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/follow") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -717,9 +655,8 @@ class UserTests: XCTestCase { } } - func test_unfollow_user() { - session.nextData = nil - session.nextStatusCode = StatusCodes.SuccessNoContentToReturn + func test_unfollow_user() throws { + try mock(.DELETE, "https://api.trakt.tv/users/sean/follow", result: .success(jsonData(named: "test_follow_user"))) let expectation = XCTestExpectation(description: "Unfollow user") traktManager.unfollowUser(username: "sean") { result in @@ -729,8 +666,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/follow") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -741,9 +676,8 @@ class UserTests: XCTestCase { // MARK: - Followers - func test_get_followers() { - session.nextData = jsonData(named: "test_get_followers") - session.nextStatusCode = StatusCodes.Success + func test_get_followers() throws { + try mock(.GET, "https://api.trakt.tv/users/me/followers", result: .success(jsonData(named: "test_get_followers"))) let expectation = XCTestExpectation(description: "Get followers") traktManager.getUserFollowers { result in @@ -758,8 +692,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/followers") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -770,9 +702,8 @@ class UserTests: XCTestCase { // MARK: - Following - func test_get_following() { - session.nextData = jsonData(named: "test_get_following") - session.nextStatusCode = StatusCodes.Success + func test_get_following() throws { + try mock(.GET, "https://api.trakt.tv/users/me/following", result: .success(jsonData(named: "test_get_following"))) let expectation = XCTestExpectation(description: "Get following") traktManager.getUserFollowing { result in @@ -787,8 +718,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/following") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -799,9 +728,8 @@ class UserTests: XCTestCase { // MARK: - Friends - func test_get_friends() { - session.nextData = jsonData(named: "test_get_friends") - session.nextStatusCode = StatusCodes.Success + func test_get_friends() throws { + try mock(.GET, "https://api.trakt.tv/users/me/friends", result: .success(jsonData(named: "test_get_friends"))) let expectation = XCTestExpectation(description: "Get friends") traktManager.getUserFriends { result in @@ -816,8 +744,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/friends") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -828,9 +754,8 @@ class UserTests: XCTestCase { // MARK: - History - func test_get_user_watched_history() { - session.nextData = jsonData(named: "test_get_user_watched_history") - session.nextStatusCode = StatusCodes.Success + func test_get_user_watched_history() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/history?extended=min", result: .success(jsonData(named: "test_get_user_watched_history"))) let expectation = XCTestExpectation(description: "Get user watched history") traktManager.getUserWatchedHistory(username: "sean") { result in @@ -841,8 +766,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/history?extended=min") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -853,9 +776,8 @@ class UserTests: XCTestCase { // MARK: - Ratings - func test_get_user_ratings() { - session.nextData = jsonData(named: "test_get_user_ratings") - session.nextStatusCode = StatusCodes.Success + func test_get_user_ratings() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/ratings", result: .success(jsonData(named: "test_get_user_ratings"))) let expectation = XCTestExpectation(description: "Get user ratings") traktManager.getUserRatings(username: "sean") { result in @@ -866,8 +788,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/ratings") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -878,21 +798,21 @@ class UserTests: XCTestCase { // MARK: - Watchlist - func test_get_user_watchlist() { - session.nextData = jsonData(named: "test_get_user_watchlist") - session.nextStatusCode = StatusCodes.Success + func test_get_user_watchlist() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/watchlist/movies?extended=min", result: .success(jsonData(named: "test_get_user_watchlist"))) let expectation = XCTestExpectation(description: "Get user watchlist") traktManager.getUserWatchlist(username: "sean", type: .Movies, extended: [.Min]) { result in - if case .success(let watchlist) = result { - XCTAssertEqual(watchlist.count, 2) + switch result { + case .success(let watchlist): + XCTAssertEqual(watchlist.count, 5) expectation.fulfill() + case .error(let error): + XCTFail("Failed to get user watchlist: \(String(describing: error))") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/watchlist/movies?extended=min") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -903,21 +823,35 @@ class UserTests: XCTestCase { // MARK: - Watching - func test_get_user_watching() { - session.nextData = jsonData(named: "test_get_user_watching") - session.nextStatusCode = StatusCodes.Success + func test_get_user_watching() throws { + try mock(.GET, "https://api.trakt.tv/users/sean/watching", result: .success(jsonData(named: "test_get_user_watching"))) let expectation = XCTestExpectation(description: "Get watching") traktManager.getUserWatching(username: "sean") { result in - if case .checkedIn(let watching) = result { - XCTAssertEqual(watching.action, "scrobble") - expectation.fulfill() + defer { expectation.fulfill() } + switch result { + case .success(let response): + XCTAssertTrue(response.isWatching) + switch response { + case .watching(let item): + XCTAssertEqual(item.action, "scrobble") + XCTAssertEqual(item.mediaType, .episode) + switch item.mediaItem { + case let .episode(episode, show): + XCTAssertEqual(episode.number, 2) + XCTAssertEqual(show.title, "Breaking Bad") + case .movie: + XCTFail("Media item should be episode") + } + case .notWatchingAnything: + XCTFail("User should be watching an episode") + } + case .error: + XCTFail("Mocked request should not fail") } } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/sean/watching") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -928,8 +862,8 @@ class UserTests: XCTestCase { // MARK: - Watched movies / shows - func test_get_user_watched_movies() { - session.nextData = jsonData(named: "test_get_user_watched_movies") + func test_get_user_watched_movies() throws { + try mock(.GET, "https://api.trakt.tv/users/me/watched/movies?extended=min", result: .success(jsonData(named: "test_get_user_watched_movies"))) let expectation = XCTestExpectation(description: "User Watched movies") traktManager.getUserWatched(type: .movies) { result in @@ -944,8 +878,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/watched/movies?extended=min") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -954,8 +886,8 @@ class UserTests: XCTestCase { } } - func test_get_user_watched_shows() { - session.nextData = jsonData(named: "test_get_user_watched_shows") + func test_get_user_watched_shows() throws { + try mock(.GET, "https://api.trakt.tv/users/me/watched/shows?extended=min", result: .success(jsonData(named: "test_get_user_watched_shows"))) let expectation = XCTestExpectation(description: "User Watched shows") traktManager.getUserWatched(type: .shows) { result in @@ -970,8 +902,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/watched/shows?extended=min") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -980,8 +910,8 @@ class UserTests: XCTestCase { } } - func test_get_user_watched_shows_no_seasons() { - session.nextData = jsonData(named: "test_get_user_watched_shows_no_seasons") + func test_get_user_watched_shows_no_seasons() throws { + try mock(.GET, "https://api.trakt.tv/users/me/watched/shows?extended=noseasons", result: .success(jsonData(named: "test_get_user_watched_shows_no_seasons"))) let expectation = XCTestExpectation(description: "User Watched shows without seasons") traktManager.getUserWatched(type: .shows, extended: [.noSeasons]) { result in @@ -997,8 +927,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/watched/shows?extended=noseasons") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -1009,8 +937,8 @@ class UserTests: XCTestCase { // MARK: - Stats - func test_get_user_stats() { - session.nextData = jsonData(named: "test_get_user_stats") + func test_get_user_stats() throws { + try mock(.GET, "https://api.trakt.tv/users/me/stats", result: .success(jsonData(named: "test_get_user_stats"))) let expectation = XCTestExpectation(description: "User Stats") traktManager.getUserStats { result in @@ -1057,8 +985,6 @@ class UserTests: XCTestCase { } let result = XCTWaiter().wait(for: [expectation], timeout: 1) - XCTAssertEqual(session.lastURL?.absoluteString, "https://api.trakt.tv/users/me/stats") - switch result { case .timedOut: XCTFail("Something isn't working") @@ -1066,5 +992,4 @@ class UserTests: XCTestCase { break } } - } diff --git a/Tests/TraktKitTests/update_tests.py b/Tests/TraktKitTests/update_tests.py new file mode 100644 index 0000000..9819a5f --- /dev/null +++ b/Tests/TraktKitTests/update_tests.py @@ -0,0 +1,63 @@ +import re +import sys + +def update_test_code(file_path): + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + # Matches each test function with its body + test_function_pattern = re.compile(r'(func test_[a-zA-Z0-9_]+\(\))\s*\{((?:.|\n)*?)\n\s{4}\}', re.DOTALL) + + def process_test_function(match): + func_def, body = match.groups() + print(f"body debug: {body}") + + # Skip if already updated (contains `mock(...)`) + if "mock(" in body: + return match.group(0) + + # Ensure `throws` is correctly added after the parentheses + if "throws" not in func_def: + func_def = func_def.rstrip('()') + "() throws {" + + # Extract JSON filename in this function + next_data_match = re.search(r'session\.nextData = jsonData\(named: "(.*?)"\)', body) + json_filename = next_data_match.group(1) if next_data_match else None + + # Extract URL in this function + url_match = re.search(r'XCTAssertEqual\(session\.lastURL\?\.(?:absoluteString)?, "(.*?)"\)', body) + url = url_match.group(1) if url_match else None + + print(f"json filename debug: {json_filename}") + print(f"url debug: {url}") + + # Apply transformations only if both values exist + if json_filename and url: + # Replace `session.nextData` line with `mock(...)` + body = re.sub( + r'session\.nextData = jsonData\(named: ".*?"\)', + f'try mock(.GET, "{url}", result: .success(jsonData(named: "{json_filename}")))', + body + ) + + # Remove `XCTAssertEqual(session.lastURL...)` + body = re.sub(r'XCTAssertEqual\(session\.lastURL\?\.(?:absoluteString)?, ".*?"\)\n?', '', body) + + # Add the closing bracket `}` at the end + return f"{func_def}{body}\n }}" + + # Apply transformation to all functions in the file + updated_content = test_function_pattern.sub(process_test_function, content) + + with open(file_path, "w", encoding="utf-8") as f: + f.write(updated_content) + + print(f"Updated {file_path}") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python update_tests.py ...") + sys.exit(1) + + for file_path in sys.argv[1:]: + update_test_code(file_path)