mirror of
https://github.com/cooperhammond/irs.git
synced 2025-01-02 19:15:26 +00:00
Initial CLI done!
This commit is contained in:
parent
acd2abb1d0
commit
76a624d32f
138
src/bottle/cli.cr
Normal file
138
src/bottle/cli.cr
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
require "./version"
|
||||||
|
require "./styles"
|
||||||
|
|
||||||
|
require "../glue/song"
|
||||||
|
|
||||||
|
|
||||||
|
class CLI
|
||||||
|
|
||||||
|
# layout:
|
||||||
|
# [[shortflag, longflag], key, type]
|
||||||
|
@options = [
|
||||||
|
[["-h", "--help"], "help", "bool"],
|
||||||
|
[["-v", "--version"], "version", "bool"],
|
||||||
|
[["-s", "--song"], "song", "string"],
|
||||||
|
[["-a", "--artist"], "artist", "string"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@args : Hash(String, String)
|
||||||
|
|
||||||
|
def initialize(argv : Array(String))
|
||||||
|
@args = parse_args(argv)
|
||||||
|
end
|
||||||
|
|
||||||
|
def version
|
||||||
|
puts "irs v#{IRS::VERSION}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def help
|
||||||
|
msg = <<-EOP
|
||||||
|
#{Style.bold "Usage: irs [-h] [-v] [-s <song> -a <artist>]"}
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
#{Style.blue "-h, --help"} Show this help message and exit
|
||||||
|
#{Style.blue "-v, --version"} Show the program version and exit
|
||||||
|
#{Style.blue "-s, --song <song>"} Specify song name for downloading
|
||||||
|
#{Style.blue "-a, --artist <artist>"} Specify artist name for downloading
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
|
||||||
|
#{Style.dim %(# => downloads the song "Bohemian Rhapsody" by "Queen")}
|
||||||
|
$ #{Style.green %(irs --album "Demon Days" --artist "Gorillaz")}
|
||||||
|
#{Style.dim %(# => downloads the album "Demon Days" by "Gorillaz")}
|
||||||
|
|
||||||
|
This project is licensed under the GNU GPL.
|
||||||
|
Project page: <github.com/cooperhammond/irs>
|
||||||
|
EOP
|
||||||
|
|
||||||
|
puts msg
|
||||||
|
end
|
||||||
|
|
||||||
|
def act_on_args
|
||||||
|
if @args["help"]?
|
||||||
|
help
|
||||||
|
exit
|
||||||
|
elsif @args["version"]?
|
||||||
|
version
|
||||||
|
exit
|
||||||
|
elsif @args["song"]? && @args["artist"]?
|
||||||
|
s = Song.new(@args["song"], @args["artist"])
|
||||||
|
s.provide_client_keys("e4198f6a3f7b48029366f22528b5dc66", "ba057d0621a5496bbb64edccf758bde5")
|
||||||
|
s.grab_it()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_args(argv : Array(String)) : Hash(String, String)
|
||||||
|
arguments = {} of String => String
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
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
|
||||||
|
pass_next_arg = false
|
||||||
|
i += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
flag = [] of Array(String) | String
|
||||||
|
valid_flag = false
|
||||||
|
|
||||||
|
@options.each do |option|
|
||||||
|
if option[0].includes?(arg)
|
||||||
|
flag = option
|
||||||
|
valid_flag = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ensure the flag is actually defined
|
||||||
|
if !valid_flag
|
||||||
|
arg_error argv, i, %("#{arg}" is an invalid flag or argument.)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ensure there's an argument if the program needs one
|
||||||
|
if flag[2] == "string" && i + 1 > argv.size
|
||||||
|
arg_error argv, i, %("#{arg}" needs an argument.)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
key = flag[1].as(String)
|
||||||
|
if flag[2] == "string"
|
||||||
|
arguments[key] = argv[i + 1]
|
||||||
|
pass_next_arg = true
|
||||||
|
elsif flag[2] == "bool"
|
||||||
|
arguments[key] = "true"
|
||||||
|
end
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
end
|
||||||
|
|
||||||
|
private def arg_error(argv : Array(String), arg : Int32, msg : String) : Nil
|
||||||
|
precursor = "irs "
|
||||||
|
|
||||||
|
start = argv[..arg - 1]
|
||||||
|
last = argv[arg + 1..]
|
||||||
|
|
||||||
|
distance = (precursor + start.join(" ")).size
|
||||||
|
|
||||||
|
print Style.dim(precursor + start.join(" "))
|
||||||
|
print Style.bold(Style.red(" " + argv[arg]).to_s)
|
||||||
|
puts Style.dim (" " + last.join(" "))
|
||||||
|
|
||||||
|
(0..distance).each do |i|
|
||||||
|
print " "
|
||||||
|
end
|
||||||
|
puts "^"
|
||||||
|
|
||||||
|
puts Style.red(Style.bold(msg).to_s)
|
||||||
|
puts "Type `irs -h` to see usage."
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
0
src/bottle/config.cr
Normal file
0
src/bottle/config.cr
Normal file
23
src/bottle/styles.cr
Normal file
23
src/bottle/styles.cr
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
|
class Style
|
||||||
|
def self.bold(txt)
|
||||||
|
txt.colorize.mode(:bold)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.dim(txt)
|
||||||
|
txt.colorize.mode(:dim)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.blue(txt)
|
||||||
|
txt.colorize(:light_blue)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.green(txt)
|
||||||
|
txt.colorize(:light_green)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.red(txt)
|
||||||
|
txt.colorize(:light_red)
|
||||||
|
end
|
||||||
|
end
|
3
src/bottle/version.cr
Normal file
3
src/bottle/version.cr
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module IRS
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
end
|
10
src/irs.cr
10
src/irs.cr
|
@ -1,6 +1,8 @@
|
||||||
# TODO: Write documentation for `IRS`
|
require "./bottle/cli"
|
||||||
module IRS
|
|
||||||
VERSION = "0.1.0"
|
|
||||||
|
|
||||||
# TODO: Put your code here
|
def main
|
||||||
|
cli = CLI.new(ARGV)
|
||||||
|
cli.act_on_args()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
main()
|
|
@ -27,7 +27,7 @@ class SpotifySearcher
|
||||||
payload = "grant_type=client_credentials"
|
payload = "grant_type=client_credentials"
|
||||||
|
|
||||||
response = HTTP::Client.post(auth_url, headers: headers, form: payload)
|
response = HTTP::Client.post(auth_url, headers: headers, form: payload)
|
||||||
__error_check(response)
|
error_check(response)
|
||||||
|
|
||||||
access_token = JSON.parse(response.body)["access_token"]
|
access_token = JSON.parse(response.body)["access_token"]
|
||||||
|
|
||||||
|
@ -57,16 +57,16 @@ 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, 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)
|
||||||
|
|
||||||
items = JSON.parse(response.body)[item_type + "s"]["items"].as_a
|
items = JSON.parse(response.body)[item_type + "s"]["items"].as_a
|
||||||
|
|
||||||
points = __rank_items(items, item_parameters)
|
points = rank_items(items, item_parameters)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
return items[points[0][1]]
|
return items[points[0][1]]
|
||||||
|
@ -84,7 +84,7 @@ class SpotifySearcher
|
||||||
url = @root_url.join("artists/#{id}").to_s()
|
url = @root_url.join("artists/#{id}").to_s()
|
||||||
|
|
||||||
response = HTTP::Client.get(url, headers: @access_header)
|
response = HTTP::Client.get(url, headers: @access_header)
|
||||||
__error_check(response)
|
error_check(response)
|
||||||
|
|
||||||
genre = JSON.parse(response.body)["genres"][0].to_s
|
genre = JSON.parse(response.body)["genres"][0].to_s
|
||||||
genre = genre.split(" ").map { |x| x.capitalize }.join(" ")
|
genre = genre.split(" ").map { |x| x.capitalize }.join(" ")
|
||||||
|
@ -93,7 +93,7 @@ class SpotifySearcher
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks for errors in HTTP requests and raises one if found
|
# Checks for errors in HTTP requests and raises one if found
|
||||||
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" +
|
||||||
|
@ -103,7 +103,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,
|
||||||
offset : Int32, limit : Int32) : String
|
offset : Int32, limit : Int32) : String
|
||||||
query = ""
|
query = ""
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ class SpotifySearcher
|
||||||
item_parameters.keys.each do |k|
|
item_parameters.keys.each do |k|
|
||||||
# This will map album, playlist, and track from the name key to the query
|
# This will map album, playlist, and track from the name key to the query
|
||||||
if k == "name"
|
if k == "name"
|
||||||
query += __param_encode(item_type, item_parameters[k])
|
query += param_encode(item_type, item_parameters[k])
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -122,7 +122,7 @@ class SpotifySearcher
|
||||||
|
|
||||||
# if it's none of the above, treat it normally
|
# if it's none of the above, treat it normally
|
||||||
else
|
else
|
||||||
query += __param_encode(k, item_parameters[k])
|
query += param_encode(k, item_parameters[k])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ class SpotifySearcher
|
||||||
# Ranks the given items based off of the info from parameters.
|
# Ranks the given items based off of the info from parameters.
|
||||||
# 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
|
||||||
|
@ -151,17 +151,17 @@ class SpotifySearcher
|
||||||
|
|
||||||
# The key to compare to for artist
|
# The key to compare to for artist
|
||||||
if k == "artist"
|
if k == "artist"
|
||||||
pts += __points_compare(item["artists"][0]["name"].to_s, val)
|
pts += points_compare(item["artists"][0]["name"].to_s, val)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The key to compare to for playlists
|
# The key to compare to for playlists
|
||||||
if k == "username"
|
if k == "username"
|
||||||
pts += __points_compare(item["owner"]["display_name"].to_s, val)
|
pts += points_compare(item["owner"]["display_name"].to_s, val)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The key regardless of whether item is track, album,or playlist
|
# The key regardless of whether item is track, album,or playlist
|
||||||
if k == "name"
|
if k == "name"
|
||||||
pts += __points_compare(item["name"].to_s, val)
|
pts += points_compare(item["name"].to_s, val)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ class SpotifySearcher
|
||||||
# If the strings are the exact same, return 3 pts.
|
# If the strings are the exact same, return 3 pts.
|
||||||
# If *item1* includes *item2*, return 1 pt.
|
# If *item1* includes *item2*, return 1 pt.
|
||||||
# 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
|
||||||
item1 = item1.downcase.gsub(/[^a-z0-9]/, "")
|
item1 = item1.downcase.gsub(/[^a-z0-9]/, "")
|
||||||
item2 = item2.downcase.gsub(/[^a-z0-9]/, "")
|
item2 = item2.downcase.gsub(/[^a-z0-9]/, "")
|
||||||
|
|
||||||
|
@ -196,10 +196,10 @@ class SpotifySearcher
|
||||||
# Returns a `String` encoded for the spotify api
|
# Returns a `String` encoded for the spotify api
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# __query_encode("album", "A Night At The Opera")
|
# query_encode("album", "A Night At The Opera")
|
||||||
# => "album:A+Night+At+The+Opera"
|
# => "album:A+Night+At+The+Opera"
|
||||||
# ```
|
# ```
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ module Youtube
|
||||||
|
|
||||||
response = HTTP::Client.get(url)
|
response = HTTP::Client.get(url)
|
||||||
|
|
||||||
valid_nodes = __get_video_link_nodes(response.body)
|
valid_nodes = get_video_link_nodes(response.body)
|
||||||
|
|
||||||
if valid_nodes.size == 0
|
if valid_nodes.size == 0
|
||||||
puts "There were no results for that query."
|
puts "There were no results for that query."
|
||||||
|
@ -49,7 +49,7 @@ module Youtube
|
||||||
|
|
||||||
return root + valid_nodes[0]["href"] if download_first
|
return root + valid_nodes[0]["href"] if download_first
|
||||||
|
|
||||||
ranked = __rank_videos(song_name, artist_name, query, valid_nodes)
|
ranked = rank_videos(song_name, artist_name, query, valid_nodes)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
return root + valid_nodes[ranked[0]["index"]]["href"]
|
return root + valid_nodes[ranked[0]["index"]]["href"]
|
||||||
|
@ -64,7 +64,7 @@ module Youtube
|
||||||
# {"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
|
||||||
|
@ -72,9 +72,9 @@ module Youtube
|
||||||
nodes.each do |node|
|
nodes.each do |node|
|
||||||
pts = 0
|
pts = 0
|
||||||
|
|
||||||
pts += __points_compare(song_name, node["title"])
|
pts += points_compare(song_name, node["title"])
|
||||||
pts += __points_compare(artist_name, node["title"])
|
pts += points_compare(artist_name, node["title"])
|
||||||
pts += __count_buzzphrases(query, node["title"])
|
pts += count_buzzphrases(query, node["title"])
|
||||||
|
|
||||||
points.push({
|
points.push({
|
||||||
"points" => pts,
|
"points" => pts,
|
||||||
|
@ -103,7 +103,7 @@ module Youtube
|
||||||
# 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
|
||||||
if item2.includes?(item1)
|
if item2.includes?(item1)
|
||||||
return 3
|
return 3
|
||||||
end
|
end
|
||||||
|
@ -123,7 +123,7 @@ module Youtube
|
||||||
# *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
|
||||||
bad_phrases = 0
|
bad_phrases = 0
|
||||||
|
|
||||||
|
@ -152,12 +152,12 @@ module Youtube
|
||||||
|
|
||||||
# Finds valid video links from a `HTTP::Client.get` request
|
# Finds valid video links from a `HTTP::Client.get` request
|
||||||
# Returns an `Array` of `XML::Node`
|
# Returns an `Array` of `XML::Node`
|
||||||
private def __get_video_link_nodes(doc : String) : Array(XML::Node)
|
private def get_video_link_nodes(doc : String) : Array(XML::Node)
|
||||||
nodes = XML.parse(doc).xpath_nodes("//a")
|
nodes = XML.parse(doc).xpath_nodes("//a")
|
||||||
valid_nodes = [] of XML::Node
|
valid_nodes = [] of XML::Node
|
||||||
|
|
||||||
nodes.each do |node|
|
nodes.each do |node|
|
||||||
if __video_link_node?(node)
|
if video_link_node?(node)
|
||||||
valid_nodes.push(node)
|
valid_nodes.push(node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -167,7 +167,7 @@ module Youtube
|
||||||
|
|
||||||
# Tests if the provided `XML::Node` has a valid link to a video
|
# Tests if the provided `XML::Node` has a valid link to a video
|
||||||
# Returns a `Bool`
|
# Returns a `Bool`
|
||||||
private def __video_link_node?(node : XML::Node) : Bool
|
private def video_link_node?(node : XML::Node) : Bool
|
||||||
# If this passes, then the node links to a playlist, not a video
|
# If this passes, then the node links to a playlist, not a video
|
||||||
if node["href"]?
|
if node["href"]?
|
||||||
return false if node["href"].includes?("&list=")
|
return false if node["href"].includes?("&list=")
|
||||||
|
|
Loading…
Reference in a new issue