mirror of
				https://github.com/cooperhammond/irs.git
				synced 2025-11-03 18:14:51 +00:00 
			
		
		
		
	crystal tool format respec
This commit is contained in:
		
							parent
							
								
									d1657ba86d
								
							
						
					
					
						commit
						de219cbe66
					
				| 
						 | 
				
			
			@ -8,9 +8,7 @@ require "../glue/song"
 | 
			
		|||
require "../glue/album"
 | 
			
		||||
require "../glue/playlist"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CLI
 | 
			
		||||
 | 
			
		||||
  # layout:
 | 
			
		||||
  # [[shortflag, longflag], key, type]
 | 
			
		||||
  @options = [
 | 
			
		||||
| 
						 | 
				
			
			@ -21,10 +19,9 @@ class CLI
 | 
			
		|||
    [["-a", "--artist"], "artist", "string"],
 | 
			
		||||
    [["-s", "--song"], "song", "string"],
 | 
			
		||||
    [["-A", "--album"], "album", "string"],
 | 
			
		||||
    [["-p", "--playlist"], "playlist", "string"]
 | 
			
		||||
    [["-p", "--playlist"], "playlist", "string"],
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  @args : Hash(String, String)
 | 
			
		||||
 | 
			
		||||
  def initialize(argv : Array(String))
 | 
			
		||||
| 
						 | 
				
			
			@ -68,8 +65,7 @@ class CLI
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def act_on_args
 | 
			
		||||
    
 | 
			
		||||
    Config.check_necessities()
 | 
			
		||||
    Config.check_necessities
 | 
			
		||||
 | 
			
		||||
    if @args["help"]? || @args.keys.size == 0
 | 
			
		||||
      help
 | 
			
		||||
| 
						 | 
				
			
			@ -86,17 +82,17 @@ class CLI
 | 
			
		|||
    elsif @args["song"]? && @args["artist"]?
 | 
			
		||||
      s = Song.new(@args["song"], @args["artist"])
 | 
			
		||||
      s.provide_client_keys(Config.client_key, Config.client_secret)
 | 
			
		||||
      s.grab_it()
 | 
			
		||||
      s.grab_it
 | 
			
		||||
      s.organize_it(Config.music_directory)
 | 
			
		||||
      exit
 | 
			
		||||
    elsif @args["album"]? && @args["artist"]?
 | 
			
		||||
      a = Album.new(@args["album"], @args["artist"])
 | 
			
		||||
      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()
 | 
			
		||||
      p.grab_it
 | 
			
		||||
    else
 | 
			
		||||
      puts Style.red("Those arguments don't do anything when used that way.")
 | 
			
		||||
      puts "Type `irs -h` to see usage."
 | 
			
		||||
| 
						 | 
				
			
			@ -111,12 +107,11 @@ class CLI
 | 
			
		|||
    current_key = ""
 | 
			
		||||
    pass_next_arg = false
 | 
			
		||||
    argv.each do |arg|
 | 
			
		||||
 | 
			
		||||
      # If the previous arg was an arg flag, this is an arg, so pass it
 | 
			
		||||
      if pass_next_arg 
 | 
			
		||||
      if pass_next_arg
 | 
			
		||||
        pass_next_arg = false
 | 
			
		||||
        i += 1
 | 
			
		||||
        next 
 | 
			
		||||
        next
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      flag = [] of Array(String) | String
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +135,6 @@ class CLI
 | 
			
		|||
        arg_error argv, i, %("#{arg}" needs an argument.)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      
 | 
			
		||||
      key = flag[1].as(String)
 | 
			
		||||
      if flag[2] == "string"
 | 
			
		||||
        arguments[key] = argv[i + 1]
 | 
			
		||||
| 
						 | 
				
			
			@ -182,4 +176,4 @@ class CLI
 | 
			
		|||
    puts "Type `irs -h` to see usage."
 | 
			
		||||
    exit 1
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ require "yaml"
 | 
			
		|||
 | 
			
		||||
require "./styles"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
EXAMPLE_CONFIG = <<-EOP
 | 
			
		||||
#{Style.dim "exampleconfig.yml"}
 | 
			
		||||
#{Style.dim "===="}
 | 
			
		||||
| 
						 | 
				
			
			@ -18,17 +17,16 @@ single_folder_playlist:
 | 
			
		|||
EOP
 | 
			
		||||
 | 
			
		||||
module Config
 | 
			
		||||
 | 
			
		||||
  extend self
 | 
			
		||||
 | 
			
		||||
  @@arguments = [
 | 
			
		||||
    "binary_directory", 
 | 
			
		||||
    "music_directory", 
 | 
			
		||||
    "client_key", 
 | 
			
		||||
    "binary_directory",
 | 
			
		||||
    "music_directory",
 | 
			
		||||
    "client_key",
 | 
			
		||||
    "client_secret",
 | 
			
		||||
    "single_folder_playlist: enabled", 
 | 
			
		||||
    "single_folder_playlist: retain_playlist_order", 
 | 
			
		||||
    "single_folder_playlist: overwrite_album"
 | 
			
		||||
    "single_folder_playlist: enabled",
 | 
			
		||||
    "single_folder_playlist: retain_playlist_order",
 | 
			
		||||
    "single_folder_playlist: overwrite_album",
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @@conf = YAML.parse("")
 | 
			
		||||
| 
						 | 
				
			
			@ -69,14 +67,14 @@ module Config
 | 
			
		|||
 | 
			
		||||
  def overwrite_album? : Bool
 | 
			
		||||
    return @@conf["single_folder_playlist"]["overwrite_album"].as_bool
 | 
			
		||||
  end 
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_necessities
 | 
			
		||||
    missing_configs = [] of String
 | 
			
		||||
    @@arguments.each do |argument|
 | 
			
		||||
      if !check_conf(argument)
 | 
			
		||||
        missing_configs.push(argument)
 | 
			
		||||
      end      
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
    if missing_configs.size > 0
 | 
			
		||||
      puts Style.red("You are missing the following key(s) in your YAML config file:")
 | 
			
		||||
| 
						 | 
				
			
			@ -102,4 +100,4 @@ module Config
 | 
			
		|||
      return @@conf[key]?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,4 +20,4 @@ class Style
 | 
			
		|||
  def self.red(txt)
 | 
			
		||||
    txt.colorize(:light_red)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
module IRS
 | 
			
		||||
  VERSION = "0.1.0"
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,17 +4,15 @@ require "./mapper"
 | 
			
		|||
require "./song"
 | 
			
		||||
require "./list"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Album < SpotifyList
 | 
			
		||||
 | 
			
		||||
  @home_music_directory = Config.music_directory
 | 
			
		||||
 | 
			
		||||
  # Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
 | 
			
		||||
  # correct metadata of the list 
 | 
			
		||||
  # correct metadata of the list
 | 
			
		||||
  def find_it
 | 
			
		||||
    album = @spotify_searcher.find_item("album", {
 | 
			
		||||
      "name" => @list_name.as(String),
 | 
			
		||||
      "artist" => @list_author.as(String)
 | 
			
		||||
      "name"   => @list_name.as(String),
 | 
			
		||||
      "artist" => @list_author.as(String),
 | 
			
		||||
    })
 | 
			
		||||
    if album
 | 
			
		||||
      return album.as(JSON::Any)
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +22,7 @@ class Album < SpotifyList
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Will define specific metadata that may not be included in the raw return 
 | 
			
		||||
  # 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,10 +8,9 @@ require "../interact/tagger"
 | 
			
		|||
 | 
			
		||||
require "./song"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# A parent class for downloading albums and playlists from spotify
 | 
			
		||||
abstract class SpotifyList
 | 
			
		||||
  @spotify_searcher = SpotifySearcher.new()
 | 
			
		||||
  @spotify_searcher = SpotifySearcher.new
 | 
			
		||||
  @file_names = [] of String
 | 
			
		||||
 | 
			
		||||
  def initialize(@list_name : String, @list_author : String?)
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +18,6 @@ abstract class SpotifyList
 | 
			
		|||
 | 
			
		||||
  # Finds the list, and downloads all of the songs using the `Song` class
 | 
			
		||||
  def grab_it
 | 
			
		||||
 | 
			
		||||
    if !@spotify_searcher.authorized?
 | 
			
		||||
      raise("Need to call provide_client_keys on Album or Playlist class.")
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +36,7 @@ abstract class SpotifyList
 | 
			
		|||
      song = Song.new(data["name"].to_s, data["artists"][0]["name"].to_s)
 | 
			
		||||
      song.provide_spotify(@spotify_searcher)
 | 
			
		||||
      song.provide_metadata(data)
 | 
			
		||||
      song.grab_it()
 | 
			
		||||
      song.grab_it
 | 
			
		||||
 | 
			
		||||
      organize(song)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -57,12 +55,11 @@ abstract class SpotifyList
 | 
			
		|||
 | 
			
		||||
  # If there's a need to organize the individual song data so that the `Song`
 | 
			
		||||
  # class can better handle it, this function will be defined in the subclass
 | 
			
		||||
  private abstract def organize_song_metadata(list : JSON::Any, 
 | 
			
		||||
    datum : JSON::Any) : JSON::Any
 | 
			
		||||
  private abstract def organize_song_metadata(list : JSON::Any,
 | 
			
		||||
                                              datum : JSON::Any) : JSON::Any
 | 
			
		||||
 | 
			
		||||
  # Will define the specific type of organization for a list of songs.
 | 
			
		||||
  # Needed because most people want albums sorted by artist, but playlists all
 | 
			
		||||
  # in one folder
 | 
			
		||||
  private abstract def organize(song : Song)
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,8 @@ require "json"
 | 
			
		|||
class PlaylistExtensionMapper
 | 
			
		||||
  JSON.mapping(
 | 
			
		||||
    tracks: {
 | 
			
		||||
      type: PlaylistTracksMapper,
 | 
			
		||||
      setter: true
 | 
			
		||||
      type:   PlaylistTracksMapper,
 | 
			
		||||
      setter: true,
 | 
			
		||||
    },
 | 
			
		||||
    id: String,
 | 
			
		||||
    images: JSON::Any,
 | 
			
		||||
| 
						 | 
				
			
			@ -17,8 +17,8 @@ end
 | 
			
		|||
class PlaylistTracksMapper
 | 
			
		||||
  JSON.mapping(
 | 
			
		||||
    items: {
 | 
			
		||||
      type: Array(JSON::Any),
 | 
			
		||||
      setter: true
 | 
			
		||||
      type:   Array(JSON::Any),
 | 
			
		||||
      setter: true,
 | 
			
		||||
    },
 | 
			
		||||
    total: Int32
 | 
			
		||||
  )
 | 
			
		||||
| 
						 | 
				
			
			@ -26,10 +26,10 @@ end
 | 
			
		|||
 | 
			
		||||
class AlbumTracksMapper
 | 
			
		||||
  JSON.mapping(
 | 
			
		||||
    album: { 
 | 
			
		||||
      type: JSON::Any,
 | 
			
		||||
    album: {
 | 
			
		||||
      type:    JSON::Any,
 | 
			
		||||
      nilable: true,
 | 
			
		||||
      setter: true
 | 
			
		||||
      setter:  true,
 | 
			
		||||
    },
 | 
			
		||||
    artists: JSON::Any,
 | 
			
		||||
    disc_number: Int32,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +41,6 @@ class AlbumTracksMapper
 | 
			
		|||
  )
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_to_json(string_json : String) : JSON::Any
 | 
			
		||||
  return JSON.parse(string_json)
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,18 +3,16 @@ require "../bottle/config"
 | 
			
		|||
require "./song"
 | 
			
		||||
require "./list"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Playlist < SpotifyList
 | 
			
		||||
 | 
			
		||||
  @home_music_directory = Config.music_directory
 | 
			
		||||
  @playlist : JSON::Any?
 | 
			
		||||
 | 
			
		||||
  # Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
 | 
			
		||||
  # correct metadata of the list 
 | 
			
		||||
  # correct metadata of the list
 | 
			
		||||
  def find_it
 | 
			
		||||
    @playlist = @spotify_searcher.find_item("playlist", {
 | 
			
		||||
      "name" => @list_name.as(String),
 | 
			
		||||
      "username" => @list_author.as(String)
 | 
			
		||||
      "name"     => @list_name.as(String),
 | 
			
		||||
      "username" => @list_author.as(String),
 | 
			
		||||
    })
 | 
			
		||||
    if @playlist
 | 
			
		||||
      return @playlist.as(JSON::Any)
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +22,7 @@ class Playlist < SpotifyList
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Will define specific metadata that may not be included in the raw return 
 | 
			
		||||
  # 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,8 @@ require "../search/youtube"
 | 
			
		|||
require "../interact/ripper"
 | 
			
		||||
require "../interact/tagger"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Song
 | 
			
		||||
  @spotify_searcher = SpotifySearcher.new()
 | 
			
		||||
  @spotify_searcher = SpotifySearcher.new
 | 
			
		||||
  @client_id = ""
 | 
			
		||||
  @client_secret = ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,29 +20,29 @@ class Song
 | 
			
		|||
  # Find, downloads, and tags the mp3 song that this class represents.
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # Song.new("Bohemian Rhapsody", "Queen").grab_it()
 | 
			
		||||
  # ```   
 | 
			
		||||
  # Song.new("Bohemian Rhapsody", "Queen").grab_it
 | 
			
		||||
  # ```
 | 
			
		||||
  def grab_it
 | 
			
		||||
    if !@spotify_searcher.authorized? && !@metadata
 | 
			
		||||
      if @client_id != "" && @client_secret != ""
 | 
			
		||||
        @spotify_searcher.authorize(@client_id, @client_secret)
 | 
			
		||||
      else
 | 
			
		||||
        raise("Need to call either `provide_metadata`, `provide_spotify`, " +
 | 
			
		||||
          "or `provide_client_keys` so that Spotify can be interfaced with.")
 | 
			
		||||
              "or `provide_client_keys` so that Spotify can be interfaced with.")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if !@metadata
 | 
			
		||||
      puts "Searching for metadata ..."
 | 
			
		||||
      @metadata = @spotify_searcher.find_item("track", {
 | 
			
		||||
        "name" => @song_name,
 | 
			
		||||
        "artist" => @artist_name
 | 
			
		||||
        "name"   => @song_name,
 | 
			
		||||
        "artist" => @artist_name,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if !@metadata
 | 
			
		||||
        raise("There was no metadata found on Spotify for\n" + 
 | 
			
		||||
          %("#{@song_name}" by "#{@artist_name}\n) +
 | 
			
		||||
          "Check your input and try again.")
 | 
			
		||||
        raise("There was no metadata found on Spotify for\n" +
 | 
			
		||||
              %("#{@song_name}" by "#{@artist_name}\n) +
 | 
			
		||||
              "Check your input and try again.")
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -54,12 +53,12 @@ class Song
 | 
			
		|||
    # TODO: should this search_term be here?
 | 
			
		||||
    url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics")
 | 
			
		||||
 | 
			
		||||
    if !url 
 | 
			
		||||
      raise("There was no url found on youtube for\n" + 
 | 
			
		||||
          %("#{@song_name}" by "#{@artist_name}\n) +
 | 
			
		||||
          "Check your input and try again.")
 | 
			
		||||
    if !url
 | 
			
		||||
      raise("There was no url found on youtube for\n" +
 | 
			
		||||
            %("#{@song_name}" by "#{@artist_name}\n) +
 | 
			
		||||
            "Check your input and try again.")
 | 
			
		||||
    end
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    puts "Downloading video:"
 | 
			
		||||
    Ripper.download_mp3(url.as(String), @filename)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,27 +75,26 @@ class Song
 | 
			
		|||
    tagger.add_text_tag("title", data["name"].to_s)
 | 
			
		||||
    tagger.add_text_tag("artist", @artist)
 | 
			
		||||
    tagger.add_text_tag("album", @album)
 | 
			
		||||
    tagger.add_text_tag("genre", 
 | 
			
		||||
    tagger.add_text_tag("genre",
 | 
			
		||||
      @spotify_searcher.find_genre(data["artists"][0]["id"].to_s))
 | 
			
		||||
    tagger.add_text_tag("track", data["track_number"].to_s)
 | 
			
		||||
    tagger.add_text_tag("disc", data["disc_number"].to_s)
 | 
			
		||||
 | 
			
		||||
    puts "Tagging metadata ..."
 | 
			
		||||
    tagger.save()
 | 
			
		||||
    tagger.save
 | 
			
		||||
    File.delete(temp_albumart_filename)
 | 
			
		||||
 | 
			
		||||
    puts %("#{data["name"].to_s}" by "#{data["artists"][0]["name"].to_s}" downloaded.)
 | 
			
		||||
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Will organize the song into the user's provided music directory as 
 | 
			
		||||
  # Will organize the song into the user's provided music directory as
 | 
			
		||||
  # music_directory > artist_name > album_name > song
 | 
			
		||||
  # Must be called AFTER the song has been downloaded.
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # s = Song.new("Bohemian Rhapsody", "Queen").grab_it()
 | 
			
		||||
  # s = Song.new("Bohemian Rhapsody", "Queen").grab_it
 | 
			
		||||
  # s.organize_it("/home/cooper/Music")
 | 
			
		||||
  # # Will move the mp3 file to 
 | 
			
		||||
  # # Will move the mp3 file to
 | 
			
		||||
  # # /home/cooper/Music/Queen/A Night At The Opera/1 - Bohemian Rhapsody.mp3
 | 
			
		||||
  # ```
 | 
			
		||||
  def organize_it(music_directory : String)
 | 
			
		||||
| 
						 | 
				
			
			@ -116,20 +114,20 @@ class Song
 | 
			
		|||
  # called.
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # Song.new(...).provide_metadata(...).grab_it()
 | 
			
		||||
  # Song.new(...).provide_metadata(...).grab_it
 | 
			
		||||
  # ```
 | 
			
		||||
  def provide_metadata(metadata : JSON::Any) : self
 | 
			
		||||
    @metadata = metadata
 | 
			
		||||
    return self
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Provide an already authenticated `SpotifySearcher` class. Useful to avoid 
 | 
			
		||||
  # Provide an already authenticated `SpotifySearcher` class. Useful to avoid
 | 
			
		||||
  # authenticating over and over again. Must be called if provide_metadata and
 | 
			
		||||
  # provide_client_keys are not called.
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # Song.new(...).provide_spotify(SpotifySearcher.new()
 | 
			
		||||
  #  .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it()
 | 
			
		||||
  # Song.new(...).provide_spotify(SpotifySearcher.new
 | 
			
		||||
  #   .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it
 | 
			
		||||
  # ```
 | 
			
		||||
  def provide_spotify(spotify : SpotifySearcher) : self
 | 
			
		||||
    @spotify_searcher = spotify
 | 
			
		||||
| 
						 | 
				
			
			@ -140,11 +138,11 @@ class Song
 | 
			
		|||
  # provide_spotify are not called.
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # Song.new(...).provide_client_keys("XXXXXXXXXX", "XXXXXXXXX").grab_it()
 | 
			
		||||
  # Song.new(...).provide_client_keys("XXXXXXXXXX", "XXXXXXXXX").grab_it
 | 
			
		||||
  # ```
 | 
			
		||||
  def provide_client_keys(client_id : String, client_secret : String) : self
 | 
			
		||||
    @client_id = client_id
 | 
			
		||||
    @client_secret = client_secret
 | 
			
		||||
    return self
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
class Logger
 | 
			
		||||
 | 
			
		||||
  @done_signal = "---DONE---"
 | 
			
		||||
 | 
			
		||||
  @command : String
 | 
			
		||||
| 
						 | 
				
			
			@ -11,16 +10,16 @@ class Logger
 | 
			
		|||
  def initialize(command : String, @log_name : String, @sleept = 0.01)
 | 
			
		||||
    # Have the command output its information to a log and after the command is
 | 
			
		||||
    # finished, append an end signal to the document
 | 
			
		||||
    @command = "#{command} > #{@log_name} " # standard output to log
 | 
			
		||||
    @command += "2> #{@log_name} && " # errors to log
 | 
			
		||||
    @command += "echo #{@done_signal} >> #{@log_name}" # 
 | 
			
		||||
    @command = "#{command} > #{@log_name} "            # standard output to log
 | 
			
		||||
    @command += "2> #{@log_name} && "                  # errors to log
 | 
			
		||||
    @command += "echo #{@done_signal} >> #{@log_name}" #
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Run @command in the background and pipe its output to the log file, with
 | 
			
		||||
  # something constantly monitoring the log file and yielding each new line to
 | 
			
		||||
  # the block call. Useful for changing the output of binaries you don't have 
 | 
			
		||||
  # the block call. Useful for changing the output of binaries you don't have
 | 
			
		||||
  # much control over.
 | 
			
		||||
  # Note that the created temp log will be deleted unless the command fails 
 | 
			
		||||
  # Note that the created temp log will be deleted unless the command fails
 | 
			
		||||
  # its exit or .start is called with delete_file: false
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +33,7 @@ class Logger
 | 
			
		|||
  #   end
 | 
			
		||||
  # end
 | 
			
		||||
  # ```
 | 
			
		||||
  def start(delete_file=true, &block) : Bool
 | 
			
		||||
  def start(delete_file = true, &block) : Bool
 | 
			
		||||
    # Delete the log if it already exists
 | 
			
		||||
    File.delete(@log_name) if File.exists?(@log_name)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +43,7 @@ class Logger
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    # Wait for the log file to be written to
 | 
			
		||||
    while !File.exists?(@log_name) 
 | 
			
		||||
    while !File.exists?(@log_name)
 | 
			
		||||
      sleep @sleept
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,12 +67,12 @@ class Logger
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    status = called.get()
 | 
			
		||||
    if status == true && delete_file == true 
 | 
			
		||||
      log.delete()
 | 
			
		||||
    status = called.get
 | 
			
		||||
    if status == true && delete_file == true
 | 
			
		||||
      log.delete
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return called.get()
 | 
			
		||||
    return called.get
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Reads each line of the file into an Array of Strings
 | 
			
		||||
| 
						 | 
				
			
			@ -86,4 +85,4 @@ class Logger
 | 
			
		|||
 | 
			
		||||
    return content
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ require "./logger"
 | 
			
		|||
require "../bottle/config"
 | 
			
		||||
 | 
			
		||||
module Ripper
 | 
			
		||||
 | 
			
		||||
  extend self
 | 
			
		||||
 | 
			
		||||
  BIN_LOC = Path[Config.binary_location]
 | 
			
		||||
| 
						 | 
				
			
			@ -11,23 +10,23 @@ module Ripper
 | 
			
		|||
  # Will create any directories that don't exist specified in *output_filename*
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # Ripper.download_mp3("https://youtube.com/watch?v=0xnciFWAqa0", 
 | 
			
		||||
  # Ripper.download_mp3("https://youtube.com/watch?v=0xnciFWAqa0",
 | 
			
		||||
  #   "Queen/A Night At The Opera/Bohemian Rhapsody.mp3")
 | 
			
		||||
  # ```
 | 
			
		||||
  def download_mp3(video_url : String, output_filename : String)
 | 
			
		||||
    ydl_loc = BIN_LOC.join("youtube-dl")
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    # remove the extension that will be added on by ydl
 | 
			
		||||
    output_filename = output_filename.split(".")[..-2].join(".")
 | 
			
		||||
 | 
			
		||||
    options = {
 | 
			
		||||
      "--output" => %("#{output_filename}.%(ext)s"), # auto-add correct ext
 | 
			
		||||
      # "--quiet" => "",
 | 
			
		||||
      "--verbose" => "",
 | 
			
		||||
      "--verbose"         => "",
 | 
			
		||||
      "--ffmpeg-location" => BIN_LOC,
 | 
			
		||||
      "--extract-audio" => "",
 | 
			
		||||
      "--audio-format" => "mp3",
 | 
			
		||||
      "--audio-quality" => "0",
 | 
			
		||||
      "--extract-audio"   => "",
 | 
			
		||||
      "--audio-format"    => "mp3",
 | 
			
		||||
      "--audio-quality"   => "0",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    command = ydl_loc.to_s + " " + video_url
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +34,6 @@ module Ripper
 | 
			
		|||
      command += " #{option} #{options[option]}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    l = Logger.new(command, ".ripper.log")
 | 
			
		||||
    o = RipperOutputCensor.new
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +62,5 @@ module Ripper
 | 
			
		|||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,10 +5,9 @@ require "../bottle/config"
 | 
			
		|||
# t = Tags.new("bohem rap.mp3")
 | 
			
		||||
# t.add_album_art("a night at the opera album cover.jpg")
 | 
			
		||||
# t.add_text_tag("title", "Bohemian Rhapsody")
 | 
			
		||||
# t.save()
 | 
			
		||||
# t.save
 | 
			
		||||
# ```
 | 
			
		||||
class Tags
 | 
			
		||||
 | 
			
		||||
  # TODO: export this path to a config file
 | 
			
		||||
  @BIN_LOC = Config.binary_location
 | 
			
		||||
  @query_args = [] of String
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +19,6 @@ class Tags
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    @query_args.push(%(-i "#{@filename}"))
 | 
			
		||||
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Add album art to the mp3. Album art must be added BEFORE text tags are.
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +50,7 @@ class Tags
 | 
			
		|||
 | 
			
		||||
    l = Logger.new(command, ".tagger.log")
 | 
			
		||||
    l.start { |line, start| }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
    File.delete(@filename)
 | 
			
		||||
    File.rename("_" + @filename, @filename)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -61,4 +59,4 @@ end
 | 
			
		|||
# a = Tags.new("test.mp3")
 | 
			
		||||
# a.add_text_tag("title", "Warwick Avenue")
 | 
			
		||||
# a.add_album_art("file.png")
 | 
			
		||||
# a.save()
 | 
			
		||||
# a.save()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ require "./bottle/cli"
 | 
			
		|||
 | 
			
		||||
def main
 | 
			
		||||
  cli = CLI.new(ARGV)
 | 
			
		||||
  cli.act_on_args()
 | 
			
		||||
  cli.act_on_args
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
main()
 | 
			
		||||
main()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,18 +11,18 @@ class SpotifySearcher
 | 
			
		|||
  @authorized = false
 | 
			
		||||
 | 
			
		||||
  # Saves an access token for future program use with spotify using client IDs.
 | 
			
		||||
  # Specs defined on spotify's developer api: 
 | 
			
		||||
  # Specs defined on spotify's developer api:
 | 
			
		||||
  # https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # SpotifySearcher.new().authorize("XXXXXXXXXX", "XXXXXXXXXX")
 | 
			
		||||
  # SpotifySearcher.new.authorize("XXXXXXXXXX", "XXXXXXXXXX")
 | 
			
		||||
  # ```
 | 
			
		||||
  def authorize(client_id : String, client_secret : String) : self
 | 
			
		||||
    auth_url = "https://accounts.spotify.com/api/token"
 | 
			
		||||
 | 
			
		||||
    headers = HTTP::Headers{
 | 
			
		||||
      "Authorization" => "Basic " + 
 | 
			
		||||
        Base64.strict_encode "#{client_id}:#{client_secret}"
 | 
			
		||||
      "Authorization" => "Basic " +
 | 
			
		||||
                         Base64.strict_encode "#{client_id}:#{client_secret}",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    payload = "grant_type=client_credentials"
 | 
			
		||||
| 
						 | 
				
			
			@ -31,9 +31,9 @@ class SpotifySearcher
 | 
			
		|||
    error_check(response)
 | 
			
		||||
 | 
			
		||||
    access_token = JSON.parse(response.body)["access_token"]
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    @access_header = HTTP::Headers{
 | 
			
		||||
      "Authorization" => "Bearer #{access_token}"
 | 
			
		||||
      "Authorization" => "Bearer #{access_token}",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @authorized = true
 | 
			
		||||
| 
						 | 
				
			
			@ -55,12 +55,11 @@ class SpotifySearcher
 | 
			
		|||
  # })
 | 
			
		||||
  # => {track metadata}
 | 
			
		||||
  # ```
 | 
			
		||||
  def find_item(item_type : String, item_parameters : Hash, offset=0, 
 | 
			
		||||
  limit=20) : JSON::Any?
 | 
			
		||||
 | 
			
		||||
  def find_item(item_type : String, item_parameters : Hash, offset = 0,
 | 
			
		||||
                limit = 20) : JSON::Any?
 | 
			
		||||
    query = generate_query(item_type, item_parameters, offset, limit)
 | 
			
		||||
 | 
			
		||||
    url = @root_url.join("search?q=#{query}").to_s()
 | 
			
		||||
    url = @root_url.join("search?q=#{query}").to_s
 | 
			
		||||
 | 
			
		||||
    response = HTTP::Client.get(url, headers: @access_header)
 | 
			
		||||
    error_check(response)
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +73,7 @@ class SpotifySearcher
 | 
			
		|||
    begin
 | 
			
		||||
      # this means no points were assigned so don't return the "best guess"
 | 
			
		||||
      if points[0][0] <= 0
 | 
			
		||||
        to_return = nil 
 | 
			
		||||
        to_return = nil
 | 
			
		||||
      else
 | 
			
		||||
        to_return = get_item(item_type, items[points[0][1]]["id"].to_s)
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -82,15 +81,15 @@ class SpotifySearcher
 | 
			
		|||
      to_return = nil
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
    if to_return == nil && item_type == "playlist"
 | 
			
		||||
      return find_user_playlist(
 | 
			
		||||
        item_parameters["username"], 
 | 
			
		||||
        item_parameters["username"],
 | 
			
		||||
        item_parameters["name"]
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
    return to_return
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -100,9 +99,8 @@ class SpotifySearcher
 | 
			
		|||
  # spotify_searcher.find_user_playlist("prakkillian", "the little man")
 | 
			
		||||
  # => {playlist metadata}
 | 
			
		||||
  # ```
 | 
			
		||||
  def find_user_playlist(username : String, name : String, offset=0,
 | 
			
		||||
  limit=20) : JSON::Any?
 | 
			
		||||
 | 
			
		||||
  def find_user_playlist(username : String, name : String, offset = 0,
 | 
			
		||||
                         limit = 20) : JSON::Any?
 | 
			
		||||
    url = "users/#{username}/playlists?limit=#{limit}&offset=#{offset}"
 | 
			
		||||
    url = @root_url.join(url).to_s
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -116,14 +114,14 @@ class SpotifySearcher
 | 
			
		|||
    items.as_a.each_index do |i|
 | 
			
		||||
      points.push([points_compare(items[i]["name"].to_s, name), i])
 | 
			
		||||
    end
 | 
			
		||||
    points.sort!{ |a, b| b[0] <=> a[0] }
 | 
			
		||||
    points.sort! { |a, b| b[0] <=> a[0] }
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      if points[0][0] < 3
 | 
			
		||||
        return find_user_playlist(username, name, offset + limit, limit)
 | 
			
		||||
      else
 | 
			
		||||
        return get_item("playlist", items[points[0][1]]["id"].to_s)
 | 
			
		||||
      end 
 | 
			
		||||
      end
 | 
			
		||||
    rescue IndexError
 | 
			
		||||
      return nil
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -132,18 +130,17 @@ class SpotifySearcher
 | 
			
		|||
  # Get the complete metadata of an item based off of its id
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # SpotifySearcher.new().authorize(...).get_item("artist", "1dfeR4HaWDbWqFHLkxsg1d")
 | 
			
		||||
  # SpotifySearcher.new.authorize(...).get_item("artist", "1dfeR4HaWDbWqFHLkxsg1d")
 | 
			
		||||
  # ```
 | 
			
		||||
  def get_item(item_type : String, id : String, offset=0, 
 | 
			
		||||
  limit=100) : JSON::Any
 | 
			
		||||
 | 
			
		||||
  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()
 | 
			
		||||
  
 | 
			
		||||
    url = @root_url.join(url).to_s
 | 
			
		||||
 | 
			
		||||
    response = HTTP::Client.get(url, headers: @access_header)
 | 
			
		||||
    error_check(response)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -156,12 +153,12 @@ class SpotifySearcher
 | 
			
		|||
  # insert ALL tracks from the playlist into the `JSON::Any`
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # SpotifySearcher.new().authorize(...).get_playlist("122Fc9gVuSZoksEjKEx7L0")
 | 
			
		||||
  # SpotifySearcher.new.authorize(...).get_playlist("122Fc9gVuSZoksEjKEx7L0")
 | 
			
		||||
  # ```
 | 
			
		||||
  def get_playlist(id, offset=0, limit=100) : JSON::Any
 | 
			
		||||
  def get_playlist(id, offset = 0, limit = 100) : JSON::Any
 | 
			
		||||
    url = "playlists/#{id}?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)
 | 
			
		||||
    error_check(response)
 | 
			
		||||
    body = JSON.parse(response.body)
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +166,7 @@ class SpotifySearcher
 | 
			
		|||
 | 
			
		||||
    more_tracks = body["tracks"]["total"].as_i > offset + limit
 | 
			
		||||
    if more_tracks
 | 
			
		||||
      return playlist_extension(parent, id, offset=offset + limit)
 | 
			
		||||
      return playlist_extension(parent, id, offset = offset + limit)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return body
 | 
			
		||||
| 
						 | 
				
			
			@ -177,10 +174,10 @@ class SpotifySearcher
 | 
			
		|||
 | 
			
		||||
  # 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
 | 
			
		||||
  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()
 | 
			
		||||
    url = @root_url.join(url).to_s
 | 
			
		||||
 | 
			
		||||
    response = HTTP::Client.get(url, headers: @access_header)
 | 
			
		||||
    error_check(response)
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +190,7 @@ class SpotifySearcher
 | 
			
		|||
 | 
			
		||||
    more_tracks = body["total"].as_i > offset + limit
 | 
			
		||||
    if more_tracks
 | 
			
		||||
      return playlist_extension(parent, id, offset=offset + limit)
 | 
			
		||||
      return playlist_extension(parent, id, offset = offset + limit)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return JSON.parse(parent.to_json)
 | 
			
		||||
| 
						 | 
				
			
			@ -202,10 +199,9 @@ class SpotifySearcher
 | 
			
		|||
  # Find the genre of an artist based off of their id
 | 
			
		||||
  #
 | 
			
		||||
  # ```
 | 
			
		||||
  # SpotifySearcher.new().authorize(...).find_genre("1dfeR4HaWDbWqFHLkxsg1d")
 | 
			
		||||
  # SpotifySearcher.new.authorize(...).find_genre("1dfeR4HaWDbWqFHLkxsg1d")
 | 
			
		||||
  # ```
 | 
			
		||||
  def find_genre(id : String) : String
 | 
			
		||||
 | 
			
		||||
    genre = get_item("artist", id)["genres"][0].to_s
 | 
			
		||||
    genre = genre.split(" ").map { |x| x.capitalize }.join(" ")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -216,15 +212,15 @@ class SpotifySearcher
 | 
			
		|||
  private def error_check(response : HTTP::Client::Response) : Nil
 | 
			
		||||
    if response.status_code != 200
 | 
			
		||||
      raise("There was an error with your request.\n" +
 | 
			
		||||
            "Status code: #{response.status_code}\n" + 
 | 
			
		||||
            "Status code: #{response.status_code}\n" +
 | 
			
		||||
            "Response: \n#{response.body}")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  # Generates url to run a GET request against to the Spotify open API
 | 
			
		||||
  # Returns a `String.`
 | 
			
		||||
  private def generate_query(item_type : String, item_parameters : Hash,
 | 
			
		||||
  offset : Int32, limit : Int32) : String
 | 
			
		||||
                             offset : Int32, limit : Int32) : String
 | 
			
		||||
    query = ""
 | 
			
		||||
 | 
			
		||||
    # parameter keys to exclude in the api request. These values will be put
 | 
			
		||||
| 
						 | 
				
			
			@ -234,21 +230,20 @@ class SpotifySearcher
 | 
			
		|||
    item_parameters.keys.each do |k|
 | 
			
		||||
      # This will map album and track names from the name key to the query
 | 
			
		||||
      if k == "name"
 | 
			
		||||
 | 
			
		||||
        # will remove the "name:<title>" param from the query
 | 
			
		||||
        if item_type == "playlist" 
 | 
			
		||||
        if item_type == "playlist"
 | 
			
		||||
          query += item_parameters[k].gsub(" ", "+") + "+"
 | 
			
		||||
        else
 | 
			
		||||
          query += param_encode(item_type, item_parameters[k])
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      # check if the key is to be excluded
 | 
			
		||||
        # check if the key is to be excluded
 | 
			
		||||
      elsif query_exclude.includes?(k)
 | 
			
		||||
        next
 | 
			
		||||
  
 | 
			
		||||
      # if it's none of the above, treat it normally
 | 
			
		||||
      # NOTE: playlist names will be inserted into the query normally, without
 | 
			
		||||
      # a parameter.
 | 
			
		||||
 | 
			
		||||
        # if it's none of the above, treat it normally
 | 
			
		||||
        # NOTE: playlist names will be inserted into the query normally, without
 | 
			
		||||
        # a parameter.
 | 
			
		||||
      else
 | 
			
		||||
        query += param_encode(k, item_parameters[k])
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			@ -264,7 +259,7 @@ class SpotifySearcher
 | 
			
		|||
  # Meant to find the item that the user desires.
 | 
			
		||||
  # Returns an `Array` of `Array(Int32)` or [[3, 1], [...], ...]
 | 
			
		||||
  private def rank_items(items : Array,
 | 
			
		||||
  parameters : Hash) : Array(Array(Int32))
 | 
			
		||||
                         parameters : Hash) : Array(Array(Int32))
 | 
			
		||||
    points = [] of Array(Int32)
 | 
			
		||||
    index = 0
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +269,7 @@ class SpotifySearcher
 | 
			
		|||
      # Think about whether this following logic is worth having in one method.
 | 
			
		||||
      # Is it nice to have a single method that handles it all or having a few
 | 
			
		||||
      # methods for each of the item types? (track, album, playlist)
 | 
			
		||||
      parameters.keys.each do |k| 
 | 
			
		||||
      parameters.keys.each do |k|
 | 
			
		||||
        val = parameters[k]
 | 
			
		||||
 | 
			
		||||
        # The key to compare to for artist
 | 
			
		||||
| 
						 | 
				
			
			@ -286,7 +281,7 @@ class SpotifySearcher
 | 
			
		|||
        if k == "username"
 | 
			
		||||
          pts_to_add = points_compare(item["owner"]["display_name"].to_s, val)
 | 
			
		||||
          pts += pts_to_add
 | 
			
		||||
          pts += -10 if pts_to_add == 0 
 | 
			
		||||
          pts += -10 if pts_to_add == 0
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        # The key regardless of whether item is track, album,or playlist
 | 
			
		||||
| 
						 | 
				
			
			@ -299,12 +294,12 @@ class SpotifySearcher
 | 
			
		|||
      index += 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    points.sort!{ |a, b| b[0] <=> a[0] }
 | 
			
		||||
    points.sort! { |a, b| b[0] <=> a[0] }
 | 
			
		||||
 | 
			
		||||
    return points
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns an `Int` based off the number of points worth assigning to the 
 | 
			
		||||
  # Returns an `Int` based off the number of points worth assigning to the
 | 
			
		||||
  # matchiness of the string. First the strings are downcased and then all
 | 
			
		||||
  # nonalphanumeric characters are stripped.
 | 
			
		||||
  # If the strings are the exact same, return 3 pts.
 | 
			
		||||
| 
						 | 
				
			
			@ -332,18 +327,16 @@ class SpotifySearcher
 | 
			
		|||
  private def param_encode(key : String, value : String) : String
 | 
			
		||||
    return key.gsub(" ", "+") + ":" + value.gsub(" ", "+") + "+"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# puts SpotifySearcher.new()
 | 
			
		||||
#   .authorize("XXXXXXXXXXXXXXX", 
 | 
			
		||||
#   .authorize("XXXXXXXXXXXXXXX",
 | 
			
		||||
#              "XXXXXXXXXXXXXXX")
 | 
			
		||||
#   .find_item("playlist", {
 | 
			
		||||
#     "name" => "Brain Food", 
 | 
			
		||||
#     "username" => "spotify" 
 | 
			
		||||
#     "name" => "Brain Food",
 | 
			
		||||
#     "username" => "spotify"
 | 
			
		||||
#     # "name " => "A Night At The Opera",
 | 
			
		||||
#     # "artist" => "Queen"
 | 
			
		||||
#     # "track" => "Bohemian Rhapsody",
 | 
			
		||||
#     # "artist" => "Queen"
 | 
			
		||||
#   })
 | 
			
		||||
#   })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,24 +1,22 @@
 | 
			
		|||
require "http"
 | 
			
		||||
require "xml"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
module Youtube
 | 
			
		||||
 | 
			
		||||
  extend self
 | 
			
		||||
 | 
			
		||||
  VALID_LINK_CLASSES = [
 | 
			
		||||
    "yt-simple-endpoint style-scope ytd-video-renderer",
 | 
			
		||||
    "yt-uix-tile-link yt-ui-ellipsis yt-ui-ellipsis-2 yt-uix-sessionlink      spf-link "
 | 
			
		||||
    "yt-uix-tile-link yt-ui-ellipsis yt-ui-ellipsis-2 yt-uix-sessionlink      spf-link ",
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  GARBAGE_PHRASES = [
 | 
			
		||||
    "cover", "album", "live", "clean", "version", "full", "full album", "row",
 | 
			
		||||
    "at", "@", "session", "how to", "npr music", "reimagined", "hr version",
 | 
			
		||||
    "trailer"
 | 
			
		||||
    "trailer",
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  GOLDEN_PHRASES = [
 | 
			
		||||
    "official video", "official music video"
 | 
			
		||||
    "official video", "official music video",
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  # Finds a youtube url based off of the given information.
 | 
			
		||||
| 
						 | 
				
			
			@ -30,11 +28,11 @@ module Youtube
 | 
			
		|||
  # Youtube.find_url("Bohemian Rhapsody", "Queen")
 | 
			
		||||
  # => "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
 | 
			
		||||
  # ```
 | 
			
		||||
  def find_url(song_name : String, artist_name : String, search_terms = "", 
 | 
			
		||||
  download_first = false) : String?
 | 
			
		||||
  def find_url(song_name : String, artist_name : String, search_terms = "",
 | 
			
		||||
               download_first = false) : String?
 | 
			
		||||
    query = (song_name + " " + artist_name + " " + search_terms).strip.gsub(" ", "+")
 | 
			
		||||
 | 
			
		||||
    url = "https://www.youtube.com/results?search_query=" + query 
 | 
			
		||||
    url = "https://www.youtube.com/results?search_query=" + query
 | 
			
		||||
 | 
			
		||||
    response = HTTP::Client.get(url)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,11 +59,11 @@ module Youtube
 | 
			
		|||
  # Will rank videos according to their title and the user input
 | 
			
		||||
  # Return:
 | 
			
		||||
  # [
 | 
			
		||||
  #   {"points" => x, "index" => x}, 
 | 
			
		||||
  #   ...  
 | 
			
		||||
  #   {"points" => x, "index" => x},
 | 
			
		||||
  #   ...
 | 
			
		||||
  # ]
 | 
			
		||||
  private def rank_videos(song_name : String, artist_name : String, 
 | 
			
		||||
  query : String, nodes : Array(XML::Node)) : Array(Hash(String, Int32))
 | 
			
		||||
  private def rank_videos(song_name : String, artist_name : String,
 | 
			
		||||
                          query : String, nodes : Array(XML::Node)) : Array(Hash(String, Int32))
 | 
			
		||||
    points = [] of Hash(String, Int32)
 | 
			
		||||
    index = 0
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,14 +76,13 @@ module Youtube
 | 
			
		|||
 | 
			
		||||
      points.push({
 | 
			
		||||
        "points" => pts,
 | 
			
		||||
        "index" => index
 | 
			
		||||
        "index"  => index,
 | 
			
		||||
      })
 | 
			
		||||
      index += 1
 | 
			
		||||
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # Sort first by points and then by original index of the song
 | 
			
		||||
    points.sort!{ |a, b| 
 | 
			
		||||
    points.sort! { |a, b|
 | 
			
		||||
      if b["points"] == a["points"]
 | 
			
		||||
        a["index"] <=> b["index"]
 | 
			
		||||
      else
 | 
			
		||||
| 
						 | 
				
			
			@ -96,11 +93,11 @@ module Youtube
 | 
			
		|||
    return points
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Returns an `Int` based off the number of points worth assigning to the 
 | 
			
		||||
  # Returns an `Int` based off the number of points worth assigning to the
 | 
			
		||||
  # matchiness of the string. First the strings are downcased and then all
 | 
			
		||||
  # nonalphanumeric characters are stripped.
 | 
			
		||||
  # If *item1* includes *item2*, return 3 pts.
 | 
			
		||||
  # If after the items have been blanked, *item1* includes *item2*, 
 | 
			
		||||
  # If after the items have been blanked, *item1* includes *item2*,
 | 
			
		||||
  #   return 1 pts.
 | 
			
		||||
  # Else, return 0 pts.
 | 
			
		||||
  private def points_compare(item1 : String, item2 : String) : Int32
 | 
			
		||||
| 
						 | 
				
			
			@ -118,10 +115,10 @@ module Youtube
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Checks if there are any phrases in the title of the video that would 
 | 
			
		||||
  # Checks if there are any phrases in the title of the video that would
 | 
			
		||||
  # indicate audio having what we want.
 | 
			
		||||
  # *video_name* is the title of the video, and *query* is what the user the
 | 
			
		||||
  # program searched for. *query* is needed in order to make sure we're not 
 | 
			
		||||
  # program searched for. *query* is needed in order to make sure we're not
 | 
			
		||||
  # subtracting points from something that's naturally in the title
 | 
			
		||||
  private def count_buzzphrases(query : String, video_name : String) : Int32
 | 
			
		||||
    good_phrases = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +126,7 @@ module Youtube
 | 
			
		|||
 | 
			
		||||
    GOLDEN_PHRASES.each do |gold_phrase|
 | 
			
		||||
      gold_phrase = gold_phrase.downcase.gsub(/[^a-z0-9]/, "")
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
      if query.downcase.gsub(/[^a-z0-9]/, "").includes?(gold_phrase)
 | 
			
		||||
        next
 | 
			
		||||
      elsif video_name.downcase.gsub(/[^a-z0-9]/, "").includes?(gold_phrase)
 | 
			
		||||
| 
						 | 
				
			
			@ -180,4 +177,4 @@ module Youtube
 | 
			
		|||
    end
 | 
			
		||||
    return false
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue