mirror of
https://github.com/cooperhammond/irs.git
synced 2024-12-22 17:35:28 +00:00
spotify searcher can now find and compile playlists >100 songs
This commit is contained in:
parent
5c611c9af5
commit
4c20735abd
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
/docs/
|
||||
/lib/
|
||||
/bin/
|
||||
/logs/
|
||||
/.shards/
|
||||
/Music/
|
||||
*.dwarf
|
||||
|
|
|
@ -2,5 +2,5 @@ version: 1.0
|
|||
shards:
|
||||
ydl_binaries:
|
||||
github: cooperhammond/ydl-binaries
|
||||
commit: 3108c8ce9456bbde24baba64b2372b431a010558
|
||||
commit: c82e3937fee20fd076b1c73e24b2d0205e2cf0da
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
47
src/glue/mapper.cr
Executable file
47
src/glue/mapper.cr
Executable 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
41
src/glue/playlist.cr
Executable 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue