├── .gitignore ├── Code └── Melody.swift ├── LICENSE ├── Makefile ├── Melody.podspec.json ├── README.md └── Tests ├── Expectation.swift ├── MelodySpec.swift ├── albumFixture.json └── fixture.json /.gitignore: -------------------------------------------------------------------------------- 1 | .conche 2 | doc 3 | -------------------------------------------------------------------------------- /Code/Melody.swift: -------------------------------------------------------------------------------- 1 | import Decodable 2 | import Foundation 3 | 4 | private func undefined(hint:String="", file:StaticString=__FILE__, line:UInt=__LINE__) -> T { 5 | let message = hint == "" ? "" : ": \(hint)" 6 | fatalError("undefined \(T.self)\(message)", file:file, line:line) 7 | } 8 | 9 | private func makeAppleMusicUrl(url: NSURL, _ propertyName: String) -> NSURL { 10 | let urlString = "\(url.absoluteString)&app=music" 11 | return NSURL(string: urlString) ?? undefined("can't generate \(propertyName)") 12 | } 13 | 14 | /// Model object for an iTunes album 15 | public struct Album: Decodable { 16 | /// Artist who made the album 17 | public let artist: Artist 18 | /// URL to an image of the album art 19 | public let artworkUrl: NSURL 20 | /// Genre 21 | public let genre: String 22 | /// iTunes identifier 23 | public let identifier: String 24 | /// Album name 25 | public let name: String 26 | /// iTunes URL for the album 27 | public let url: NSURL 28 | 29 | /// Apple Music URL for the album 30 | public var appleMusicUrl: NSURL { 31 | return makeAppleMusicUrl(url, "Album.appleMusicUrl") 32 | } 33 | 34 | /// Decode album from JSON 35 | public static func decode(json: AnyObject) throws -> Album { 36 | let artworkUrlString: String = try json => "artworkUrl100" 37 | let urlString: String = try json => "trackViewUrl" 38 | 39 | return try Album( 40 | artist: Artist.decode(json), 41 | artworkUrl: NSURL(string: artworkUrlString) ?? undefined("can't decode Album.artworkUrl"), 42 | genre: json => "primaryGenreName", 43 | identifier: json => "id", 44 | name: json => "name", 45 | url: NSURL(string: urlString) ?? undefined("can't decode Album.urlString") 46 | ) 47 | } 48 | } 49 | 50 | /// Model object for an iTunes artist 51 | public struct Artist: Decodable { 52 | /// iTunes identifier 53 | public let identifier: String 54 | /// URL to an image of the artist 55 | public let imageUrl: NSURL 56 | /// Artist name 57 | public let name: String 58 | /// iTunes URL for the artist 59 | public let url: NSURL 60 | 61 | /// Apple Music URL for the artist 62 | public var appleMusicUrl: NSURL { 63 | return makeAppleMusicUrl(url, "Artist.appleMusicUrl") 64 | } 65 | 66 | /// Decode artist from JSON 67 | public static func decode(json: AnyObject) throws -> Artist { 68 | let artistUrlString: String = try json => "artistUrl" 69 | let imageUrlString: String = try json => "artistImage" 70 | 71 | return try Artist( 72 | identifier: json => "artistId", 73 | imageUrl: NSURL(string: imageUrlString) ?? undefined("can't decode Artist.imageUrl"), 74 | name: json => "artistName", 75 | url: NSURL(string: artistUrlString) ?? undefined("can't decode Artist.artistUrl") 76 | ) 77 | } 78 | } 79 | 80 | /// Model object for an iTunes track 81 | public struct Track: Decodable { 82 | /// Artist who made the track 83 | public let artist: Artist 84 | /// URL to an image of the album art 85 | public let artworkUrl: NSURL 86 | /// Genre 87 | public let genre: String 88 | /// iTunes identifier 89 | public let identifier: String 90 | /// Track name 91 | public let name: String 92 | /// iTunes URL of the track 93 | public let url: NSURL 94 | 95 | /// Apple Music URL of the track 96 | public var appleMusicUrl: NSURL { 97 | return makeAppleMusicUrl(url, "Track.appleMusicUrl") 98 | } 99 | 100 | /// Decode track from JSON 101 | public static func decode(json: AnyObject) throws -> Track { 102 | //let artworkUrlString: String = try json => "artwork" => "url" 103 | let artworkUrlString: String = try json => "artworkUrl100" 104 | let urlString: String = try json => "trackViewUrl" 105 | 106 | return try Track( 107 | artist: Artist.decode(json), 108 | artworkUrl: NSURL(string: artworkUrlString) ?? undefined("can't decode Track.artworkUrl"), 109 | genre: json => "primaryGenreName", 110 | identifier: json => "id", 111 | name: json => "name", 112 | url: NSURL(string: urlString) ?? undefined("can't decode Track.urlString") 113 | ) 114 | } 115 | } 116 | 117 | enum Entity: String { 118 | case Album = "album" 119 | case Track = "track" 120 | } 121 | 122 | /// Search the iTunes Store for albums and tracks 123 | public class Melody { 124 | private let baseUrl = "https://sticky-summer-lb.inkstone-clients.net/api/v1/searchMusic" 125 | private let session: NSURLSession 126 | 127 | func parse(json: AnyObject) throws -> [T] { 128 | return try json => "results" 129 | } 130 | 131 | func searchUrl(term: String, entity: Entity = .Track, country: String = "de") -> NSURL { 132 | let parameters: [String:AnyObject] = [ 133 | "term": term, 134 | "country": country, 135 | "media": "appleMusic", 136 | "entity": entity.rawValue, 137 | "genreId": "", 138 | "limit": 30, 139 | "lang": "en_us" 140 | ] 141 | 142 | let components = NSURLComponents(string: baseUrl) ?? undefined("NSURLComponents is nil") 143 | components.queryItems = parameters.map() { (key, value) in 144 | NSURLQueryItem(name: key, value: value.description) 145 | } 146 | 147 | return components.URL ?? undefined("NSURLComponents.URL is nil") 148 | } 149 | 150 | /// Initializer 151 | public init() { 152 | let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration() 153 | session = NSURLSession(configuration: sessionConfiguration) 154 | } 155 | 156 | private func search(term: String, _ entity: Entity, _ completion: ([T]?, NSError?) -> Void) -> NSURLSessionDataTask { 157 | let task = session.dataTaskWithURL(searchUrl(term)) { (data, response, error) in 158 | var elements: [T]? 159 | 160 | if let data = data, json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) { 161 | elements = try? self.parse(json) 162 | } 163 | 164 | completion(elements, error) 165 | } 166 | 167 | task.resume() 168 | return task 169 | } 170 | 171 | /// Search for albums 172 | public func searchAlbums(term: String, _ completion: ([Album]?, NSError?) -> Void) -> NSURLSessionDataTask { 173 | return search(term, .Album, completion) 174 | } 175 | 176 | /// Search for tracks 177 | public func searchTracks(term: String, _ completion: ([Track]?, NSError?) -> Void) -> NSURLSessionDataTask { 178 | return search(term, .Track, completion) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Boris Bügling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all doc test 2 | 3 | all: test 4 | 5 | doc: 6 | jazzy -o doc --podspec Melody.podspec.json 7 | @cat doc/undocumented.txt 8 | 9 | test: 10 | pod lib lint Melody.podspec.json --allow-warnings 11 | conche test 12 | -------------------------------------------------------------------------------- /Melody.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Melody", 3 | "version": "0.0.1", 4 | "source_files": "Code/*.swift", 5 | "summary": "Generate Apple Music URLs for tracks.", 6 | "homepage": "https://github.com/neonichu/Melody", 7 | "license": { 8 | "type": "BSD", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "Boris Bügling": "boris@buegling.com" 13 | }, 14 | "social_media_url": "http://twitter.com/NeoNacho", 15 | "source": { 16 | "git": "https://github.com/neonichu/Melody.git", 17 | "tag": "0.0.1" 18 | }, 19 | "platforms": { 20 | "ios": "8.0", 21 | "osx": "10.10", 22 | "tvos": "9.0", 23 | "watchos": "2.0" 24 | }, 25 | "dependencies": { 26 | "Decodable": ["~> 0.3.2"] 27 | }, 28 | "test_specification": { 29 | "source_files": "Tests/*.swift", 30 | "dependencies": { 31 | "Spectre": ["~> 0.5.0"] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Melody 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | ![](http://i.giphy.com/U77vQrdZYt8EU.gif) 6 | 7 | A library for retrieving iTunes Music Store information. 8 | 9 | ## Usage 10 | 11 | You can search for albums or tracks and retrieve some model objects: 12 | 13 | ```swift 14 | Melody().searchTracks("lucky") { (tracks, _) in 15 | if let track = tracks?.first { 16 | print("\(track.name): \(track.appleMusicUrl)") 17 | } 18 | } 19 | ``` 20 | 21 | The `appleMusicUrl` will open directly in `Music.app` :tada: 22 | 23 | ## Unit Tests 24 | 25 | The tests require [Conche][1], install it via [Homebrew][2]: 26 | 27 | ``` 28 | $ brew install --HEAD kylef/formulae/conche 29 | ``` 30 | 31 | and run the tests: 32 | 33 | ``` 34 | $ make test 35 | ``` 36 | 37 | [1]: https://github.com/Conche/conche 38 | [2]: http://brew.sh 39 | -------------------------------------------------------------------------------- /Tests/Expectation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Spectre 3 | 4 | class Expectation { 5 | let timeoutInterval: NSTimeInterval 6 | let pollInterval: NSTimeInterval = 0.5 7 | var fulfilled = false 8 | 9 | init(timeoutInterval: NSTimeInterval = 10) { 10 | self.timeoutInterval = timeoutInterval 11 | } 12 | 13 | func fulfil() { 14 | fulfilled = true 15 | } 16 | 17 | func wait() throws { 18 | let runLoop = NSRunLoop.mainRunLoop() 19 | let startDate = NSDate() 20 | repeat { 21 | if fulfilled { 22 | break 23 | } 24 | 25 | let runDate = NSDate().dateByAddingTimeInterval(pollInterval) 26 | runLoop.runUntilDate(runDate) 27 | } while(NSDate().timeIntervalSinceDate(startDate) < timeoutInterval) 28 | 29 | if !fulfilled { 30 | throw failure("Expectation was not fulfilled") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/MelodySpec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Melody 3 | import Spectre 4 | 5 | describe("melody") { 6 | $0.it("should generate search URLs") { 7 | let url = Melody().searchUrl("lucky") 8 | 9 | try expect(url) == NSURL(string:"https://sticky-summer-lb.inkstone-clients.net/api/v1/searchMusic?term=lucky&genreId=&limit=30&lang=en_us&country=de&media=appleMusic&entity=track")! 10 | } 11 | 12 | $0.it("should parse JSON responses") { 13 | let fixture = NSData(contentsOfFile: "Tests/fixture.json")! 14 | let json = try NSJSONSerialization.JSONObjectWithData(fixture, options: []) 15 | let track: Track = try Melody().parse(json).first! 16 | let urlString = "https://itunes.apple.com/de/album/get-lucky-radio-edit-feat./id636967993?i=636968288" 17 | 18 | try expect(track.artist.name) == "Daft Punk" 19 | try expect(track.artist.url) == NSURL(string: "https://itunes.apple.com/de/artist/daft-punk/id5468295")! 20 | try expect(track.name) == "Get Lucky (Radio Edit) [feat. Pharrell Williams]" 21 | try expect(track.url) == NSURL(string: urlString)! 22 | try expect(track.appleMusicUrl) == NSURL(string: "\(urlString)&app=music")! 23 | } 24 | 25 | $0.it("can fetch live data for albums") { 26 | let expectation = Expectation(timeoutInterval: 15) 27 | 28 | var albums: [Album]? 29 | var error: NSError? 30 | 31 | Melody().searchAlbums("nirvana") { 32 | albums = $0 33 | error = $1 34 | 35 | expectation.fulfil() 36 | } 37 | 38 | try expectation.wait() 39 | 40 | let album = albums?.first 41 | try expect(album != nil).to.beTrue() 42 | 43 | if let album = album { 44 | try expect(album.name) == "Smells Like Teen Spirit" 45 | } 46 | } 47 | 48 | $0.it("can fetch live data for tracks") { 49 | let expectation = Expectation(timeoutInterval: 15) 50 | 51 | var error: NSError? 52 | var tracks: [Track]? 53 | 54 | Melody().searchTracks("lucky") { 55 | tracks = $0 56 | error = $1 57 | 58 | expectation.fulfil() 59 | } 60 | 61 | try expectation.wait() 62 | 63 | try expect(tracks != nil).to.beTrue() 64 | try expect(error).to.beNil() 65 | 66 | if let track = tracks?.first! { 67 | try expect(track.name) == "Get Lucky (Radio Edit) [feat. Pharrell Williams]" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/albumFixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "artistId": "5468295", 5 | "artistImage": "http://is5.mzstatic.com/image/thumb/Music/v4/00/d8/eb/00d8eb0c-df8b-a786-4b88-abd5df2c92eb/source/200x200sr.png", 6 | "artistName": "Daft Punk", 7 | "artistUrl": "https://itunes.apple.com/de/artist/daft-punk/id5468295", 8 | "artwork": { 9 | "bgColor": "090d0e", 10 | "height": 1500, 11 | "supportsLayeredImage": false, 12 | "textColor1": "c6d9f2", 13 | "textColor2": "9daed1", 14 | "textColor3": "a0b0c5", 15 | "textColor4": "808daa", 16 | "url": "http://is5.mzstatic.com/image/thumb/Music2/v4/0c/8c/4a/0c8c4a84-465f-ee54-d999-eb0743d224ef/source/{w}x{h}bb.{f}", 17 | "width": 1500 18 | }, 19 | "artworkUrl100": "http://is5.mzstatic.com/image/thumb/Music2/v4/0c/8c/4a/0c8c4a84-465f-ee54-d999-eb0743d224ef/source/200x200bb.png", 20 | "collectionCensoredName": "Random Access Memories", 21 | "collectionExplicitness": "", 22 | "contentAdvisoryRating": "", 23 | "contentRatingsBySystem": {}, 24 | "copyright": "℗ 2013 Daft Life Limited under exclusive license to Columbia Records, a Division of Sony Music Entertainment", 25 | "description": "", 26 | "genreNames": [ 27 | "Pop", 28 | "Musik", 29 | "Rock", 30 | "Electronic", 31 | "Dance", 32 | "Electronic", 33 | "House" 34 | ], 35 | "id": "617154241", 36 | "isComplete": true, 37 | "isMasteredForItunes": true, 38 | "itunesNotes": { 39 | "short": null, 40 | "standard": "Begleitet von zahlreichen Gerüchten melden sich die geheimnisumwitterten Elektro-Pioniere mit den charakteristischen Robotermasken zurück und präsentieren ihr viertes Studioalbum „Random Access Memories“. Daft Punk, das für seine futuristischen Disco-Funk-Experimente und eindrucksvollen Bühnenshows gefeierte extravagante französische Duo ist mit dem Nachfolger des unglaublich erfolgreichen „Human After All“ (2005) und des innovativen Soundtracks zu „Tron: Legacy“ (2010) auf dem besten Weg, ins Rampenlicht zurückzukehren. Sieh dir an, was es Neues von Daft Punk gibt und sei bei den Ersten, die wissen, was hinter den Gerüchten steckt." 41 | }, 42 | "kind": "album", 43 | "mediaArchiveAdamId": "643475112", 44 | "name": "Random Access Memories", 45 | "primaryGenreName": "Pop", 46 | "releaseDate": "2013-05-21T00:00:00Z", 47 | "trackCensoredName": "Random Access Memories", 48 | "trackCount": 13, 49 | "trackExplicitness": "", 50 | "trackViewUrl": "https://itunes.apple.com/de/album/random-access-memories/id617154241", 51 | "uber": { 52 | "backgroundColor": "000000", 53 | "description": null, 54 | "headerTextColor": "ffffff", 55 | "masterArt": { 56 | "bgColor": "000000", 57 | "height": 600, 58 | "supportsLayeredImage": false, 59 | "textColor1": "ffffff", 60 | "textColor2": "dae8fc", 61 | "textColor3": "cbcbcb", 62 | "textColor4": "afb9c9", 63 | "url": "http://is1.mzstatic.com/image/thumb/Features/v4/a4/f8/6f/a4f86fd2-6f33-db33-8f78-fbe9e7c6827c/source/{w}x{h}{c}.{f}", 64 | "width": 3200 65 | }, 66 | "name": null, 67 | "primaryTextColor": "ffffff", 68 | "primaryTextColorOnBlack": "rgba(241,241,241,1.0)", 69 | "titleTextColor": "ffffff", 70 | "titleTextColorOnBlack": "rgba(241,241,241,1.0)" 71 | }, 72 | "uploadedContentIds": [], 73 | "url": "https://itunes.apple.com/de/album/random-access-memories/id617154241", 74 | "version": null 75 | } 76 | ], 77 | "resultCount": 1 78 | } -------------------------------------------------------------------------------- /Tests/fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "artistId": "5468295", 5 | "artistImage": "http://is5.mzstatic.com/image/thumb/Music/v4/00/d8/eb/00d8eb0c-df8b-a786-4b88-abd5df2c92eb/source/200x200sr.png", 6 | "artistName": "Daft Punk", 7 | "artistUrl": "https://itunes.apple.com/de/artist/daft-punk/id5468295", 8 | "artwork": { 9 | "bgColor": "b14416", 10 | "height": 1500, 11 | "supportsLayeredImage": false, 12 | "textColor1": "fcf2f0", 13 | "textColor2": "fdd8d2", 14 | "textColor3": "edcfc5", 15 | "textColor4": "eebaac", 16 | "url": "http://is5.mzstatic.com/image/thumb/Music/v4/d8/86/b0/d886b031-8a86-548b-4c98-f794f7f10a02/source/{w}x{h}bb.{f}", 17 | "width": 1500 18 | }, 19 | "artworkUrl100": "http://is5.mzstatic.com/image/thumb/Music/v4/d8/86/b0/d886b031-8a86-548b-4c98-f794f7f10a02/source/200x200bb.png", 20 | "collectionCensoredName": "Get Lucky (Radio Edit) [feat. Pharrell Williams] - Single", 21 | "collectionExplicitness": "", 22 | "collectionId": "636967993", 23 | "collectionName": "Get Lucky (Radio Edit) [feat. Pharrell Williams] - Single", 24 | "composer": { 25 | "name": "Pharrell Williams, Thomas Bangalter, Guy-Manuel de Homem-Christo & Nile Rodgers", 26 | "url": "https://itunes.apple.com/de/composer/id722791" 27 | }, 28 | "contentAdvisoryRating": "", 29 | "contentRatingsBySystem": {}, 30 | "copyright": "℗ 2013 Daft Life Limited under exclusive license to Columbia Records, a Division of Sony Music Entertainment", 31 | "description": "", 32 | "genreNames": [ 33 | "Pop", 34 | "Musik" 35 | ], 36 | "id": "636968288", 37 | "kind": "song", 38 | "name": "Get Lucky (Radio Edit) [feat. Pharrell Williams]", 39 | "primaryGenreName": "Pop", 40 | "releaseDate": "2013-04-19T00:00:00Z", 41 | "trackCensoredName": "Get Lucky (Radio Edit) [feat. Pharrell Williams]", 42 | "trackExplicitness": "", 43 | "trackViewUrl": "https://itunes.apple.com/de/album/get-lucky-radio-edit-feat./id636967993?i=636968288", 44 | "url": "https://itunes.apple.com/de/album/get-lucky-radio-edit-feat./id636967993?i=636968288", 45 | "version": null 46 | } 47 | ], 48 | "resultCount": 1 49 | } --------------------------------------------------------------------------------