apple_music 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +4 -0
  3. data/.gitignore +4 -0
  4. data/.rubocop.yml +11 -10
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +1 -0
  8. data/Gemfile.lock +50 -35
  9. data/README.md +223 -1
  10. data/apple_music.gemspec +2 -1
  11. data/bin/console +3 -9
  12. data/bin/setup +1 -0
  13. data/lib/apple_music.rb +36 -0
  14. data/lib/apple_music/activity.rb +52 -0
  15. data/lib/apple_music/activity/attributes.rb +19 -0
  16. data/lib/apple_music/activity/relationships.rb +16 -0
  17. data/lib/apple_music/album.rb +62 -0
  18. data/lib/apple_music/album/attributes.rb +50 -0
  19. data/lib/apple_music/album/relationships.rb +18 -0
  20. data/lib/apple_music/apple_curator.rb +10 -0
  21. data/lib/apple_music/apple_curator/attributes.rb +19 -0
  22. data/lib/apple_music/apple_curator/relationships.rb +16 -0
  23. data/lib/apple_music/artist.rb +67 -0
  24. data/lib/apple_music/artist/attributes.rb +19 -0
  25. data/lib/apple_music/artist/relationships.rb +20 -0
  26. data/lib/apple_music/artwork.rb +30 -0
  27. data/lib/apple_music/chart.rb +33 -0
  28. data/lib/apple_music/chart_response.rb +21 -0
  29. data/lib/apple_music/config.rb +45 -0
  30. data/lib/apple_music/connection.rb +45 -0
  31. data/lib/apple_music/curator.rb +52 -0
  32. data/lib/apple_music/curator/attributes.rb +19 -0
  33. data/lib/apple_music/curator/relationships.rb +16 -0
  34. data/lib/apple_music/editorial_notes.rb +13 -0
  35. data/lib/apple_music/error.rb +27 -0
  36. data/lib/apple_music/genre.rb +38 -0
  37. data/lib/apple_music/genre/attributes.rb +16 -0
  38. data/lib/apple_music/music_video.rb +76 -0
  39. data/lib/apple_music/music_video/attributes.rb +42 -0
  40. data/lib/apple_music/music_video/relationships.rb +18 -0
  41. data/lib/apple_music/play_parameters.rb +13 -0
  42. data/lib/apple_music/playlist.rb +57 -0
  43. data/lib/apple_music/playlist/attributes.rb +45 -0
  44. data/lib/apple_music/playlist/relationships.rb +17 -0
  45. data/lib/apple_music/preview.rb +13 -0
  46. data/lib/apple_music/relationship.rb +23 -0
  47. data/lib/apple_music/resource.rb +57 -0
  48. data/lib/apple_music/response.rb +29 -0
  49. data/lib/apple_music/search.rb +39 -0
  50. data/lib/apple_music/search_result.rb +20 -0
  51. data/lib/apple_music/song.rb +81 -0
  52. data/lib/apple_music/song/attributes.rb +39 -0
  53. data/lib/apple_music/song/relationships.rb +19 -0
  54. data/lib/apple_music/station.rb +38 -0
  55. data/lib/apple_music/station/attributes.rb +27 -0
  56. data/lib/apple_music/storefront.rb +32 -0
  57. data/lib/apple_music/storefront/attributes.rb +18 -0
  58. data/lib/apple_music/version.rb +1 -1
  59. metadata +66 -5
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class MusicVideo < Resource
5
+ # https://developer.apple.com/documentation/applemusicapi/musicvideo/relationships
6
+ class Relationships
7
+ attr_reader :albums, :artists, :genres
8
+
9
+ def initialize(props = {})
10
+ @albums = Relationship.new(props['albums']).data
11
+ @artists = Relationship.new(props['artists']).data
12
+ @genres = Relationship.new(props['genres']).data
13
+ end
14
+ end
15
+
16
+ self.relationships_model = self::Relationships
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/playparameters
5
+ class PlayParameters
6
+ attr_reader :id, :kind
7
+
8
+ def initialize(props = {})
9
+ @id = props['id'] # required
10
+ @kind = props['kind'] # required
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/playlist
5
+ class Playlist < Resource
6
+ class << self
7
+ # e.g. AppleMusic::Playlist.find('pl.acc464c750b94302b8806e5fcbe56e17')
8
+ # https://developer.apple.com/documentation/applemusicapi/get_a_catalog_playlist
9
+ def find(id, **options)
10
+ storefront = Storefront.lookup(options.delete(:storefront))
11
+ response = AppleMusic.get("catalog/#{storefront}/playlists/#{id}", options)
12
+ Response.new(response.body).data.first
13
+ end
14
+
15
+ # e.g. AppleMusic::Playlist.list(ids: ['pl.acc464c750b94302b8806e5fcbe56e17', 'pl.97c6f95b0b884bedbcce117f9ea5d54b'])
16
+ def list(**options)
17
+ raise ParameterMissing, 'required parameter :ids is missing' unless options[:ids]
18
+
19
+ get_collection_by_ids(options.delete(:ids), options)
20
+ end
21
+
22
+ # e.g. AppleMusic::Playlist.get_collection_by_ids(['pl.acc464c750b94302b8806e5fcbe56e17', 'pl.97c6f95b0b884bedbcce117f9ea5d54b'])
23
+ # https://developer.apple.com/documentation/applemusicapi/get_multiple_catalog_playlists
24
+ def get_collection_by_ids(ids, **options)
25
+ ids = ids.is_a?(Array) ? ids.join(',') : ids
26
+ storefront = Storefront.lookup(options.delete(:storefront))
27
+ response = AppleMusic.get("catalog/#{storefront}/playlists", options.merge(ids: ids))
28
+ Response.new(response.body).data
29
+ end
30
+
31
+ # e.g. AppleMusic::Playlist.get_relationship('pl.acc464c750b94302b8806e5fcbe56e17', :curator)
32
+ # https://developer.apple.com/documentation/applemusicapi/get_a_catalog_playlist_s_relationship_directly_by_name
33
+ def get_relationship(id, relationship_type, **options)
34
+ storefront = Storefront.lookup(options.delete(:storefront))
35
+ response = AppleMusic.get("catalog/#{storefront}/playlists/#{id}/#{relationship_type}", options)
36
+ Response.new(response.body).data
37
+ end
38
+
39
+ # e.g. AppleMusic::Playlist.related_curator('pl.acc464c750b94302b8806e5fcbe56e17')
40
+ def related_curator(id, **options)
41
+ get_relationship(id, :curator, options).first
42
+ end
43
+
44
+ # e.g. AppleMusic::Playlist.related_tracks('pl.acc464c750b94302b8806e5fcbe56e17')
45
+ def related_tracks(id, **options)
46
+ get_relationship(id, :tracks, options)
47
+ end
48
+
49
+ def search(term, **options)
50
+ AppleMusic.search(**options.merge(term: term, types: :playlists)).playlists
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ require 'apple_music/playlist/attributes'
57
+ require 'apple_music/playlist/relationships'
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class Playlist < Resource
5
+ # https://developer.apple.com/documentation/applemusicapi/playlist/attributes
6
+ class Attributes
7
+ attr_reader :artwork, :curator_name, :description, :last_modified_date,
8
+ :name, :play_params, :playlist_type, :url, :is_chart
9
+
10
+ def initialize(props = {})
11
+ @artwork = Artwork.new(props['artwork']) if props['artwork']
12
+ @curator_name = props['curatorName']
13
+ @description = EditorialNotes.new(props['description']) if props['description']
14
+ @last_modified_date = Date.parse(props['lastModifiedDate']) if props['lastModifiedDate']
15
+ @name = props['name'] # required
16
+ @play_params = PlayParameters.new(props['playParams']) if props['playParams']
17
+ @playlist_type = props['playlistType'] # required
18
+ @url = props['url'] # required
19
+ @is_chart = props['isChart']
20
+ end
21
+
22
+ def chart?
23
+ is_chart
24
+ end
25
+
26
+ def user_shared?
27
+ playlist_type == 'user-shared'
28
+ end
29
+
30
+ def editorial?
31
+ playlist_type == 'editorial'
32
+ end
33
+
34
+ def external?
35
+ playlist_type == 'external'
36
+ end
37
+
38
+ def personal_mix?
39
+ playlist_type == 'personal-mix'
40
+ end
41
+ end
42
+
43
+ self.attributes_model = self::Attributes
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class Playlist < Resource
5
+ # https://developer.apple.com/documentation/applemusicapi/playlist/relationships
6
+ class Relationships
7
+ attr_reader :curator, :tracks
8
+
9
+ def initialize(props = {})
10
+ @curator = Relationship.new(props['curator']).data.first
11
+ @tracks = Relationship.new(props['tracks']).data
12
+ end
13
+ end
14
+
15
+ self.relationships_model = self::Relationships
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/preview
5
+ class Preview < Resource
6
+ attr_reader :artwork, :url
7
+
8
+ def initialize(props = {})
9
+ @artwork = Artwork.new(props['artwork']) if props['artwork']
10
+ @url = props['url'] # required
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/relationship
5
+ class Relationship
6
+ attr_reader :data, :href, :meta, :next
7
+
8
+ def initialize(props = {})
9
+ props ||= {}
10
+
11
+ @data = build_list(props['data'])
12
+ @href = props['href']
13
+ @meta = props['meta']
14
+ @next = props['next']
15
+ end
16
+
17
+ private
18
+
19
+ def build_list(data)
20
+ Array(data).map { |attrs| Resource.build(attrs) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/resource
5
+ class Resource
6
+ class InvalidTypeError < StandardError; end
7
+
8
+ RESOURCE_MAP = {
9
+ 'activities' => 'AppleMusic::Activity',
10
+ 'albums' => 'AppleMusic::Album',
11
+ 'apple-curators' => 'AppleMusic::AppleCurator',
12
+ 'artists' => 'AppleMusic::Artist',
13
+ 'curators' => 'AppleMusic::Curator',
14
+ 'genres' => 'AppleMusic::Genre',
15
+ 'music-videos' => 'AppleMusic::MusicVideo',
16
+ 'playlists' => 'AppleMusic::Playlist',
17
+ 'songs' => 'AppleMusic::Song',
18
+ 'stations' => 'AppleMusic::Station',
19
+ 'storefronts' => 'AppleMusic::Storefront'
20
+ }.freeze
21
+
22
+ class << self
23
+ attr_accessor :attributes_model, :relationships_model
24
+
25
+ def build(props)
26
+ class_name = RESOURCE_MAP[props['type']] || raise(InvalidTypeError, "#{props['type']} type is undefined.")
27
+ const_get(class_name).new(props)
28
+ end
29
+ end
30
+
31
+ attr_reader :href, :id, :type, :attributes, :relationships
32
+
33
+ def initialize(props = {})
34
+ @href = props['href']
35
+ @id = props['id']
36
+ @type = props['type']
37
+ @attributes = self.class.attributes_model.new(props['attributes']) if props['attributes']
38
+ @relationships = self.class.relationships_model.new(props['relationships']) if props['relationships']
39
+ end
40
+
41
+ private
42
+
43
+ def method_missing(name, *args, &block)
44
+ if attributes.respond_to?(name)
45
+ attributes.send(name, *args, &block)
46
+ elsif relationships.respond_to?(name)
47
+ relationships.send(name, *args, &block)
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def respond_to_missing?(name, include_private = false)
54
+ attributes.respond_to?(name, include_private) || relationships.respond_to?(name, include_private)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/responseroot
5
+ class Response
6
+ attr_reader :data, :errors, :href, :meta, :next, :results
7
+
8
+ def initialize(props = {})
9
+ props ||= {}
10
+ @data = Array(props['data']).map { |attrs| Resource.build(attrs) }
11
+ @errors = Array(props['errors']).map { |attrs| Error.new(attrs) }
12
+ @href = props['href']
13
+ @meta = props['meta']
14
+ @next = props['next']
15
+ @results = props['results']
16
+ raise_api_error unless success?
17
+ end
18
+
19
+ private
20
+
21
+ def raise_api_error
22
+ raise ApiError, errors.map(&:title).join(', ')
23
+ end
24
+
25
+ def success?
26
+ errors.empty?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class SearchResponse < Response
5
+ def initialize(props = {})
6
+ @results = SearchResult.new(props['results'])
7
+ super
8
+ end
9
+ end
10
+
11
+ module Search
12
+ class << self
13
+ # e.g. AppleMusic::Search.search(term: 'aaamyyy')
14
+ # https://developer.apple.com/documentation/applemusicapi/search_for_catalog_resources
15
+ def search(**options)
16
+ options[:term] = format_term(options[:term]) if options[:term]
17
+
18
+ storefront = Storefront.lookup(options.delete(:storefront))
19
+ response = AppleMusic.get("catalog/#{storefront}/search", options)
20
+ SearchResult.new(response.body['results'] || {})
21
+ end
22
+
23
+ # e.g. AppleMusic::Search.search_hints(term: 'aaamyyy')
24
+ # https://developer.apple.com/documentation/applemusicapi/get_catalog_search_hints
25
+ def search_hints(**options)
26
+ options[:term] = format_term(options[:term]) if options[:term]
27
+ storefront = Storefront.lookup(options.delete(:storefront))
28
+ response = AppleMusic.get("catalog/#{storefront}/search/hints", options)
29
+ response.body.dig('results', 'terms') || []
30
+ end
31
+
32
+ private
33
+
34
+ def format_term(term)
35
+ term.is_a?(Array) ? term.join('+') : term
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class SearchResult
5
+ attr_reader :activities, :albums, :apple_curators, :artists,
6
+ :curators, :music_videos, :playlists, :songs, :stations
7
+
8
+ def initialize(props = {})
9
+ @activities = Relationship.new(props['activities']).data
10
+ @albums = Relationship.new(props['albums']).data
11
+ @apple_curators = Relationship.new(props['apple-curators']).data
12
+ @artists = Relationship.new(props['artists']).data
13
+ @curators = Relationship.new(props['curators']).data
14
+ @music_videos = Relationship.new(props['music-videos']).data
15
+ @playlists = Relationship.new(props['playlists']).data
16
+ @songs = Relationship.new(props['songs']).data
17
+ @stations = Relationship.new(props['stations']).data
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ # https://developer.apple.com/documentation/applemusicapi/song
5
+ class Song < Resource
6
+ class << self
7
+ # e.g. AppleMusic::Song.find(900032829)
8
+ # https://developer.apple.com/documentation/applemusicapi/get_a_catalog_song
9
+ def find(id, **options)
10
+ storefront = Storefront.lookup(options.delete(:storefront))
11
+ response = AppleMusic.get("catalog/#{storefront}/songs/#{id}", options)
12
+ Response.new(response.body).data.first
13
+ end
14
+
15
+ # e.g. AppleMusic::Song.list(ids: '203709340,201281527')
16
+ # e.g. AppleMusic::Song.list(isrc: 'NLH851300057')
17
+ def list(**options)
18
+ if options[:ids]
19
+ get_collection_by_ids(options.delete(:ids), options)
20
+ elsif options[:isrc]
21
+ get_collection_by_isrc(options.delete(:isrc), options)
22
+ else
23
+ raise ParameterMissing, 'required parameter :ids or :isrc is missing'
24
+ end
25
+ end
26
+
27
+ # e.g. AppleMusic::Song.get_collection_by_ids([203709340, 201281527])
28
+ # https://developer.apple.com/documentation/applemusicapi/get_multiple_catalog_songs_by_id
29
+ def get_collection_by_ids(ids, **options)
30
+ ids = ids.is_a?(Array) ? ids.join(',') : ids
31
+ storefront = Storefront.lookup(options.delete(:storefront))
32
+ response = AppleMusic.get("catalog/#{storefront}/songs", options.merge(ids: ids))
33
+ Response.new(response.body).data
34
+ end
35
+
36
+ # e.g. AppleMusic::Song.get_collection_by_isrc('NLH851300057')
37
+ # https://developer.apple.com/documentation/applemusicapi/get_multiple_catalog_songs_by_isrc
38
+ def get_collection_by_isrc(isrc, **options)
39
+ isrc = isrc.is_a?(Array) ? isrc.join(',') : isrc
40
+ storefront = Storefront.lookup(options.delete(:storefront))
41
+ response = AppleMusic.get("catalog/#{storefront}/songs", options.merge('filter[isrc]': isrc))
42
+ Response.new(response.body).data
43
+ end
44
+
45
+ # e.g. AppleMusic::Song.get_relationship(900032829, :albums)
46
+ # https://developer.apple.com/documentation/applemusicapi/get_a_catalog_song_s_relationship_directly_by_name
47
+ def get_relationship(id, relationship_type, **options)
48
+ storefront = Storefront.lookup(options.delete(:storefront))
49
+ response = AppleMusic.get("catalog/#{storefront}/songs/#{id}/#{relationship_type}", options)
50
+ Response.new(response.body).data
51
+ end
52
+
53
+ # e.g. AppleMusic::Song.related_albums(900032829)
54
+ def related_albums(id, **options)
55
+ get_relationship(id, :albums, options)
56
+ end
57
+
58
+ # e.g. AppleMusic::Song.related_artists(900032829)
59
+ def related_artists(id, **options)
60
+ get_relationship(id, :artists, options)
61
+ end
62
+
63
+ # e.g. AppleMusic::Song.related_genres(900032829)
64
+ def related_genres(id, **options)
65
+ get_relationship(id, :genres, options)
66
+ end
67
+
68
+ # e.g. AppleMusic::Song.related_station(900032829)
69
+ def related_station(id, **options)
70
+ get_relationship(id, :station, options).first
71
+ end
72
+
73
+ def search(term, **options)
74
+ AppleMusic.search(**options.merge(term: term, types: :songs)).songs
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ require 'apple_music/song/attributes'
81
+ require 'apple_music/song/relationships'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppleMusic
4
+ class Song < Resource
5
+ # https://developer.apple.com/documentation/applemusicapi/song/attributes
6
+ class Attributes
7
+ attr_reader :album_name, :artist_name, :artwork, :composer_name,
8
+ :content_rating, :disc_number, :duration_in_millis,
9
+ :editorial_notes, :genre_names, :isrc, :movement_count,
10
+ :movement_name, :movement_number, :name, :play_params,
11
+ :previews, :release_date, :track_number, :url, :work_name
12
+
13
+ def initialize(props = {})
14
+ @album_name = props['albumName'] # required
15
+ @artist_name = props['artistName'] # required
16
+ @artwork = Artwork.new(props['artwork']) # required
17
+ @composer_name = props['composerName']
18
+ @content_rating = props['contentRating']
19
+ @disc_number = props['discNumber'] # required
20
+ @duration_in_millis = props['durationInMillis']
21
+ @editorial_notes = EditorialNotes.new(props['editorialNotes']) if props['editorialNotes']
22
+ @genre_names = props['genreNames'] # required
23
+ @isrc = props['isrc'] # required
24
+ @movement_count = props['movementCount']
25
+ @movement_name = props['movementName']
26
+ @movement_number = props['movementNumber']
27
+ @name = props['name'] # required
28
+ @play_params = PlayParameters.new(props['playParams']) if props['playParams']
29
+ @previews = Array(props['previews']).map { |attrs| Preview.new(attrs) } # required
30
+ @release_date = Date.parse(props['releaseDate']) # required
31
+ @track_number = props['trackNumber'] # required
32
+ @url = props['url'] # required
33
+ @work_name = props['workName']
34
+ end
35
+ end
36
+
37
+ self.attributes_model = self::Attributes
38
+ end
39
+ end