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`
|
||||
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()
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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=")
|
||||
|
|
Loading…
Reference in a new issue