From c675c0de5c75748deefbcda41259ec543740579a Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 17 Aug 2022 14:13:15 -0600 Subject: [PATCH] add open iso8601 date formatter --- Sources/Entities/SessionInfo.swift | 12 +++++++- Sources/Entities/UserDto.swift | 12 +++++++- Sources/JellyfinClient.swift | 13 +++++++-- Sources/OpenISO8601Formatter.swift | 45 ++++++++++++++++++++++++++++++ Sources/create-api-config.yaml | 4 --- 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 Sources/OpenISO8601Formatter.swift diff --git a/Sources/Entities/SessionInfo.swift b/Sources/Entities/SessionInfo.swift index 6b636ec0..989f06d5 100644 --- a/Sources/Entities/SessionInfo.swift +++ b/Sources/Entities/SessionInfo.swift @@ -29,6 +29,10 @@ public struct SessionInfo: Codable, Identifiable { public var id: String? /// Gets a value indicating whether this instance is active. public var isActive: Bool? + /// Gets or sets the last activity date. + public var lastActivityDate: Date? + /// Gets or sets the last playback check in. + public var lastPlaybackCheckIn: Date? /// This is strictly used as a data transfer object from the api layer. /// /// This holds information about a BaseItem in a format that is convenient for the client. @@ -57,7 +61,7 @@ public struct SessionInfo: Codable, Identifiable { public var userName: String? public var userPrimaryImageTag: String? - public init(additionalUsers: [SessionUserInfo]? = nil, applicationVersion: String? = nil, capabilities: ClientCapabilities? = nil, client: String? = nil, deviceID: String? = nil, deviceName: String? = nil, deviceType: String? = nil, fullNowPlayingItem: BaseItem? = nil, hasCustomDeviceName: Bool? = nil, id: String? = nil, isActive: Bool? = nil, nowPlayingItem: BaseItemDto? = nil, nowPlayingQueue: [QueueItem]? = nil, nowPlayingQueueFullItems: [BaseItemDto]? = nil, nowViewingItem: BaseItemDto? = nil, playState: PlayerStateInfo? = nil, playableMediaTypes: [String]? = nil, playlistItemID: String? = nil, remoteEndPoint: String? = nil, serverID: String? = nil, supportedCommands: [GeneralCommandType]? = nil, isSupportsMediaControl: Bool? = nil, isSupportsRemoteControl: Bool? = nil, transcodingInfo: TranscodingInfo? = nil, userID: String? = nil, userName: String? = nil, userPrimaryImageTag: String? = nil) { + public init(additionalUsers: [SessionUserInfo]? = nil, applicationVersion: String? = nil, capabilities: ClientCapabilities? = nil, client: String? = nil, deviceID: String? = nil, deviceName: String? = nil, deviceType: String? = nil, fullNowPlayingItem: BaseItem? = nil, hasCustomDeviceName: Bool? = nil, id: String? = nil, isActive: Bool? = nil, lastActivityDate: Date? = nil, lastPlaybackCheckIn: Date? = nil, nowPlayingItem: BaseItemDto? = nil, nowPlayingQueue: [QueueItem]? = nil, nowPlayingQueueFullItems: [BaseItemDto]? = nil, nowViewingItem: BaseItemDto? = nil, playState: PlayerStateInfo? = nil, playableMediaTypes: [String]? = nil, playlistItemID: String? = nil, remoteEndPoint: String? = nil, serverID: String? = nil, supportedCommands: [GeneralCommandType]? = nil, isSupportsMediaControl: Bool? = nil, isSupportsRemoteControl: Bool? = nil, transcodingInfo: TranscodingInfo? = nil, userID: String? = nil, userName: String? = nil, userPrimaryImageTag: String? = nil) { self.additionalUsers = additionalUsers self.applicationVersion = applicationVersion self.capabilities = capabilities @@ -69,6 +73,8 @@ public struct SessionInfo: Codable, Identifiable { self.hasCustomDeviceName = hasCustomDeviceName self.id = id self.isActive = isActive + self.lastActivityDate = lastActivityDate + self.lastPlaybackCheckIn = lastPlaybackCheckIn self.nowPlayingItem = nowPlayingItem self.nowPlayingQueue = nowPlayingQueue self.nowPlayingQueueFullItems = nowPlayingQueueFullItems @@ -100,6 +106,8 @@ public struct SessionInfo: Codable, Identifiable { self.hasCustomDeviceName = try values.decodeIfPresent(Bool.self, forKey: "HasCustomDeviceName") self.id = try values.decodeIfPresent(String.self, forKey: "Id") self.isActive = try values.decodeIfPresent(Bool.self, forKey: "IsActive") + self.lastActivityDate = try values.decodeIfPresent(Date.self, forKey: "LastActivityDate") + self.lastPlaybackCheckIn = try values.decodeIfPresent(Date.self, forKey: "LastPlaybackCheckIn") self.nowPlayingItem = try values.decodeIfPresent(BaseItemDto.self, forKey: "NowPlayingItem") self.nowPlayingQueue = try values.decodeIfPresent([QueueItem].self, forKey: "NowPlayingQueue") self.nowPlayingQueueFullItems = try values.decodeIfPresent([BaseItemDto].self, forKey: "NowPlayingQueueFullItems") @@ -131,6 +139,8 @@ public struct SessionInfo: Codable, Identifiable { try values.encodeIfPresent(hasCustomDeviceName, forKey: "HasCustomDeviceName") try values.encodeIfPresent(id, forKey: "Id") try values.encodeIfPresent(isActive, forKey: "IsActive") + try values.encodeIfPresent(lastActivityDate, forKey: "LastActivityDate") + try values.encodeIfPresent(lastPlaybackCheckIn, forKey: "LastPlaybackCheckIn") try values.encodeIfPresent(nowPlayingItem, forKey: "NowPlayingItem") try values.encodeIfPresent(nowPlayingQueue, forKey: "NowPlayingQueue") try values.encodeIfPresent(nowPlayingQueueFullItems, forKey: "NowPlayingQueueFullItems") diff --git a/Sources/Entities/UserDto.swift b/Sources/Entities/UserDto.swift index f4445cc7..14299791 100644 --- a/Sources/Entities/UserDto.swift +++ b/Sources/Entities/UserDto.swift @@ -22,6 +22,10 @@ public struct UserDto: Codable, Identifiable { public var hasPassword: Bool? /// Gets or sets the id. public var id: String? + /// Gets or sets the last activity date. + public var lastActivityDate: Date? + /// Gets or sets the last login date. + public var lastLoginDate: Date? /// Gets or sets the name. public var name: String? /// Gets or sets the policy. @@ -37,13 +41,15 @@ public struct UserDto: Codable, Identifiable { /// This is not used by the server and is for client-side usage only. public var serverName: String? - public init(configuration: UserConfiguration? = nil, enableAutoLogin: Bool? = nil, hasConfiguredEasyPassword: Bool? = nil, hasConfiguredPassword: Bool? = nil, hasPassword: Bool? = nil, id: String? = nil, name: String? = nil, policy: UserPolicy? = nil, primaryImageAspectRatio: Double? = nil, primaryImageTag: String? = nil, serverID: String? = nil, serverName: String? = nil) { + public init(configuration: UserConfiguration? = nil, enableAutoLogin: Bool? = nil, hasConfiguredEasyPassword: Bool? = nil, hasConfiguredPassword: Bool? = nil, hasPassword: Bool? = nil, id: String? = nil, lastActivityDate: Date? = nil, lastLoginDate: Date? = nil, name: String? = nil, policy: UserPolicy? = nil, primaryImageAspectRatio: Double? = nil, primaryImageTag: String? = nil, serverID: String? = nil, serverName: String? = nil) { self.configuration = configuration self.enableAutoLogin = enableAutoLogin self.hasConfiguredEasyPassword = hasConfiguredEasyPassword self.hasConfiguredPassword = hasConfiguredPassword self.hasPassword = hasPassword self.id = id + self.lastActivityDate = lastActivityDate + self.lastLoginDate = lastLoginDate self.name = name self.policy = policy self.primaryImageAspectRatio = primaryImageAspectRatio @@ -60,6 +66,8 @@ public struct UserDto: Codable, Identifiable { self.hasConfiguredPassword = try values.decodeIfPresent(Bool.self, forKey: "HasConfiguredPassword") self.hasPassword = try values.decodeIfPresent(Bool.self, forKey: "HasPassword") self.id = try values.decodeIfPresent(String.self, forKey: "Id") + self.lastActivityDate = try values.decodeIfPresent(Date.self, forKey: "LastActivityDate") + self.lastLoginDate = try values.decodeIfPresent(Date.self, forKey: "LastLoginDate") self.name = try values.decodeIfPresent(String.self, forKey: "Name") self.policy = try values.decodeIfPresent(UserPolicy.self, forKey: "Policy") self.primaryImageAspectRatio = try values.decodeIfPresent(Double.self, forKey: "PrimaryImageAspectRatio") @@ -76,6 +84,8 @@ public struct UserDto: Codable, Identifiable { try values.encodeIfPresent(hasConfiguredPassword, forKey: "HasConfiguredPassword") try values.encodeIfPresent(hasPassword, forKey: "HasPassword") try values.encodeIfPresent(id, forKey: "Id") + try values.encodeIfPresent(lastActivityDate, forKey: "LastActivityDate") + try values.encodeIfPresent(lastLoginDate, forKey: "LastLoginDate") try values.encodeIfPresent(name, forKey: "Name") try values.encodeIfPresent(policy, forKey: "Policy") try values.encodeIfPresent(primaryImageAspectRatio, forKey: "PrimaryImageAspectRatio") diff --git a/Sources/JellyfinClient.swift b/Sources/JellyfinClient.swift index 06b84005..9a6eda21 100644 --- a/Sources/JellyfinClient.swift +++ b/Sources/JellyfinClient.swift @@ -35,9 +35,18 @@ public final class JellyfinClient { self._apiClient = APIClient(baseURL: configuration.url) { configuration in configuration.sessionConfiguration = sessionConfiguration - configuration.decoder = JSONDecoder() - configuration.encoder = JSONEncoder() configuration.delegate = self + + let isoDateFormatter: DateFormatter = OpenISO8601DateFormatter() + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(isoDateFormatter) + configuration.decoder = decoder + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(isoDateFormatter) + encoder.outputFormatting = .prettyPrinted + configuration.encoder = encoder } } diff --git a/Sources/OpenISO8601Formatter.swift b/Sources/OpenISO8601Formatter.swift new file mode 100644 index 00000000..bd145e07 --- /dev/null +++ b/Sources/OpenISO8601Formatter.swift @@ -0,0 +1,45 @@ +// +// jellyfin-sdk-swift is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +// https://stackoverflow.com/a/50281094/976628 +public class OpenISO8601DateFormatter: DateFormatter { + static let withoutSeconds: DateFormatter = { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .iso8601) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + return formatter + }() + + private func setup() { + calendar = Calendar(identifier: .iso8601) + locale = Locale(identifier: "en_US_POSIX") + timeZone = TimeZone(secondsFromGMT: 0) + dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" + } + + override init() { + super.init() + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + override public func date(from string: String) -> Date? { + if let result = super.date(from: string) { + return result + } + return OpenISO8601DateFormatter.withoutSeconds.date(from: string) + } +} diff --git a/Sources/create-api-config.yaml b/Sources/create-api-config.yaml index 5bd8ec94..4cbf643d 100644 --- a/Sources/create-api-config.yaml +++ b/Sources/create-api-config.yaml @@ -19,10 +19,6 @@ entities: sortPropertiesAlphabetically: true exclude: - BaseItemDto.CurrentProgram # removed due to circular references for struct conformance - - SessionInfo.LastActivityDate # removed due to ISO 8601 incompliance - - SessionInfo.LastPlaybackCheckIn # removed due to ISO 8601 incompliance - - UserDto.LastActivityDate # removed due to ISO 8601 incompliance - - UserDto.LastLoginDate # removed due to ISO 8601 incompliance paths: style: operations filenameTemplate: "%0API.swift"