import ArgumentParser import MusicKit import iTunesLibrary enum MusicError: Error { case notAuthorized case noData } enum Source: String, CaseIterable { case itunes case musickit } struct Track { var title: String? var dateAdded: Date? var dateModified: Date? var playDateUTC: Date? } extension Track: Encodable { func jsonData(_ strategy: JSONEncoder.DateEncodingStrategy) throws -> Data { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] encoder.dateEncodingStrategy = strategy return try encoder.encode(self) } } extension Track { func jsonString(_ strategy: JSONEncoder.DateEncodingStrategy = .iso8601) -> String { guard let data = try? jsonData(strategy) else { return "cannot encode" } guard let s = String(data: data, encoding: .utf8) else { return "cannot convert" } return s } } extension Track: CustomStringConvertible { var description: String { jsonString() } } extension Track { var referenceDate: String { jsonString( .custom({ date, encoder in var container = encoder.singleValueContainer() try container.encode(date.timeIntervalSinceReferenceDate) })) } } extension Track { init(_ mediaItem: ITLibMediaItem) { self.title = mediaItem.title self.dateAdded = mediaItem.addedDate self.dateModified = mediaItem.modifiedDate self.playDateUTC = mediaItem.lastPlayedDate } static func firstITunesTrack() async throws -> Track { let itunes = try ITLibrary(apiVersion: "1.1") guard let first = itunes.allMediaItems.first else { throw MusicError.noData } return Track(first) } } extension Track { init(_ song: Song) { self.title = song.title self.dateAdded = song.libraryAddedDate self.dateModified = nil // FB15762879 self.playDateUTC = song.lastPlayedDate } static func firstMusicKitTrack() async throws -> Track { guard await MusicAuthorization.request() == .authorized else { throw MusicError.notAuthorized } let response = try await MusicLibraryRequest().response() guard let first = response.items.first else { throw MusicError.noData } return Track(first) } } extension Source { func track() async throws -> Track { switch self { case .itunes: try await Track.firstITunesTrack() case .musickit: try await Track.firstMusicKitTrack() } } } @main struct MusicDate: AsyncParsableCommand { func run() async throws { print("TimeZone.autoupdatingCurrent: \(TimeZone.autoupdatingCurrent)") print("TimeZone.current: \(TimeZone.current)") for source in Source.allCases { print("\(source.rawValue) -") let track = try await source.track() print("json: \(track)") print("reference: \(track.referenceDate)") } } }