mirror of
https://github.com/cooperhammond/irs.git
synced 2025-01-08 20:05:27 +00:00
song glue done!
This commit is contained in:
parent
9e230ac13f
commit
6485324acc
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,3 +3,5 @@
|
||||||
/bin/
|
/bin/
|
||||||
/.shards/
|
/.shards/
|
||||||
*.dwarf
|
*.dwarf
|
||||||
|
|
||||||
|
*.mp3
|
122
src/glue/song.cr
Normal file
122
src/glue/song.cr
Normal file
|
@ -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()
|
|
@ -11,7 +11,7 @@ module Ripper
|
||||||
# Ripper.download_mp3("https://youtube.com/watch?v=0xnciFWAqa0",
|
# Ripper.download_mp3("https://youtube.com/watch?v=0xnciFWAqa0",
|
||||||
# "Queen/A Night At The Opera/Bohemian Rhapsody.mp3")
|
# "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")
|
ydl_loc = BIN_LOC.join("youtube-dl")
|
||||||
|
|
||||||
# remove the extension that will be added on by ydl
|
# remove the extension that will be added on by ydl
|
||||||
|
@ -33,7 +33,11 @@ module Ripper
|
||||||
command += " #{option} #{options[option]}"
|
command += " #{option} #{options[option]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
system(command)
|
if system(command)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
|
@ -36,7 +36,7 @@ class Tags
|
||||||
def save : Nil
|
def save : Nil
|
||||||
@query_args.push(%("_#{@filename}"))
|
@query_args.push(%("_#{@filename}"))
|
||||||
command = @BIN_LOC.to_s + "/ffmpeg " + @query_args.join(" ")
|
command = @BIN_LOC.to_s + "/ffmpeg " + @query_args.join(" ")
|
||||||
system command
|
system(command)
|
||||||
|
|
||||||
File.delete(@filename)
|
File.delete(@filename)
|
||||||
File.rename("_" + @filename, @filename)
|
File.rename("_" + @filename, @filename)
|
||||||
|
|
|
@ -27,21 +27,24 @@ 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)
|
||||||
|
|
||||||
if response.status_code == 200
|
access_token = JSON.parse(response.body)["access_token"]
|
||||||
access_token = JSON.parse(response.body)["access_token"]
|
|
||||||
|
@access_header = HTTP::Headers{
|
||||||
@access_header = HTTP::Headers{
|
"Authorization" => "Bearer #{access_token}"
|
||||||
"Authorization" => "Bearer #{access_token}"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@authorized = true
|
@authorized = true
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
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
|
# 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()
|
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)
|
||||||
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
|
|
||||||
|
|
||||||
items = JSON.parse(response.body)[item_type + "s"]["items"].as_a
|
items = JSON.parse(response.body)[item_type + "s"]["items"].as_a
|
||||||
|
|
||||||
|
@ -73,10 +70,36 @@ class SpotifySearcher
|
||||||
|
|
||||||
begin
|
begin
|
||||||
return items[points[0][1]]
|
return items[points[0][1]]
|
||||||
rescue IndexException
|
rescue IndexError
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
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
|
# Generates url to run a GET request against to the Spotify open API
|
||||||
# Returns a `String.`
|
# Returns a `String.`
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Youtube
|
||||||
# => "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
# => "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
# ```
|
# ```
|
||||||
def find_url(song_name : String, artist_name : String, search_terms = "",
|
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(" ", "+")
|
query = (song_name + " " + artist_name + " " + search_terms).strip.gsub(" ", "+")
|
||||||
|
|
||||||
url = "https://www.youtube.com/results?search_query=" + query
|
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)
|
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
|
end
|
||||||
|
|
||||||
# Will rank videos according to their title and the user input
|
# Will rank videos according to their title and the user input
|
||||||
# Returns an `Array` of Arrays each layed out like
|
# Return:
|
||||||
# [<points>, <original index>].
|
# [
|
||||||
|
# {"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(Array(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
|
||||||
|
|
||||||
|
@ -171,5 +178,6 @@ module Youtube
|
||||||
return true if node["class"].includes?(valid_class)
|
return true if node["class"].includes?(valid_class)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
Loading…
Reference in a new issue