From 5881d21f48739b909f9e4678133d6a0446ead84d Mon Sep 17 00:00:00 2001 From: Cooper Hammond Date: Tue, 10 Mar 2020 14:12:12 -0600 Subject: [PATCH] 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. --- .gitignore | 1 + src/bottle/cli.cr | 24 +++++++++++++++----- src/bottle/config.cr | 13 +++++++++++ src/glue/album.cr | 46 +++++++++++++++++++++++++++++++++++-- src/glue/list.cr | 41 +++++++++++++++++++++++++-------- src/glue/song.cr | 51 ++++++++++++++++++++++++++++++------------ src/interact/ripper.cr | 2 ++ src/search/spotify.cr | 4 ++-- 8 files changed, 150 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 2da7df7..fa277ac 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /lib/ /bin/ /.shards/ +/Music/ *.dwarf *.mp3 diff --git a/src/bottle/cli.cr b/src/bottle/cli.cr index 13bb5a6..8e91b16 100755 --- a/src/bottle/cli.cr +++ b/src/bottle/cli.cr @@ -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 "} Specify song name for downloading #{Style.blue "-a, --artist "} Specify artist name for downloading + #{Style.blue "-s, --song "} Specify song name to download + #{Style.blue "-A, --album @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 diff --git a/src/glue/list.cr b/src/glue/list.cr index cc39082..44664bf 100755 --- a/src/glue/list.cr +++ b/src/glue/list.cr @@ -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 \ No newline at end of file diff --git a/src/glue/song.cr b/src/glue/song.cr index 848e33d..0e59155 100755 --- a/src/glue/song.cr +++ b/src/glue/song.cr @@ -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 @@ -120,8 +147,4 @@ class Song @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 +end \ No newline at end of file diff --git a/src/interact/ripper.cr b/src/interact/ripper.cr index 5486cf9..24e3607 100755 --- a/src/interact/ripper.cr +++ b/src/interact/ripper.cr @@ -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 diff --git a/src/search/spotify.cr b/src/search/spotify.cr index 2c6e537..36f209e 100755 --- a/src/search/spotify.cr +++ b/src/search/spotify.cr @@ -218,8 +218,8 @@ end # puts SpotifySearcher.new() -# .authorize("e4198f6a3f7b48029366f22528b5dc66", -# "ba057d0621a5496bbb64edccf758bde5") +# .authorize("XXXXXXXXXXXXXXX", +# "XXXXXXXXXXXXXXX") # .find_item("playlist", { # "name" => "Brain Food", # "username" => "spotify"