spotify searcher can now find and compile playlists >100 songs

This commit is contained in:
Cooper Hammond 2020-05-09 16:13:50 -07:00
parent 5c611c9af5
commit 4c20735abd
8 changed files with 175 additions and 36 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
/docs/ /docs/
/lib/ /lib/
/bin/ /bin/
/logs/
/.shards/ /.shards/
/Music/ /Music/
*.dwarf *.dwarf

View file

@ -2,5 +2,5 @@ version: 1.0
shards: shards:
ydl_binaries: ydl_binaries:
github: cooperhammond/ydl-binaries github: cooperhammond/ydl-binaries
commit: 3108c8ce9456bbde24baba64b2372b431a010558 commit: c82e3937fee20fd076b1c73e24b2d0205e2cf0da

View file

@ -6,6 +6,7 @@ require "./version"
require "../glue/song" require "../glue/song"
require "../glue/album" require "../glue/album"
require "../glue/playlist"
class CLI class CLI
@ -18,7 +19,8 @@ class CLI
[["-i", "--install"], "install", "bool"], [["-i", "--install"], "install", "bool"],
[["-a", "--artist"], "artist", "string"], [["-a", "--artist"], "artist", "string"],
[["-s", "--song"], "song", "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 = Album.new(@args["album"], @args["artist"])
a.provide_client_keys(Config.client_key, Config.client_secret) a.provide_client_keys(Config.client_key, Config.client_secret)
a.grab_it() 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
end end

View file

@ -13,10 +13,10 @@ module Config
end end
def client_key : String def client_key : String
return "e4198f6a3f7b48029366f22528b5dc66" return "362f75b91aeb471bb392945f93eba842"
end end
def client_secret : String def client_secret : String
return "ba057d0621a5496bbb64edccf758bde5" return "013556dd71e14e1da9443dee73e23a91"
end end
end end

View file

@ -1,9 +1,12 @@
require "../bottle/config" require "../bottle/config"
require "./mapper"
require "./song" require "./song"
require "./list" require "./list"
class Album < SpotifyList class Album < SpotifyList
@home_music_directory = Config.music_directory @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 # of spotify's album json. Moves the title of the album and the album art
# to the json of the single song # to the json of the single song
def organize_song_metadata(list : JSON::Any, datum : JSON::Any) : JSON::Any def organize_song_metadata(list : JSON::Any, datum : JSON::Any) : JSON::Any
json_string = %( album_metadata = parse_to_json(%(
{ {
"album": { "name": "#{list["name"]}",
"name": "#{list["name"]}", "images": [{"url": "#{list["images"][0]["url"]}"}]
"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 += %(
} }
) ))
json_string = json_string.gsub(" ", "") prepped_data = AlbumTrackMetadataMapper.from_json(datum.to_json)
json_string = json_string.gsub("\n", " ") prepped_data.album = album_metadata
json_string = json_string.gsub("\t", "")
data = JSON.parse(json_string) data = parse_to_json(prepped_data.to_json)
return data return data
end end

47
src/glue/mapper.cr Executable file
View file

@ -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

41
src/glue/playlist.cr Executable file
View file

@ -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

View file

@ -2,6 +2,8 @@ require "http"
require "json" require "json"
require "base64" require "base64"
require "../glue/mapper"
class SpotifySearcher class SpotifySearcher
@root_url = Path["https://api.spotify.com/v1/"] @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 # if this triggers, it means that a playlist has failed to be found, so
# the search will be bootstrapped into find_user_playlist # the search will be bootstrapped into find_user_playlist
if to_return == nil && item_type == "playlist" if to_return == nil && item_type == "playlist"
return self.find_user_playlist( return find_user_playlist(
item_parameters["username"], item_parameters["username"],
item_parameters["name"] item_parameters["name"]
) )
@ -101,7 +103,7 @@ class SpotifySearcher
def find_user_playlist(username : String, name : String, offset=0, def find_user_playlist(username : String, name : String, offset=0,
limit=20) : JSON::Any? 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 url = @root_url.join(url).to_s
response = HTTP::Client.get(url, headers: @access_header) response = HTTP::Client.get(url, headers: @access_header)
@ -118,9 +120,9 @@ class SpotifySearcher
begin begin
if points[0][0] < 3 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 else
return items[points[0][1]] return get_item("playlist", items[points[0][1]]["id"].to_s)
end end
rescue IndexError rescue IndexError
return nil return nil
@ -132,13 +134,69 @@ class SpotifySearcher
# ``` # ```
# SpotifySearcher.new().authorize(...).get_item("artist", "1dfeR4HaWDbWqFHLkxsg1d") # SpotifySearcher.new().authorize(...).get_item("artist", "1dfeR4HaWDbWqFHLkxsg1d")
# ``` # ```
def get_item(item_type : String, id : String) : JSON::Any? def get_item(item_type : String, id : String, offset=0,
url = @root_url.join("#{item_type}s/#{id}").to_s() 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) response = HTTP::Client.get(url, headers: @access_header)
error_check(response) 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 end
# Find the genre of an artist based off of their id # Find the genre of an artist based off of their id
@ -197,7 +255,7 @@ class SpotifySearcher
end end
# extra api info # extra api info
query += "&type=#{item_type}&offset=#{offset}&limit=#{limit}" query += "&type=#{item_type}&limit=#{limit}&offset=#{offset}"
return query return query
end end