178 lines
7.3 KiB
Swift
178 lines
7.3 KiB
Swift
//
|
|
// 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=10&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
|
|
}
|
|
|
|
}
|