From 6485324acc3fe1f85b893455891fa5fba13a6aed Mon Sep 17 00:00:00 2001 From: Cooper Hammond Date: Wed, 19 Jun 2019 19:06:09 -0700 Subject: [PATCH] song glue done! --- .gitignore | 2 + src/glue/song.cr | 122 +++++++++++++++++++++++++++++++++++++++++ src/interact/ripper.cr | 8 ++- src/interact/tagger.cr | 2 +- src/search/spotify.cr | 57 +++++++++++++------ src/search/youtube.cr | 18 ++++-- 6 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 src/glue/song.cr diff --git a/.gitignore b/.gitignore index 0bb75ea..b48c7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /bin/ /.shards/ *.dwarf + +*.mp3 \ No newline at end of file diff --git a/src/glue/song.cr b/src/glue/song.cr new file mode 100644 index 0000000..5497aea --- /dev/null +++ b/src/glue/song.cr @@ -0,0 +1,122 @@ +require "../search/spotify" +require "../search/youtube" + +require "../interact/ripper" +require "../interact/tagger" + + +class Song + @spotify_searcher = SpotifySearcher.new() + @client_id = "" + @client_secret = "" + + @metadata : JSON::Any? + @filename : String? + + def initialize(@song_name : String, @artist_name : String) + end + + # Find, downloads, and tags the mp3 song that this class represents. + # Will return true on complete success and false on failure. + # + # ``` + # Song.new("Bohemian Rhapsody", "Queen").grab_it() + # ``` + def grab_it : Nil + if !@spotify_searcher.authorized? && !@metadata + if @client_id != "" && @client_secret != "" + @spotify_searcher.authorize(@client_id, @client_secret) + else + raise("Need to call either `provide_metadata`, `provide_spotify`, " + + "or `provide_client_keys` so that Spotify can be interfaced with.") + end + end + + if !@metadata + @metadata = @spotify_searcher.find_item("track", { + "name" => @song_name, + "artist" => @artist_name + }) + + if !@metadata + raise("There was no metadata found on Spotify for\n" + + %("#{@song_name}" by "#{@artist_name}\n) + + "Check your input and try again.") + end + end + + data = @metadata.as(JSON::Any) + filename = data["track_number"].to_s + " - #{data["name"].to_s}.mp3" + + url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics") + + if !url + raise("There was no link found on youtube for\n" + + %("#{@song_name}" by "#{@artist_name}\n) + + "Check your input and try again.") + end + + + Ripper.download_mp3(url.as(String), filename) + + temp_albumart_filename = ".tempalbumart.jpg" + HTTP::Client.get(data["album"]["images"][0]["url"].to_s) do |response| + File.write(temp_albumart_filename, response.body_io) + end + + tagger = Tags.new(filename) + tagger.add_album_art(temp_albumart_filename) + tagger.add_text_tag("title", data["name"].to_s) + tagger.add_text_tag("artist", data["artists"][0]["name"].to_s) + tagger.add_text_tag("album", data["album"]["name"].to_s) + tagger.add_text_tag("genre", + @spotify_searcher.find_genre(data["artists"][0]["id"].to_s)) + tagger.add_text_tag("track", data["track_number"].to_s) + tagger.add_text_tag("disc", data["disc_number"].to_s) + + tagger.save() + File.delete(temp_albumart_filename) + + end + + # Provide metadata so that it doesn't have to find it. Useful for overwriting + # metadata. Must be called if provide_client_keys and provide_spotify are not + # called. + # + # ``` + # Song.new(...).provide_metadata(...).grab_it() + # ``` + def provide_metadata(metadata : JSON::Any) : self + @metadata = metadata + return self + end + + # Provide an already authenticated `SpotifySearcher` class. Useful to avoid + # authenticating over and over again. Must be called if provide_metadata and + # provide_client_keys are not called. + # + # ``` + # Song.new(...).provide_spotify(SpotifySearcher.new() + # .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it() + # ``` + def provide_spotify(spotify : SpotifySearcher) : self + @spotify = spotify + return self + end + + # Provide spotify client keys. Must be called if provide_metadata and + # provide_spotify are not called. + # + # ``` + # Song.new(...).provide_client_keys("XXXXXXXXXX", "XXXXXXXXX").grab_it() + # ``` + def provide_client_keys(client_id : String, client_secret : String) : self + @client_id = client_id + @client_secret = client_secret + return self + end +end + +# s = Song.new("Bohemian Rhapsody", "Queen") +# s.provide_client_keys("e4198f6a3f7b48029366f22528b5dc66", "ba057d0621a5496bbb64edccf758bde5") +# s.grab_it() \ No newline at end of file diff --git a/src/interact/ripper.cr b/src/interact/ripper.cr index 376199e..779480f 100644 --- a/src/interact/ripper.cr +++ b/src/interact/ripper.cr @@ -11,7 +11,7 @@ module Ripper # Ripper.download_mp3("https://youtube.com/watch?v=0xnciFWAqa0", # "Queen/A Night At The Opera/Bohemian Rhapsody.mp3") # ``` - def download_mp3(video_url : String, output_filename : String) : Nil + def download_mp3(video_url : String, output_filename : String) : Bool ydl_loc = BIN_LOC.join("youtube-dl") # remove the extension that will be added on by ydl @@ -33,7 +33,11 @@ module Ripper command += " #{option} #{options[option]}" end - system(command) + if system(command) + return true + else + return false + end end end \ No newline at end of file diff --git a/src/interact/tagger.cr b/src/interact/tagger.cr index fb1d58c..50208d1 100644 --- a/src/interact/tagger.cr +++ b/src/interact/tagger.cr @@ -36,7 +36,7 @@ class Tags def save : Nil @query_args.push(%("_#{@filename}")) command = @BIN_LOC.to_s + "/ffmpeg " + @query_args.join(" ") - system command + system(command) File.delete(@filename) File.rename("_" + @filename, @filename) diff --git a/src/search/spotify.cr b/src/search/spotify.cr index 3fdfad5..ab70007 100644 --- a/src/search/spotify.cr +++ b/src/search/spotify.cr @@ -27,21 +27,24 @@ class SpotifySearcher payload = "grant_type=client_credentials" response = HTTP::Client.post(auth_url, headers: headers, form: payload) + __error_check(response) - if response.status_code == 200 - access_token = JSON.parse(response.body)["access_token"] - - @access_header = HTTP::Headers{ - "Authorization" => "Bearer #{access_token}" - } + access_token = JSON.parse(response.body)["access_token"] + + @access_header = HTTP::Headers{ + "Authorization" => "Bearer #{access_token}" + } - @authorized = true - - end + @authorized = true return self end + # Check if the class is authorized or not + def authorized? : Bool + return @authorized + end + # Searches spotify with the specified parameters for the specified items # # ``` @@ -59,13 +62,7 @@ class SpotifySearcher url = @root_url.join("search?q=#{query}").to_s() response = HTTP::Client.get(url, headers: @access_header) - - if response.status_code != 200 - puts "There was an error with your request." - puts "Status code: #{response.status_code}" - puts "Reponse: \n#{response.body}" - return nil - end + __error_check(response) items = JSON.parse(response.body)[item_type + "s"]["items"].as_a @@ -73,10 +70,36 @@ class SpotifySearcher begin return items[points[0][1]] - rescue IndexException + rescue IndexError return nil end end + + # Find the genre of an artist based off of their id + # + # ``` + # SpotifySearcher.new().authorize(...).find_genre("1dfeR4HaWDbWqFHLkxsg1d") + # ``` + def find_genre(id : String) : String + url = @root_url.join("artists/#{id}").to_s() + + response = HTTP::Client.get(url, headers: @access_header) + __error_check(response) + + genre = JSON.parse(response.body)["genres"][0].to_s + genre = genre.split(" ").map { |x| x.capitalize }.join(" ") + + return genre + end + + # Checks for errors in HTTP requests and raises one if found + 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" + + "Response: \n#{response.body}") + end + end # Generates url to run a GET request against to the Spotify open API # Returns a `String.` diff --git a/src/search/youtube.cr b/src/search/youtube.cr index 4c82655..442d9b7 100644 --- a/src/search/youtube.cr +++ b/src/search/youtube.cr @@ -31,7 +31,7 @@ module Youtube # => "https://www.youtube.com/watch?v=dQw4w9WgXcQ" # ``` def find_url(song_name : String, artist_name : String, search_terms = "", - download_first = false) : Nil + download_first = false) : String? query = (song_name + " " + artist_name + " " + search_terms).strip.gsub(" ", "+") url = "https://www.youtube.com/results?search_query=" + query @@ -51,14 +51,21 @@ module Youtube ranked = __rank_videos(song_name, artist_name, query, valid_nodes) - return root + valid_nodes[ranked[0]["index"]]["href"] + begin + return root + valid_nodes[ranked[0]["index"]]["href"] + rescue IndexError + return nil + end end # Will rank videos according to their title and the user input - # Returns an `Array` of Arrays each layed out like - # [, ]. + # Return: + # [ + # {"points" => x, "index" => x}, + # ... + # ] private def __rank_videos(song_name : String, artist_name : String, - query : String, nodes : Array(XML::Node)) : Array(Array(Int32)) + query : String, nodes : Array(XML::Node)) : Array(Hash(String, Int32)) points = [] of Hash(String, Int32) index = 0 @@ -171,5 +178,6 @@ module Youtube return true if node["class"].includes?(valid_class) end end + return false end end \ No newline at end of file