mirror of
				https://github.com/cooperhammond/irs.git
				synced 2025-11-03 18:14:51 +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/
 | 
					/docs/
 | 
				
			||||||
/lib/
 | 
					/lib/
 | 
				
			||||||
/bin/
 | 
					/bin/
 | 
				
			||||||
 | 
					/logs/
 | 
				
			||||||
/.shards/
 | 
					/.shards/
 | 
				
			||||||
/Music/
 | 
					/Music/
 | 
				
			||||||
*.dwarf
 | 
					*.dwarf
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
									
								
							
							
						
						
									
										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 "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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue