mirror of
https://github.com/cooperhammond/irs.git
synced 2024-12-22 17:35:28 +00:00
Merge pull request #83 from imsamuka/fix-options
Fix options and add --ask-skip
This commit is contained in:
commit
390d59b9a0
|
@ -21,7 +21,8 @@ class CLI
|
||||||
[["-A", "--album"], "album", "string"],
|
[["-A", "--album"], "album", "string"],
|
||||||
[["-p", "--playlist"], "playlist", "string"],
|
[["-p", "--playlist"], "playlist", "string"],
|
||||||
[["-u", "--url"], "url", "string"],
|
[["-u", "--url"], "url", "string"],
|
||||||
[["-S", "--select"], "select", "bool"]
|
[["-S", "--select"], "select", "bool"],
|
||||||
|
[["--ask-skip"], "ask_skip", "bool"]
|
||||||
]
|
]
|
||||||
|
|
||||||
@args : Hash(String, String)
|
@args : Hash(String, String)
|
||||||
|
@ -50,10 +51,11 @@ class CLI
|
||||||
#{Style.blue "-s, --song <song>"} Specify song name to download
|
#{Style.blue "-s, --song <song>"} Specify song name to download
|
||||||
#{Style.blue "-A, --album <album>"} Specify the album name to download
|
#{Style.blue "-A, --album <album>"} Specify the album name to download
|
||||||
#{Style.blue "-p, --playlist <playlist>"} Specify the playlist name to download
|
#{Style.blue "-p, --playlist <playlist>"} Specify the playlist name to download
|
||||||
#{Style.blue "-u, --url [<url>]"} Specify the youtube url to download from
|
#{Style.blue "-u, --url <url>"} Specify the youtube url to download from
|
||||||
#{Style.blue " "} (for single songs, include as an command-line
|
#{Style.blue " "} (for albums and playlists, the command-line
|
||||||
#{Style.blue " "} argument, for albums or playlists do not)
|
#{Style.blue " "} argument is ignored, and it should be '')
|
||||||
#{Style.blue "-S, --select"} Use a menu to choose each song's video source
|
#{Style.blue "-S, --select"} Use a menu to choose each song's video source
|
||||||
|
#{Style.blue "--ask-skip"} Before every playlist/album song, ask to skip
|
||||||
|
|
||||||
#{Style.bold "Examples:"}
|
#{Style.bold "Examples:"}
|
||||||
$ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
|
$ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
|
||||||
|
|
|
@ -29,6 +29,8 @@ 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(flags = {} of String => String)
|
def grab_it(flags = {} of String => String)
|
||||||
ask_url = flags["url"]?
|
ask_url = flags["url"]?
|
||||||
|
ask_skip = flags["ask_skip"]?
|
||||||
|
is_playlist = flags["playlist"]?
|
||||||
|
|
||||||
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.")
|
||||||
|
@ -45,22 +47,28 @@ abstract class SpotifyList
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
contents.each do |datum|
|
contents.each do |datum|
|
||||||
|
i += 1
|
||||||
if datum["track"]?
|
if datum["track"]?
|
||||||
datum = datum["track"]
|
datum = datum["track"]
|
||||||
end
|
end
|
||||||
|
|
||||||
data = organize_song_metadata(list, datum)
|
data = organize_song_metadata(list, datum)
|
||||||
|
|
||||||
song = Song.new(data["name"].to_s, data["artists"][0]["name"].to_s)
|
s_name = data["name"].to_s
|
||||||
|
s_artist = data["artists"][0]["name"].to_s
|
||||||
|
|
||||||
|
song = Song.new(s_name, s_artist)
|
||||||
song.provide_spotify(@spotify_searcher)
|
song.provide_spotify(@spotify_searcher)
|
||||||
song.provide_metadata(data)
|
song.provide_metadata(data)
|
||||||
|
|
||||||
puts Style.bold("[#{data["track_number"]}/#{contents.size}]")
|
puts Style.bold("[#{i}/#{contents.size}]")
|
||||||
song.grab_it(flags: flags)
|
|
||||||
|
|
||||||
organize(song)
|
unless ask_skip && skip?(s_name, s_artist, is_playlist)
|
||||||
|
song.grab_it(flags: flags)
|
||||||
i += 1
|
organize(song)
|
||||||
|
else
|
||||||
|
puts "Skipping..."
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -69,6 +77,13 @@ abstract class SpotifyList
|
||||||
@spotify_searcher.authorize(client_key, client_secret)
|
@spotify_searcher.authorize(client_key, client_secret)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def skip?(name, artist, is_playlist)
|
||||||
|
print "Skip #{Style.blue name}" +
|
||||||
|
(is_playlist ? " (by #{Style.green artist})": "") + "? "
|
||||||
|
response = gets
|
||||||
|
return response && response.lstrip.downcase.starts_with? "y"
|
||||||
|
end
|
||||||
|
|
||||||
private def outputter(key : String, index : Int32)
|
private def outputter(key : String, index : Int32)
|
||||||
text = @outputs[key][index]
|
text = @outputs[key][index]
|
||||||
.gsub("%l", @list_name)
|
.gsub("%l", @list_name)
|
||||||
|
|
|
@ -53,12 +53,12 @@ class Song
|
||||||
|
|
||||||
# Find, downloads, and tags the mp3 song that this class represents.
|
# Find, downloads, and tags the mp3 song that this class represents.
|
||||||
# Optionally takes a youtube URL to download from
|
# Optionally takes a youtube URL to download from
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# Song.new("Bohemian Rhapsody", "Queen").grab_it
|
# Song.new("Bohemian Rhapsody", "Queen").grab_it
|
||||||
# ```
|
# ```
|
||||||
def grab_it(url : (String | Nil) = nil, flags = {} of String => String)
|
def grab_it(url : (String | Nil) = nil, flags = {} of String => String)
|
||||||
ask_url = flags["url"]?
|
passed_url : (String | Nil) = flags["url"]?
|
||||||
select_link = flags["select"]?
|
select_link = flags["select"]?
|
||||||
|
|
||||||
outputter("intro", 0)
|
outputter("intro", 0)
|
||||||
|
@ -92,11 +92,15 @@ class Song
|
||||||
@artist_name = data["artists"][0]["name"].as_s
|
@artist_name = data["artists"][0]["name"].as_s
|
||||||
@filename = "#{Pattern.parse(Config.filename_pattern, data)}.mp3"
|
@filename = "#{Pattern.parse(Config.filename_pattern, data)}.mp3"
|
||||||
|
|
||||||
if ask_url
|
if passed_url
|
||||||
outputter("url", 4)
|
if passed_url.strip != ""
|
||||||
url = gets
|
url = passed_url
|
||||||
if !url.nil? && url.strip == ""
|
else
|
||||||
url = nil
|
outputter("url", 4)
|
||||||
|
url = gets
|
||||||
|
if !url.nil? && url.strip == ""
|
||||||
|
url = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,8 +115,9 @@ class Song
|
||||||
outputter("url", 1)
|
outputter("url", 1)
|
||||||
else
|
else
|
||||||
outputter("url", 2)
|
outputter("url", 2)
|
||||||
if !Youtube.is_valid_url(url)
|
url = Youtube.validate_url(url)
|
||||||
raise("The url '#{url}' is an invalid youtube URL " +
|
if !url
|
||||||
|
raise("The url is an invalid youtube URL " +
|
||||||
"Check the URL and try again")
|
"Check the URL and try again")
|
||||||
end
|
end
|
||||||
outputter("url", 3)
|
outputter("url", 3)
|
||||||
|
@ -130,7 +135,7 @@ class Song
|
||||||
outputter("albumart", 0)
|
outputter("albumart", 0)
|
||||||
|
|
||||||
# check if song's metadata has been modded in playlist, update artist accordingly
|
# check if song's metadata has been modded in playlist, update artist accordingly
|
||||||
if data["artists"][-1]["owner"]?
|
if data["artists"][-1]["owner"]?
|
||||||
@artist = data["artists"][-1]["name"].as_s
|
@artist = data["artists"][-1]["name"].as_s
|
||||||
else
|
else
|
||||||
@artist = data["artists"][0]["name"].as_s
|
@artist = data["artists"][0]["name"].as_s
|
||||||
|
|
|
@ -60,9 +60,10 @@ class SpotifySearcher
|
||||||
# ```
|
# ```
|
||||||
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)
|
||||||
|
|
||||||
url = @root_url.join("search?q=#{query}").to_s
|
url = "search?q=#{query}&type=#{item_type}&limit=#{limit}&offset=#{offset}"
|
||||||
|
url = @root_url.join(url).to_s
|
||||||
|
|
||||||
response = HTTP::Client.get(url, headers: @access_header)
|
response = HTTP::Client.get(url, headers: @access_header)
|
||||||
error_check(response)
|
error_check(response)
|
||||||
|
@ -228,8 +229,7 @@ class SpotifySearcher
|
||||||
|
|
||||||
# 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) : 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
|
||||||
|
@ -241,9 +241,9 @@ class SpotifySearcher
|
||||||
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] + "+"
|
||||||
else
|
else
|
||||||
query += param_encode(item_type, item_parameters[k])
|
query += as_field(item_type, item_parameters[k])
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if the key is to be excluded
|
# check if the key is to be excluded
|
||||||
|
@ -254,14 +254,21 @@ class SpotifySearcher
|
||||||
# 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 += as_field(k, item_parameters[k])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# extra api info
|
return URI.encode(query.rchop("+"))
|
||||||
query += "&type=#{item_type}&limit=#{limit}&offset=#{offset}"
|
end
|
||||||
|
|
||||||
return query
|
# Returns a `String` encoded for the spotify api
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# query_encode("album", "A Night At The Opera")
|
||||||
|
# => "album:A Night At The Opera+"
|
||||||
|
# ```
|
||||||
|
private def as_field(key, value) : String
|
||||||
|
return "#{key}:#{value}+"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Ranks the given items based off of the info from parameters.
|
# Ranks the given items based off of the info from parameters.
|
||||||
|
@ -327,15 +334,6 @@ class SpotifySearcher
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a `String` encoded for the spotify api
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# query_encode("album", "A Night At The Opera")
|
|
||||||
# => "album:A+Night+At+The+Opera"
|
|
||||||
# ```
|
|
||||||
private def param_encode(key : String, value : String) : String
|
|
||||||
return key.gsub(" ", "+") + ":" + value.gsub(" ", "+") + "+"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# puts SpotifySearcher.new()
|
# puts SpotifySearcher.new()
|
||||||
|
|
|
@ -34,18 +34,15 @@ module Youtube
|
||||||
|
|
||||||
search_terms = Config.search_terms
|
search_terms = Config.search_terms
|
||||||
|
|
||||||
download_first = flags["dl_first"]?
|
|
||||||
select_link = flags["select"]?
|
select_link = flags["select"]?
|
||||||
|
|
||||||
song_name = spotify_metadata["name"].as_s
|
song_name = spotify_metadata["name"].as_s
|
||||||
artist_name = spotify_metadata["artists"][0]["name"].as_s
|
artist_name = spotify_metadata["artists"][0]["name"].as_s
|
||||||
|
|
||||||
human_query = song_name + " " + artist_name + " " + search_terms.strip
|
human_query = "#{song_name} #{artist_name} #{search_terms.strip}"
|
||||||
url_query = human_query.gsub(" ", "+")
|
params = HTTP::Params.encode({"search_query" => human_query})
|
||||||
|
|
||||||
url = "https://www.youtube.com/results?search_query=" + url_query
|
response = HTTP::Client.get("https://www.youtube.com/results?#{params}")
|
||||||
|
|
||||||
response = HTTP::Client.get(url)
|
|
||||||
|
|
||||||
yt_metadata = get_yt_search_metadata(response.body)
|
yt_metadata = get_yt_search_metadata(response.body)
|
||||||
|
|
||||||
|
@ -55,19 +52,14 @@ module Youtube
|
||||||
end
|
end
|
||||||
|
|
||||||
root = "https://youtube.com"
|
root = "https://youtube.com"
|
||||||
|
|
||||||
if download_first
|
|
||||||
return root + yt_metadata[0]["href"]
|
|
||||||
end
|
|
||||||
|
|
||||||
ranked = Ranker.rank_videos(spotify_metadata, yt_metadata, human_query)
|
ranked = Ranker.rank_videos(spotify_metadata, yt_metadata, human_query)
|
||||||
|
|
||||||
if select_link
|
if select_link
|
||||||
return root + select_link_menu(spotify_metadata, yt_metadata)
|
return root + select_link_menu(spotify_metadata, yt_metadata)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
puts Style.dim(" Video: ") + yt_metadata[ranked[0]["index"]]["title"]
|
||||||
return root + yt_metadata[ranked[0]["index"]]["href"]
|
return root + yt_metadata[ranked[0]["index"]]["href"]
|
||||||
rescue IndexError
|
rescue IndexError
|
||||||
return nil
|
return nil
|
||||||
|
@ -75,12 +67,12 @@ module Youtube
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# Presents a menu with song info for the user to choose which url they want to download
|
# Presents a menu with song info for the user to choose which url they want to download
|
||||||
private def select_link_menu(spotify_metadata : JSON::Any,
|
private def select_link_menu(spotify_metadata : JSON::Any,
|
||||||
yt_metadata : YT_METADATA_CLASS) : String
|
yt_metadata : YT_METADATA_CLASS) : String
|
||||||
puts Style.dim(" Spotify info: ") +
|
puts Style.dim(" Spotify info: ") +
|
||||||
Style.bold("\"" + spotify_metadata["name"].to_s) + "\" by \"" +
|
Style.bold("\"" + spotify_metadata["name"].to_s) + "\" by \"" +
|
||||||
Style.bold(spotify_metadata["artists"][0]["name"].to_s + "\"") +
|
Style.bold(spotify_metadata["artists"][0]["name"].to_s + "\"") +
|
||||||
" @ " + Style.blue((spotify_metadata["duration_ms"].as_i / 1000).to_i.to_s) + "s"
|
" @ " + Style.blue((spotify_metadata["duration_ms"].as_i / 1000).to_i.to_s) + "s"
|
||||||
puts " Choose video to download:"
|
puts " Choose video to download:"
|
||||||
|
@ -107,11 +99,11 @@ module Youtube
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return yt_metadata[input]["href"]
|
return yt_metadata[input-1]["href"]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Finds valid video links from a `HTTP::Client.get` request
|
# Finds valid video links from a `HTTP::Client.get` request
|
||||||
# Returns an `Array` of `NODES_CLASS` containing additional metadata from Youtube
|
# Returns an `Array` of `NODES_CLASS` containing additional metadata from Youtube
|
||||||
private def get_yt_search_metadata(response_body : String) : YT_METADATA_CLASS
|
private def get_yt_search_metadata(response_body : String) : YT_METADATA_CLASS
|
||||||
yt_initial_data : JSON::Any = JSON.parse("{}")
|
yt_initial_data : JSON::Any = JSON.parse("{}")
|
||||||
|
@ -170,42 +162,39 @@ module Youtube
|
||||||
return video_metadata
|
return video_metadata
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks if the given URL is a valid youtube URL
|
# Returns as a valid URL if possible
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# Youtube.is_valid_url("https://www.youtube.com/watch?v=NOTANACTUALVIDEOID")
|
# Youtube.validate_url("https://www.youtube.com/watch?v=NOTANACTUALVIDEOID")
|
||||||
# => false
|
# => nil
|
||||||
# ```
|
# ```
|
||||||
def is_valid_url(url : String) : Bool
|
def validate_url(url : String) : String | Nil
|
||||||
uri = URI.parse url
|
uri = URI.parse url
|
||||||
|
return nil if !uri
|
||||||
|
|
||||||
# is it a video on youtube, with a query
|
|
||||||
query = uri.query
|
query = uri.query
|
||||||
if uri.host != "www.youtube.com" || uri.path != "/watch" || !query
|
return nil if !query
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
queries = query.split('&')
|
|
||||||
|
|
||||||
# find the video ID
|
# find the video ID
|
||||||
i = 0
|
vID = nil
|
||||||
while i < queries.size
|
query.split('&').each do |q|
|
||||||
if queries[i].starts_with?("v=")
|
if q.starts_with?("v=")
|
||||||
vID = queries[i][2..-1]
|
vID = q[2..-1]
|
||||||
break
|
|
||||||
end
|
end
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if !vID
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
|
return nil if !vID
|
||||||
|
|
||||||
|
url = "https://www.youtube.com/watch?v=#{vID}"
|
||||||
|
|
||||||
# this is an internal endpoint to validate the video ID
|
# this is an internal endpoint to validate the video ID
|
||||||
response = HTTP::Client.get "https://www.youtube.com/get_video_info?video_id=#{vID}"
|
params = HTTP::Params.encode({"format" => "json", "url" => url})
|
||||||
|
response = HTTP::Client.get "https://www.youtube.com/oembed?#{params}"
|
||||||
|
return nil unless response.success?
|
||||||
|
|
||||||
return response.body.includes?("status=ok")
|
res_json = JSON.parse(response.body)
|
||||||
|
title = res_json["title"].as_s
|
||||||
|
puts Style.dim(" Video: ") + title
|
||||||
|
|
||||||
|
return url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue