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/
/bin/
/.shards/
/Music/
*.dwarf
*.mp3

View file

@ -5,6 +5,7 @@ require "./styles"
require "./version"
require "../glue/song"
require "../glue/album"
class CLI
@ -15,8 +16,9 @@ class CLI
[["-h", "--help"], "help", "bool"],
[["-v", "--version"], "version", "bool"],
[["-i", "--install"], "install", "bool"],
[["-a", "--artist"], "artist", "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 "-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 "-s, --song <song>"} Specify song 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.green %(irs --song "Bohemian Rhapsody" --artist "Queen")}
@ -66,9 +69,14 @@ class CLI
exit
elsif @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.organize_it(Config.music_directory)
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
@ -124,9 +132,15 @@ class CLI
end
private def arg_error(argv : Array(String), arg : Int32, msg : String) : Nil
precursor = "irs "
precursor = "irs"
precursor += " " if arg != 0
if arg == 0
start = [] of String
else
start = argv[..arg - 1]
end
last = argv[arg + 1..]
distance = (precursor + start.join(" ")).size

View file

@ -6,4 +6,17 @@ module Config
path = "~/.irs/bin"
return Path[path].expand(home: true).to_s
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

View file

@ -1,9 +1,15 @@
require "../bottle/config"
require "./song"
require "./list"
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
album = @spotify_searcher.find_item("album", {
"name" => @list_name.as(String),
@ -17,7 +23,43 @@ class Album < SpotifyList
end
end
private def set_organization(index : Int32, song : Song)
# pass
# Will define specific metadata that may not be included in the raw return
# 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

View file

@ -1,3 +1,5 @@
require "json"
require "../search/spotify"
require "../search/youtube"
@ -13,33 +15,54 @@ abstract class SpotifyList
@file_names = [] of String
def initialize(@list_name : String, @list_author : String?)
@spotify_searcher.authorize(
"e4198f6a3f7b48029366f22528b5dc66",
"ba057d0621a5496bbb64edccf758bde5")
end
# Finds the list, and downloads all of the songs using the `Song` class
def grab_it
if !@spotify_searcher.authorized?
raise("Need to call provide_client_keys on Album or Playlist class.")
end
list = find_it()
contents = list["tracks"][0]["items"]
contents = list["tracks"]["items"].as_a
i = 0
contents.each do |data|
if song["track"]?
data = data["track"]
contents.each do |datum|
if datum["track"]?
datum = datum["track"]
end
data = organize_song_metadata(list, datum)
song = Song.new(data["name"].to_s, data["artists"][0]["name"].to_s)
song.provide_spotify(@spotify_searcher)
set_organization(i , song)
song.provide_metadata(data)
song.grab_it()
organize(song)
i += 1
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
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

View file

@ -11,18 +11,19 @@ class Song
@client_secret = ""
@metadata : JSON::Any?
@filename : String?
@filename = ""
@artist = ""
@album = ""
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
def grab_it
if !@spotify_searcher.authorized? && !@metadata
if @client_id != "" && @client_secret != ""
@spotify_searcher.authorize(@client_id, @client_secret)
@ -47,9 +48,10 @@ class Song
end
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 ..."
# TODO: should this search_term be here?
url = Youtube.find_url(@song_name, @artist_name, search_terms: "lyrics")
if !url
@ -59,18 +61,21 @@ class Song
end
puts "Downloading video:"
Ripper.download_mp3(url.as(String), filename)
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)
@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_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("artist", @artist)
tagger.add_text_tag("album", @album)
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)
@ -84,6 +89,28 @@ class Song
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
# metadata. Must be called if provide_client_keys and provide_spotify are not
# called.
@ -105,7 +132,7 @@ class Song
# .authenticate("XXXXXXXXXX", "XXXXXXXXXXX")).grab_it()
# ```
def provide_spotify(spotify : SpotifySearcher) : self
@spotify = spotify
@spotify_searcher = spotify
return self
end
@ -121,7 +148,3 @@ class Song
return self
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
# An internal class that will keep track of what to output to the user or
# what should be hidden.
private class RipperOutputCensor
@dl_status_index = 0

View file

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