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