Initial CLI done!

This commit is contained in:
Cooper Hammond 2019-06-20 09:43:31 -07:00
parent acd2abb1d0
commit 76a624d32f
7 changed files with 197 additions and 31 deletions

138
src/bottle/cli.cr Normal file
View 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
View file

23
src/bottle/styles.cr Normal file
View 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
View file

@ -0,0 +1,3 @@
module IRS
VERSION = "0.1.0"
end

View file

@ -1,6 +1,8 @@
# TODO: Write documentation for `IRS`
module IRS
VERSION = "0.1.0"
require "./bottle/cli"
# TODO: Put your code here
def main
cli = CLI.new(ARGV)
cli.act_on_args()
end
main()

View file

@ -27,7 +27,7 @@ class SpotifySearcher
payload = "grant_type=client_credentials"
response = HTTP::Client.post(auth_url, headers: headers, form: payload)
__error_check(response)
error_check(response)
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,
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()
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
points = __rank_items(items, item_parameters)
points = rank_items(items, item_parameters)
begin
return items[points[0][1]]
@ -84,7 +84,7 @@ class SpotifySearcher
url = @root_url.join("artists/#{id}").to_s()
response = HTTP::Client.get(url, headers: @access_header)
__error_check(response)
error_check(response)
genre = JSON.parse(response.body)["genres"][0].to_s
genre = genre.split(" ").map { |x| x.capitalize }.join(" ")
@ -93,7 +93,7 @@ class SpotifySearcher
end
# 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
raise("There was an error with your request.\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
# 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
query = ""
@ -114,7 +114,7 @@ class SpotifySearcher
item_parameters.keys.each do |k|
# This will map album, playlist, and track from the name key to the query
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
elsif !query_exclude.includes?(k)
@ -122,7 +122,7 @@ class SpotifySearcher
# if it's none of the above, treat it normally
else
query += __param_encode(k, item_parameters[k])
query += param_encode(k, item_parameters[k])
end
end
@ -135,7 +135,7 @@ class SpotifySearcher
# Ranks the given items based off of the info from parameters.
# Meant to find the item that the user desires.
# 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))
points = [] of Array(Int32)
index = 0
@ -151,17 +151,17 @@ class SpotifySearcher
# The key to compare to for 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
# The key to compare to for playlists
if k == "username"
pts += __points_compare(item["owner"]["display_name"].to_s, val)
pts += points_compare(item["owner"]["display_name"].to_s, val)
end
# The key regardless of whether item is track, album,or playlist
if k == "name"
pts += __points_compare(item["name"].to_s, val)
pts += points_compare(item["name"].to_s, val)
end
end
@ -180,7 +180,7 @@ class SpotifySearcher
# If the strings are the exact same, return 3 pts.
# If *item1* includes *item2*, return 1 pt.
# 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]/, "")
item2 = item2.downcase.gsub(/[^a-z0-9]/, "")
@ -196,10 +196,10 @@ class SpotifySearcher
# 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"
# ```
private def __param_encode(key : String, value : String) : String
private def param_encode(key : String, value : String) : String
return key.gsub(" ", "+") + ":" + value.gsub(" ", "+") + "+"
end

View file

@ -38,7 +38,7 @@ module Youtube
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
puts "There were no results for that query."
@ -49,7 +49,7 @@ module Youtube
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
return root + valid_nodes[ranked[0]["index"]]["href"]
@ -64,7 +64,7 @@ module Youtube
# {"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))
points = [] of Hash(String, Int32)
index = 0
@ -72,9 +72,9 @@ module Youtube
nodes.each do |node|
pts = 0
pts += __points_compare(song_name, node["title"])
pts += __points_compare(artist_name, node["title"])
pts += __count_buzzphrases(query, node["title"])
pts += points_compare(song_name, node["title"])
pts += points_compare(artist_name, node["title"])
pts += count_buzzphrases(query, node["title"])
points.push({
"points" => pts,
@ -103,7 +103,7 @@ module Youtube
# 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
private def points_compare(item1 : String, item2 : String) : Int32
if item2.includes?(item1)
return 3
end
@ -123,7 +123,7 @@ module Youtube
# *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
# 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
bad_phrases = 0
@ -152,12 +152,12 @@ module Youtube
# Finds valid video links from a `HTTP::Client.get` request
# 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")
valid_nodes = [] of XML::Node
nodes.each do |node|
if __video_link_node?(node)
if video_link_node?(node)
valid_nodes.push(node)
end
end
@ -167,7 +167,7 @@ module Youtube
# Tests if the provided `XML::Node` has a valid link to a video
# 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 node["href"]?
return false if node["href"].includes?("&list=")