diff --git a/src/search/spotify.cr b/src/search/spotify.cr index cb5f847..4130f86 100644 --- a/src/search/spotify.cr +++ b/src/search/spotify.cr @@ -1,7 +1,7 @@ +require "http" +require "json" require "base64" -require "spotify" - class SpotifySearcher @root_url = Path["https://api.spotify.com/v1/"] @@ -74,7 +74,8 @@ class SpotifySearcher end - # Generates url to run a GET request against + # 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, offset : Int32, limit : Int32) query = "" @@ -106,6 +107,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, parameters : Hash) points = [] of Array(Int32) index = 0 @@ -163,7 +165,7 @@ class SpotifySearcher end end - # Returns a parameter encoded for the spotify api + # Returns a `String` encoded for the spotify api # # ``` # __query_encode("album", "A Night At The Opera") @@ -176,10 +178,10 @@ class SpotifySearcher end -puts SpotifySearcher.new() - .authorize("e4198f6a3f7b48029366f22528b5dc66", - "ba057d0621a5496bbb64edccf758bde5") - .find_item("track", { - "name" => "Bohemian Rhapsody", - "artist" => "Queen" - }) \ No newline at end of file +# puts SpotifySearcher.new() +# .authorize("e4198f6a3f7b48029366f22528b5dc66", +# "ba057d0621a5496bbb64edccf758bde5") +# .find_item("album", { +# "name" => "A Night At The Opera", +# "artist" => "Queen" +# }) \ No newline at end of file diff --git a/src/search/youtube.cr b/src/search/youtube.cr new file mode 100644 index 0000000..c6f98d3 --- /dev/null +++ b/src/search/youtube.cr @@ -0,0 +1,119 @@ +require "http" +require "xml" + + +module Youtube + + extend self + + VALID_LINK_CLASSES = [ + "yt-simple-endpoint style-scope ytd-video-renderer", + "yt-uix-tile-link yt-ui-ellipsis yt-ui-ellipsis-2 yt-uix-sessionlink spf-link " + ] + + # Finds a youtube url based off of the given information. + # The query to youtube is constructed like this: + # " " + # If *download_first* is provided, the first link found will be downloaded. + # + # ``` + # Youtube.find_url("Bohemian Rhapsody", "Queen") + # => "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + # ``` + def find_url(song_name : String, artist_name : String, search_terms = "", download_first = false) + query = (song_name + " " + artist_name + " " + search_terms).strip.gsub(" ", "+") + + url = "https://www.youtube.com/results?search_query=" + query + + response = HTTP::Client.get(url) + + valid_nodes = __get_video_link_nodes(response.body) + + if valid_nodes.size == 0 + puts "There were no results for that query." + return nil + end + + root = "https://youtube.com" + + return root + valid_nodes[0]["href"] if download_first + + ranked = __rank_videos(song_name, artist_name, valid_nodes) + + return root + valid_nodes[ranked[0][1]]["href"] + end + + # Will rank videos according to their title and the user input + # Returns an `Array` of Arrays each layed out like + # [, ]. + private def __rank_videos(song_name, artist_name, nodes : Array(XML::Node)) + points = [] of Array(Int32) + index = 0 + + nodes.each do |node| + pts = 0 + + pts += __points_compare(song_name, node["title"]) + pts += __points_compare(artist_name, node["title"]) + + points.push([pts, index]) + index += 1 + end + + points.sort!{ |a, b| b[0] <=> a[0] } + + return points + end + + # 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 + # nonalphanumeric characters are stripped. + # If *item1* includes *item2*, return 3 pts. + # 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) + if item1.includes?(item2) + return 3 + end + + item1 = item1.downcase.gsub(/[^a-z0-9]/, "") + item2 = item2.downcase.gsub(/[^a-z0-9]/, "") + + if item1.includes?(item2) + return 1 + else + return 0 + end + end + + # Finds valid video links from a `HTTP::Client.get` request + # Returns an `Array` of `XML::Node` + private def __get_video_link_nodes(doc : String) + nodes = XML.parse(doc).xpath_nodes("//a") + valid_nodes = [] of XML::Node + + nodes.each do |node| + if __video_link_node?(node) + valid_nodes.push(node) + end + end + + return valid_nodes + end + + # Tests if the provided `XML::Node` has a valid link to a video + # Returns a `Bool` + private def __video_link_node?(node : XML::Node) + # If this passes, then the node links to a playlist, not a video + if node["href"]? + return false if node["href"].includes?("&list=") + end + + VALID_LINK_CLASSES.each do |valid_class| + if node["class"]? + return true if node["class"].includes?(valid_class) + end + end + end +end \ No newline at end of file