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