Cleaned up list inheritance, songs are automatically organized

Moved more permanent variables to the config. I need to start
thinking about custom configs rather than a hard-coded one.
This commit is contained in:
Cooper Hammond 2020-03-10 14:12:12 -06:00
parent a24703d7bf
commit 5881d21f48
8 changed files with 150 additions and 32 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
/lib/ /lib/
/bin/ /bin/
/.shards/ /.shards/
/Music/
*.dwarf *.dwarf
*.mp3 *.mp3

View file

@ -5,6 +5,7 @@ require "./styles"
require "./version" require "./version"
require "../glue/song" require "../glue/song"
require "../glue/album"
class CLI class CLI
@ -15,8 +16,9 @@ class CLI
[["-h", "--help"], "help", "bool"], [["-h", "--help"], "help", "bool"],
[["-v", "--version"], "version", "bool"], [["-v", "--version"], "version", "bool"],
[["-i", "--install"], "install", "bool"], [["-i", "--install"], "install", "bool"],
[["-a", "--artist"], "artist", "string"],
[["-s", "--song"], "song", "string"], [["-s", "--song"], "song", "string"],
[["-a", "--artist"], "artist", "string"] [["-A", "--album"], "album", "string"]
] ]
@ -38,8 +40,9 @@ class CLI
#{Style.blue "-h, --help"} Show this help message and exit #{Style.blue "-h, --help"} Show this help message and exit
#{Style.blue "-v, --version"} Show the program version and exit #{Style.blue "-v, --version"} Show the program version and exit
#{Style.blue "-i, --install"} Download ffmpeg and youtube_dl binaries to #{Style.green Config.binary_location} #{Style.blue "-i, --install"} Download ffmpeg and youtube_dl binaries to #{Style.green Config.binary_location}
#{Style.blue "-s, --song <song>"} Specify song name for downloading
#{Style.blue "-a, --artist <artist>"} Specify artist name for downloading #{Style.blue "-a, --artist <artist>"} Specify artist name for downloading
#{Style.blue "-s, --song <song>"} Specify song name to download
#{Style.blue "-A, --album <album"} Specify the album name to download
#{Style.bold "Examples:"} #{Style.bold "Examples:"}
$ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")} $ #{Style.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
@ -66,9 +69,14 @@ class CLI
exit exit
elsif @args["song"]? && @args["artist"]? elsif @args["song"]? && @args["artist"]?
s = Song.new(@args["song"], @args["artist"]) s = Song.new(@args["song"], @args["artist"])
s.provide_client_keys("e4198f6a3f7b48029366f22528b5dc66", "ba057d0621a5496bbb64edccf758bde5") s.provide_client_keys(Config.client_key, Config.client_secret)
s.grab_it() s.grab_it()
s.organize_it(Config.music_directory)
exit exit
elsif @args["album"]? && @args["artist"]?
a = Album.new(@args["album"], @args["artist"])
a.provide_client_keys(Config.client_key, Config.client_secret)
a.grab_it()
end end
end end
@ -124,9 +132,15 @@ class CLI
end end
private def arg_error(argv : Array(String), arg : Int32, msg : String) : Nil private def arg_error(argv : Array(String), arg : Int32, msg : String) : Nil
precursor = "irs " precursor = "irs"
start = argv[..arg - 1] precursor += " " if arg != 0
if arg == 0
start = [] of String
else
start = argv[..arg - 1]
end
last = argv[arg + 1..] last = argv[arg + 1..]
distance = (precursor + start.join(" ")).size distance = (precursor + start.join(" ")).size

View file

@ -6,4 +6,17 @@ module Config
path = "~/.irs/bin" path = "~/.irs/bin"
return Path[path].expand(home: true).to_s return Path[path].expand(home: true).to_s
end end
def music_directory : String
path = "./Music/"
return Path[path].expand(home: true).to_s
end
def client_key : String
return "e4198f6a3f7b48029366f22528b5dc66"
end
def client_secret : String
return "ba057d0621a5496bbb64edccf758bde5"
end
end end

View file

@ -1,9 +1,15 @@
require "../bottle/config"
require "./song" require "./song"
require "./list" require "./list"
class Album < SpotifyList class Album < SpotifyList
@music_directory = Config.music_directory
# Uses the `spotify_searcher` defined in parent `SpotifyList` to find the
# correct metadata of the list
def find_it def find_it
album = @spotify_searcher.find_item("album", { album = @spotify_searcher.find_item("album", {
"name" => @list_name.as(String), "name" => @list_name.as(String),
@ -17,7 +23,43 @@ class Album < SpotifyList
end end
end end
private def set_organization(index : Int32, song : Song) # Will define specific metadata that may not be included in the raw return
# pass # of spotify's album json. Moves the title of the album and the album art
# to the json of the single song
def organize_song_metadata(list : JSON::Any, datum : JSON::Any) : JSON::Any
json_string = %(
{
"album": {
"name": "#{list["name"]}",
"images": [{"url": "#{list["images"][0]["url"]}"}]
},
)
datum.as_h.keys.each_with_index do |key, index|
value = datum[key]
if value.as_s?
json_string += %("#{key}": "#{datum[key]}")
else
json_string += %("#{key}": #{datum[key].to_s.gsub(" => ", ": ")})
end
if index != datum.as_h.keys.size - 1
json_string += ",\n"
end
end
json_string += %(
}
)
json_string = json_string.gsub(" ", "")
json_string = json_string.gsub("\n", " ")
json_string = json_string.gsub("\t", "")
data = JSON.parse(json_string)
return data
end
private def organize(song : Song)
song.organize_it(@music_directory)
end end
end end

View file

@ -1,3 +1,5 @@
require "json"
require "../search/spotify" require "../search/spotify"
require "../search/youtube" require "../search/youtube"
@ -13,33 +15,54 @@ abstract class SpotifyList
@file_names = [] of String @file_names = [] of String
def initialize(@list_name : String, @list_author : String?) def initialize(@list_name : String, @list_author : String?)
@spotify_searcher.authorize(
"e4198f6a3f7b48029366f22528b5dc66",
"ba057d0621a5496bbb64edccf758bde5")
end end
# Finds the list, and downloads all of the songs using the `Song` class # Finds the list, and downloads all of the songs using the `Song` class
def grab_it def grab_it
if !@spotify_searcher.authorized?
raise("Need to call provide_client_keys on Album or Playlist class.")
end
list = find_it() list = find_it()
contents = list["tracks"][0]["items"] contents = list["tracks"]["items"].as_a
i = 0 i = 0
contents.each do |data| contents.each do |datum|
if song["track"]? if datum["track"]?
data = data["track"] datum = datum["track"]
end end
data = organize_song_metadata(list, datum)
song = Song.new(data["name"].to_s, data["artists"][0]["name"].to_s) song = Song.new(data["name"].to_s, data["artists"][0]["name"].to_s)
song.provide_spotify(@spotify_searcher) song.provide_spotify(@spotify_searcher)
set_organization(i , song) song.provide_metadata(data)
song.grab_it() song.grab_it()
organize(song)
i += 1 i += 1
end end
end end
# Will authorize the class associated `SpotifySearcher`
def provide_client_keys(client_key : String, client_secret : String)
@spotify_searcher.authorize(client_key, client_secret)
end
# Defined in subclasses, will return the appropriate information or call an
# error if the info is not found and exit
abstract def find_it : JSON::Any abstract def find_it : JSON::Any
private abstract def set_organization(song_index : Int32, song : Song) # If there's a need to organize the individual song data so that the `Song`
# class can better handle it, this function will be defined in the subclass
private abstract def organize_song_metadata(list : JSON::Any,
datum : JSON::Any) : JSON::Any
# Will define the specific type of organization for a list of songs.
# Needed because most people want albums sorted by artist, but playlists all
# in one folder
private abstract def organize(song : Song)
end end

View file

@ -11,18 +11,19 @@ class Song
@client_secret = "" @client_secret = ""
@metadata : JSON::Any? @metadata : JSON::Any?
@filename : String? @filename = ""
@artist = ""
@album = ""
def initialize(@song_name : String, @artist_name : String) def initialize(@song_name : String, @artist_name : String)
end end
# Find, downloads, and tags the mp3 song that this class represents. # 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() # Song.new("Bohemian Rhapsody", "Queen").grab_it()
# ``` # ```
def grab_it : Nil def grab_it
if !@spotify_searcher.authorized? && !@metadata if !@spotify_searcher.authorized? && !@metadata
if @client_id != "" && @client_secret != "" if @client_id != "" && @client_secret != ""
@spotify_searcher.authorize(@client_id, @client_secret) @spotify_searcher.authorize(@client_id, @client_secret)
@ -47,9 +48,10 @@ class Song
end end
data = @metadata.as(JSON::Any) data = @metadata.as(JSON::Any)
filename = data["track_number"].to_s + " - #{data["name"].to_s}.mp3" @filename = data["track_number"].to_s + " - #{data["name"].to_s}.mp3"
puts "Searching for url ..." puts "Searching for url ..."
# TODO: should this search_term be here?
url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics") url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics")
if !url if !url
@ -59,18 +61,21 @@ class Song
end end
puts "Downloading video:" puts "Downloading video:"
Ripper.download_mp3(url.as(String), filename) Ripper.download_mp3(url.as(String), @filename)
temp_albumart_filename = ".tempalbumart.jpg" temp_albumart_filename = ".tempalbumart.jpg"
HTTP::Client.get(data["album"]["images"][0]["url"].to_s) do |response| HTTP::Client.get(data["album"]["images"][0]["url"].to_s) do |response|
File.write(temp_albumart_filename, response.body_io) File.write(temp_albumart_filename, response.body_io)
end end
tagger = Tags.new(filename) @artist = data["artists"][0]["name"].to_s
@album = data["album"]["name"].to_s
tagger = Tags.new(@filename)
tagger.add_album_art(temp_albumart_filename) tagger.add_album_art(temp_albumart_filename)
tagger.add_text_tag("title", data["name"].to_s) tagger.add_text_tag("title", data["name"].to_s)
tagger.add_text_tag("artist", data["artists"][0]["name"].to_s) tagger.add_text_tag("artist", @artist)
tagger.add_text_tag("album", data["album"]["name"].to_s) tagger.add_text_tag("album", @album)
tagger.add_text_tag("genre", tagger.add_text_tag("genre",
@spotify_searcher.find_genre(data["artists"][0]["id"].to_s)) @spotify_searcher.find_genre(data["artists"][0]["id"].to_s))
tagger.add_text_tag("track", data["track_number"].to_s) tagger.add_text_tag("track", data["track_number"].to_s)
@ -84,6 +89,28 @@ class Song
end end
# Will organize the song into the user's provided music directory as
# music_directory > artist_name > album_name > song
# Must be called AFTER the song has been downloaded.
#
# ```
# s = Song.new("Bohemian Rhapsody", "Queen").grab_it()
# s.organize_it("/home/cooper/Music")
# # Will move the mp3 file to
# # /home/cooper/Music/Queen/A Night At The Opera/1 - Bohemian Rhapsody.mp3
# ```
def organize_it(music_directory : String)
path = Path[music_directory].expand(home: true)
path = path / @artist_name.gsub(/[\/]/, "").gsub(" ", " ")
path = path / @album.gsub(/[\/]/, "").gsub(" ", " ")
strpath = path.to_s
if !File.directory?(strpath)
FileUtils.mkdir_p(strpath)
end
safe_filename = @filename.gsub(/[\/]/, "").gsub(" ", " ")
File.rename("./" + @filename, (path / safe_filename).to_s)
end
# Provide metadata so that it doesn't have to find it. Useful for overwriting # 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 # metadata. Must be called if provide_client_keys and provide_spotify are not
# called. # called.
@ -105,7 +132,7 @@ class Song
# .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it() # .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it()
# ``` # ```
def provide_spotify(spotify : SpotifySearcher) : self def provide_spotify(spotify : SpotifySearcher) : self
@spotify = spotify @spotify_searcher = spotify
return self return self
end end
@ -120,8 +147,4 @@ class Song
@client_secret = client_secret @client_secret = client_secret
return self return self
end end
end end
# s = Song.new("Bohemian Rhapsody", "Queen")
# s.provide_client_keys("e4198f6a3f7b48029366f22528b5dc66", "ba057d0621a5496bbb64edccf758bde5")
# s.grab_it()

View file

@ -44,6 +44,8 @@ module Ripper
end end
end end
# An internal class that will keep track of what to output to the user or
# what should be hidden.
private class RipperOutputCensor private class RipperOutputCensor
@dl_status_index = 0 @dl_status_index = 0

View file

@ -218,8 +218,8 @@ end
# puts SpotifySearcher.new() # puts SpotifySearcher.new()
# .authorize("e4198f6a3f7b48029366f22528b5dc66", # .authorize("XXXXXXXXXXXXXXX",
# "ba057d0621a5496bbb64edccf758bde5") # "XXXXXXXXXXXXXXX")
# .find_item("playlist", { # .find_item("playlist", {
# "name" => "Brain Food", # "name" => "Brain Food",
# "username" => "spotify" # "username" => "spotify"