// // iTunes.swift // Harmony // // Created by Kim Wittenburg on 14.04.15. // Copyright (c) 2015 Das Code Kollektiv. All rights reserved. // import Foundation import AppKitPlus /// The Cocoa Scripting Bridge interface of iTunes. public let iTunes = iTunesApplication(bundleIdentifier: "com.apple.iTunes")! /// An ID as returned from the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) public typealias iTunesId = UInt /// This struct contains static helper objects and methods to work with the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). public struct iTunesAPI { // MARK: Types /// Error types indicating error responses from the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). public enum Error: ErrorType { case InvalidCountryCode case InvalidLanguageCode case UnknownError } /// Contains constants identifying the fields in a response from the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). public enum Field: String { case WrapperType = "wrapperType" case Kind = "kind" case TrackId = "trackId" case TrackName = "trackName" case TrackCensoredName = "trackCensoredName" case ArtistName = "artistName" case ReleaseDate = "releaseDate" case TrackNumber = "trackNumber" case TrackCount = "trackCount" case DiscNumber = "discNumber" case DiscCount = "discCount" case PrimaryGenreName = "primaryGenreName" case CollectionId = "collectionId" case CollectionName = "collectionName" case CollectionCensoredName = "collectionCensoredName" case CollectionViewUrl = "collectionViewUrl" case CollectionArtistName = "collectionArtistName" case ArtworkUrl60 = "artworkUrl60" case ArtworkUrl100 = "artworkUrl100" } // MARK: Static Properties and Functions /// This formatter is configured to be used to parse dates returned from the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) internal static let sharedDateFormatter: NSDateFormatter = { let dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" return dateFormatter }() /// Processes the data returned from a request to the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). /// The `data` has to be in a valid JSON format. See `NSJSONSerialization` /// for details. /// /// Currently only tracks and albums are supported. If there are any other /// entries in the specified data this function will raise an exception. /// /// - throws: Parsing errors and `iTUnesAPI.Error` constants. /// - returns: An array of albums populated with all associated tracks in the /// specified data. public static func parseAPIData(data: NSData) throws -> [Album] { guard let parsedData = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else { throw Error.UnknownError } // Handle API Errors if let errorMessage = parsedData["errorMessage"] as? String { switch errorMessage { case "Invalid value(s) for key(s): [country]": throw Error.InvalidCountryCode case "Invalid value(s) for key(s): [language]": throw Error.InvalidLanguageCode default: throw Error.UnknownError } } // Parse API Results var albums = [iTunesId: Album]() if let results = parsedData["results"] as? [[String: AnyObject]] { for result in results { let convertedResult = result.filter({ (key, value) -> Bool in Field(rawValue: key) != nil }).map({ (Field(rawValue: $0)!, $1) }) let albumId = convertedResult[.CollectionId] as! iTunesId if albums[albumId] == nil { albums[albumId] = Album(data: convertedResult) } if isTrack(convertedResult) { albums[albumId]?.addTrack(Track(data: convertedResult)) } } } return Array(albums.values) } private static func isTrack(data: [Field: AnyObject]) -> Bool { return data[.WrapperType] as! String == "track" && data[.Kind] as! String == "song" } /// Creates an URL that searches the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html) /// for albums matching a specific `term`. /// /// This function respects the user's preferences (See `Preferences` class). /// /// - returns: The query URL or `nil` if `term` is invalid. public static func createAlbumSearchURLForTerm(term: String) -> NSURL? { var searchTerm = term.stringByReplacingOccurrencesOfString(" ", withString: "+") searchTerm = searchTerm.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet())! if searchTerm.isEmpty { return nil } return NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)&media=music&entity=album&limit=\(Preferences.sharedPreferences.numberOfSearchResults)&country=de&lang=de_DE") } /// Creates an URL that looks up all tracks that belong to the album with the /// specified `id` in the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). /// /// This function respects the user's preferences (See `Preferences` class). public static func createAlbumLookupURLForId(id: iTunesId) -> NSURL { return NSURL(string: "http://itunes.apple.com/lookup?id=\(id)&entity=song&country=de&lang=de_DE&limit=200")! } } /// Defines a type that can be parsed from a result of the /// [Search API](https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html). public protocol iTunesType { /// Initializes the receiver with the specified data. The receiver may use /// all or only some of the values. /// /// This method requires `data` to contain the expected formats. If data /// contains no data or data in an invalid format for an expected key, this /// method raises an exception. To check wether a specified dictionary is /// valid use `canInitializeFromData`. init(data: [iTunesAPI.Field: AnyObject]) /// Returns all fields that are required to initialize an instance of the /// receiving type. static var requiredFields: [iTunesAPI.Field] { get } } extension iTunesType { /// Returns wether the specified `data` can be used to initialize a instance /// of the receiving type. public static func canInitializeFromData(data: [iTunesAPI.Field: AnyObject]) -> Bool { for field in requiredFields { if data[field] == nil { return false } } return true } }