diff --git a/.gitignore b/.gitignore index fa277ac..f22fb15 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /docs/ /lib/ /bin/ +/logs/ /.shards/ /Music/ *.dwarf diff --git a/shard.lock b/shard.lock index 33cbfa9..fefdfa4 100755 --- a/shard.lock +++ b/shard.lock @@ -2,5 +2,5 @@ version: 1.0 shards: ydl_binaries: github: cooperhammond/ydl-binaries - commit: 3108c8ce9456bbde24baba64b2372b431a010558 + commit: c82e3937fee20fd076b1c73e24b2d0205e2cf0da diff --git a/src/bottle/cli.cr b/src/bottle/cli.cr index 532c88a..13affa8 100755 --- a/src/bottle/cli.cr +++ b/src/bottle/cli.cr @@ -6,6 +6,7 @@ require "./version" require "../glue/song" require "../glue/album" +require "../glue/playlist" class CLI @@ -18,7 +19,8 @@ class CLI [["-i", "--install"], "install", "bool"], [["-a", "--artist"], "artist", "string"], [["-s", "--song"], "song", "string"], - [["-A", "--album"], "album", "string"] + [["-A", "--album"], "album", "string"], + [["-p", "--playlist"], "playlist", "string"] ] @@ -83,6 +85,10 @@ class CLI a = Album.new(@args["album"], @args["artist"]) a.provide_client_keys(Config.client_key, Config.client_secret) a.grab_it() + elsif @args["playlist"]? && @args["artist"]? + p = Playlist.new(@args["playlist"], @args["artist"]) + p.provide_client_keys(Config.client_key, Config.client_secret) + p.grab_it() end end diff --git a/src/bottle/config.cr b/src/bottle/config.cr index 819a2d7..c288b5d 100755 --- a/src/bottle/config.cr +++ b/src/bottle/config.cr @@ -13,10 +13,10 @@ module Config end def client_key : String - return "e4198f6a3f7b48029366f22528b5dc66" + return "362f75b91aeb471bb392945f93eba842" end def client_secret : String - return "ba057d0621a5496bbb64edccf758bde5" + return "013556dd71e14e1da9443dee73e23a91" end end \ No newline at end of file diff --git a/src/glue/album.cr b/src/glue/album.cr index 98a080a..e6f0781 100755 --- a/src/glue/album.cr +++ b/src/glue/album.cr @@ -1,9 +1,12 @@ require "../bottle/config" +require "./mapper" require "./song" require "./list" + + class Album < SpotifyList @home_music_directory = Config.music_directory @@ -27,34 +30,17 @@ class Album < SpotifyList # of spotify's album json. Moves the title of the album and the album art # to the json of the single song def organize_song_metadata(list : JSON::Any, datum : JSON::Any) : JSON::Any - json_string = %( + album_metadata = parse_to_json(%( { - "album": { - "name": "#{list["name"]}", - "images": [{"url": "#{list["images"][0]["url"]}"}] - }, - ) - datum.as_h.keys.each_with_index do |key, index| - value = datum[key] - if value.as_s? - json_string += %("#{key}": "#{datum[key]}") - else - json_string += %("#{key}": #{datum[key].to_s.gsub(" => ", ": ")}) - end - - if index != datum.as_h.keys.size - 1 - json_string += ",\n" - end - end - json_string += %( + "name": "#{list["name"]}", + "images": [{"url": "#{list["images"][0]["url"]}"}] } - ) + )) - json_string = json_string.gsub(" ", "") - json_string = json_string.gsub("\n", " ") - json_string = json_string.gsub("\t", "") + prepped_data = AlbumTrackMetadataMapper.from_json(datum.to_json) + prepped_data.album = album_metadata - data = JSON.parse(json_string) + data = parse_to_json(prepped_data.to_json) return data end diff --git a/src/glue/mapper.cr b/src/glue/mapper.cr new file mode 100755 index 0000000..c423662 --- /dev/null +++ b/src/glue/mapper.cr @@ -0,0 +1,47 @@ +require "json" + +class PlaylistExtensionMapper + JSON.mapping( + tracks: { + type: PlaylistTracksMapper, + setter: true + }, + id: String, + images: JSON::Any, + name: String, + owner: JSON::Any, + type: String + ) +end + +class PlaylistTracksMapper + JSON.mapping( + items: { + type: Array(JSON::Any), + setter: true + }, + total: Int32 + ) +end + +class AlbumTrackMetadataMapper + JSON.mapping( + album: { + type: JSON::Any, + nilable: true, + setter: true + }, + artists: JSON::Any, + disc_number: Int32, + id: String, + name: String, + track_number: Int32, + type: String, + uri: String + ) +end + + +def parse_to_json(string_json : String) : JSON::Any + return JSON.parse(string_json) +end \ No newline at end of file diff --git a/src/glue/playlist.cr b/src/glue/playlist.cr new file mode 100755 index 0000000..0600b3e --- /dev/null +++ b/src/glue/playlist.cr @@ -0,0 +1,41 @@ +require "../bottle/config" + +require "./song" +require "./list" + +class Playlist < SpotifyList + + @home_music_directory = Config.music_directory + + # Uses the `spotify_searcher` defined in parent `SpotifyList` to find the + # correct metadata of the list + def find_it + @playlist = @spotify_searcher.find_item("playlist", { + "name" => @list_name.as(String), + "username" => @list_author.as(String) + }) + if playlist + return playlist.as(JSON::Any) + else + puts "No playlists were found by that name and user." + exit 1 + end + end + + # Will define specific metadata that may not be included in the raw return + # of spotify's album json. Moves the title of the album and the album art + # to the json of the single song + def organize_song_metadata(list : JSON::Any, datum : JSON::Any) : JSON::Any + puts datum + puts "THIS" + + exit 0 + data = datum + + return data + end + + private def organize(song : Song) + song.organize_it(@home_music_directory) + end +end diff --git a/src/search/spotify.cr b/src/search/spotify.cr index 9d54128..1d2d4c7 100755 --- a/src/search/spotify.cr +++ b/src/search/spotify.cr @@ -2,6 +2,8 @@ require "http" require "json" require "base64" +require "../glue/mapper" + class SpotifySearcher @root_url = Path["https://api.spotify.com/v1/"] @@ -83,7 +85,7 @@ class SpotifySearcher # if this triggers, it means that a playlist has failed to be found, so # the search will be bootstrapped into find_user_playlist if to_return == nil && item_type == "playlist" - return self.find_user_playlist( + return find_user_playlist( item_parameters["username"], item_parameters["name"] ) @@ -101,7 +103,7 @@ class SpotifySearcher def find_user_playlist(username : String, name : String, offset=0, limit=20) : JSON::Any? - url = "users/#{username}/playlists?offset=#{offset}&limit=#{limit}" + url = "users/#{username}/playlists?limit=#{limit}&offset=#{offset}" url = @root_url.join(url).to_s response = HTTP::Client.get(url, headers: @access_header) @@ -118,9 +120,9 @@ class SpotifySearcher begin if points[0][0] < 3 - return self.find_user_playlist(username, name, offset + limit, limit) + return find_user_playlist(username, name, offset + limit, limit) else - return items[points[0][1]] + return get_item("playlist", items[points[0][1]]["id"].to_s) end rescue IndexError return nil @@ -132,13 +134,69 @@ class SpotifySearcher # ``` # SpotifySearcher.new().authorize(...).get_item("artist", "1dfeR4HaWDbWqFHLkxsg1d") # ``` - def get_item(item_type : String, id : String) : JSON::Any? - url = @root_url.join("#{item_type}s/#{id}").to_s() + def get_item(item_type : String, id : String, offset=0, + limit=100) : JSON::Any + if item_type == "playlist" + return get_playlist(id, offset, limit) + end + + url = "#{item_type}s/#{id}?limit=#{limit}&offset=#{offset}" + url = @root_url.join(url).to_s() + response = HTTP::Client.get(url, headers: @access_header) error_check(response) - return JSON.parse(response.body) + body = JSON.parse(response.body) + + return body + end + + # The only way this method differs from `get_item` is that it makes sure to + # insert ALL tracks from the playlist into the `JSON::Any` + # + # ``` + # SpotifySearcher.new().authorize(...).get_playlist("122Fc9gVuSZoksEjKEx7L0") + # ``` + def get_playlist(id, offset=0, limit=100) : JSON::Any + url = "playlists/#{id}?limit=#{limit}&offset=#{offset}" + url = @root_url.join(url).to_s() + + response = HTTP::Client.get(url, headers: @access_header) + error_check(response) + body = JSON.parse(response.body) + parent = PlaylistExtensionMapper.from_json(response.body) + + more_tracks = body["tracks"]["total"].as_i > offset + limit + if more_tracks + return playlist_extension(parent, id, offset=offset + limit) + end + + return body + end + + # This method exists to loop through spotify API requests and combine all + # tracks that may not be captured by the limit of 100. + private def playlist_extension(parent : PlaylistExtensionMapper, + id : String, offset=0, limit=100) : JSON::Any + url = "playlists/#{id}/tracks?limit=#{limit}&offset=#{offset}" + url = @root_url.join(url).to_s() + + response = HTTP::Client.get(url, headers: @access_header) + error_check(response) + body = JSON.parse(response.body) + new_tracks = PlaylistTracksMapper.from_json(response.body) + + new_tracks.items.each do |track| + parent.tracks.items.push(track) + end + + more_tracks = body["total"].as_i > offset + limit + if more_tracks + return playlist_extension(parent, id, offset=offset + limit) + end + + return JSON.parse(parent.to_json) end # Find the genre of an artist based off of their id @@ -197,7 +255,7 @@ class SpotifySearcher end # extra api info - query += "&type=#{item_type}&offset=#{offset}&limit=#{limit}" + query += "&type=#{item_type}&limit=#{limit}&offset=#{offset}" return query end