From 559a5d10bcca12943a1a1e71b3744216e7a46f6a Mon Sep 17 00:00:00 2001 From: Jackson Taylor Date: Fri, 24 Mar 2023 15:48:32 -0400 Subject: Download songs with album art --- jamos | 301 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 248 insertions(+), 53 deletions(-) diff --git a/jamos b/jamos index 8c8e2a8..78c377d 100755 --- a/jamos +++ b/jamos @@ -6,12 +6,33 @@ import json import music_tag import os from pathlib import Path +import requests +import shutil import sys import youtube_dl -import ytmusicapi -import musicpd +from ytmusicapi import YTMusic +# import musicpd -DEBUG = False +DEBUG = True + +# Includes the format characters +YOUTUBE_MUSIC_URL = "https://music.youtube.com/watch?v={}" + +""" +How the albums will be formatted: +{ + 'id': { + 'name': '', + 'tracks': [ + { + 'id': '', + 'name': '' + } + ], + } +} +""" +ALBUMS = {} def cleanup_metadata_files(music_directory): @@ -62,6 +83,11 @@ def get_command_line_options(): type=str, help="Cookie file to use.") + parser.add_argument("--headers", + metavar="header_path", + type=str, + help="Header file to use.") + parser.add_argument("-o", "--output", metavar="output_directory", @@ -132,7 +158,7 @@ def create_downloader(music_directory, cookies): audio_options = { 'format': 'mp3/bestaudio/best', 'cookiefile': cookies, - 'outtmpl': os.path.join(music_directory, '%(title)s.%(ext)s'), + 'outtmpl': os.path.join(music_directory, '%(id)s.%(ext)s'), 'postprocessors': [ { 'key': 'FFmpegExtractAudio', @@ -141,10 +167,13 @@ def create_downloader(music_directory, cookies): }, {'key': 'FFmpegMetadata'}, ], - 'writeinfojson': True, + # 'writeinfojson': True, 'quiet': not DEBUG } + return youtube_dl.YoutubeDL(audio_options) + + def get_video_urls_in_playlist(playlist_url, ytdl): videos = ytdl.extract_info(playlist_url, download=False) @@ -221,63 +250,125 @@ def write_metadata_to_song_file(filename, metadata): file.save() -if __name__ == "__main__": - try: - args = get_command_line_options() - except Exception as ex: - sys.exit() +def create_youtube_music_api_object(header_path): + return YTMusic(header_path) - music_directory = args.output or os.path.join(os.path.expanduser("~"), - "Music") - cookies = args.cookies or os.path.join(os.path.expanduser("~"), - "cookies.txt") - # From some testing, if your playlist is public, you don't have to use a - # cookie file. Youtube-dl doesn't break or throw if the file doesn't exist. - ytdl = create_downloader(music_directory, cookies) +def save_album(app, album_id): + raw_album = app.get_album(album_id) + # TODO: Add thumbnail_url + ALBUMS[album_id] = { + 'name': raw_album['title'], + 'tracks': [{'title': song['title'], 'id': song['videoId'], + 'track_num': index + 1} + for index, song in enumerate(raw_album['tracks'])], + 'year': raw_album['year'], + } - urls = [] + # TODO: Add get_thumbnail + thumbnail_url = raw_album['thumbnails'][-1]['url'] + r = requests.get(thumbnail_url, stream=True) + filename = './thumbnails/' + album_id + '.jpg' + + if not os.path.isfile(filename): + # Check if the image was retrieved successfully + if r.status_code == 200: + # Set decode_content value to True, + # otherwise the downloaded image file's size will be zero. + r.raw.decode_content = True + + # Open a local file with wb ( write binary ) permission. + with open(filename, 'wb') as f: + shutil.copyfileobj(r.raw, f) + + print('Image sucessfully Downloaded: ', filename) + else: + print('Image Couldn\'t be retreived') + + ALBUMS[album_id]['thumbnail_filepath'] = filename + + +def save_debug_data(filename, data, debug=True): + if debug: + with open(filename, 'w') as f: + f.write(data) + + +def parse_songs(app, all_raw_song_data): + all_parsed_songs = [] + for raw_song in all_raw_song_data: + song_id = raw_song['videoId'] + album_id = raw_song['album']['id'] + if album_id not in ALBUMS.keys(): + save_album(app, album_id) - if args.url: - playlist_url = args.url + album = ALBUMS[album_id] - print("Downloading urls...", end="") - urls = get_video_urls_in_playlist(playlist_url, ytdl) - print("Done.") - elif args.retryFile: - with open(args.retryFile) as retry_file: - urls = retry_file.read().splitlines() + track_num = None + for track in ALBUMS[album_id]['tracks']: + if track['id'] == song_id: + track_num = track['track_num'] - failed_urls = [] - for url in urls: + parsed_song = { + 'id': raw_song['videoId'], + 'title': raw_song['title'], + 'artists': [artist['name'] for artist in raw_song['artists']], + 'album': album['name'], + 'track': track_num, + 'url': YOUTUBE_MUSIC_URL.format(raw_song['videoId']), + 'year': album['year'], + 'thumbnail_filepath': album['thumbnail_filepath'] + } + all_parsed_songs.append(parsed_song) + + save_debug_data('parsed_album_data.json', json.dumps(ALBUMS), + debug=DEBUG) + save_debug_data('parsed_song_data.json', json.dumps(all_parsed_songs), + debug=DEBUG) + + return all_parsed_songs + + +def get_library_songs(app, song_limit, order='a_to_z'): + all_raw_song_data = app.get_library_songs(limit=song_limit, order=order) + + save_debug_data('raw_song_data.json', json.dumps(all_raw_song_data), + debug=DEBUG) + + all_parsed_songs = parse_songs(app, all_raw_song_data) + + return all_parsed_songs + + +def stuff(): + # TODO: Can you use the cookies from YTMusic here + # cookies = os.path.join(os.path.expanduser("~"), "cookies.txt") + cookies = './cookies.txt' + ytdl = create_downloader(music_directory, cookies) + + failed_songs = [] + for song in []: try: - print("Downloading: {}...".format(url), end='') - ytdl.extract_info(url, download=True) - print("Done.") + ytdl.extract_info(song['url'], download=True) except Exception as ex: print(ex) - print("Could not download: {}".format((url))) - failed_urls.append(url) + print("Could not download: {}".format((song))) + failed_songs.append(song) - try: - if len(failed_urls) > 1: - print("Saving failed urls to txt file.") - save_urls_from_playlist_to_file( - os.path.join(music_directory, "jamos_failed_urls.txt"), - failed_urls) - elif args.retryFile: - # Just because we don't have any failed urls in this run, doesn't - # mean that we can get rid of the retry file. We'll only remove it - # if it's been explicitly tried and we have no failed urls. - - # We've successfully downloaded all of the previously failed urls. - # Delete the file - os.remove(args.retryFile) - except Exception as ex: - print(ex) - print("Saving failed urls to file failed! Printing failed urls:") - for url in failed_urls: - print(url) + failed_songs = [] + + for song in all_songs: + try: + print("Downloading: {} - {} - {} from {}...".format( + song['title'], song['artist'], song['album'], song['url']), + end='') + ytdl.extract_info(song['url'], download=True) + sys.exit() + print("Done.") + except Exception as ex: + print(ex) + print("Could not download: {}".format((song['url']))) + failed_songs.append(song) files = get_all_files(music_directory) @@ -289,7 +380,7 @@ if __name__ == "__main__": json_data = json.load(json_file) metadata = get_song_metadata_from_json(json_data, counter) write_metadata_to_song_file(f, metadata) - print("Done".format(f)) + print("Done") print("Moving file...", end="") move_file(f, metadata, music_directory) print("Done") @@ -301,3 +392,107 @@ if __name__ == "__main__": print("Cleaning up JSON files...", end='') cleanup_metadata_files(music_directory) print("Done.") + + +# TODO: Implement +def retry_downloading_songs(): + # try: + # if len(failed_urls) > 1: + # print("Saving failed urls to txt file.") + # save_urls_from_playlist_to_file( + # os.path.join(music_directory, "jamos_failed_urls.txt"), + # failed_urls) + # elif args.retryFile: + # # Just because we don't have any failed urls in this run, doesn't + # # mean that we can get rid of the retry file. We'll only remove it + # # if it's been explicitly tried and we have no failed urls. + + # # We've successfully downloaded all of the previously failed urls. + # # Delete the file + # os.remove(args.retryFile) + # except Exception as ex: + # print(ex) + # print("Saving failed urls to file failed! Printing failed urls:") + # for url in failed_urls: + # print(url) + print("Not Implemented Yet!") + sys.exit() + + +# TODO: Implement +def handle_downloading_playlist(): + print("Not Implemented Yet!") + sys.exit() + pass + + +def sanitize_for_filename(filename): + new_filename = filename.replace(' ', '_') + new_filename = (''.join( + [s for s in new_filename if s.isalnum() or s == '_'])).lower() + return new_filename + + +if __name__ == "__main__": + try: + args = get_command_line_options() + except Exception as ex: + print(ex) + sys.exit() + + music_directory = args.output or os.path.join(os.path.expanduser("~"), + "Music") + cookies = args.cookies or os.path.join(os.path.expanduser("~"), + "cookies.txt") + + app = create_youtube_music_api_object(args.headers) + + all_songs = get_library_songs(app, 5000) + + # From some testing, if your playlist is public, you don't have to use a + # cookie file. Youtube-dl doesn't break or throw if the file doesn't exist. + ytdl = create_downloader(music_directory, cookies) + + for song in all_songs: + # download song + ytdl.extract_info(song['url'], download=True) + + # get filename song was downloaded to + filenames = glob.glob(os.path.join(music_directory, song['id'] + '.*')) + filename = filenames[0] + + # tag basic data + file = music_tag.load_file(filename) + file['name'] = song['title'] + file['artist'] = song['artists'][0] + file['albumartist'] = song['artists'][0] + file['album'] = song['album'] + file['tracknumber'] = song['track'] + file['year'] = song['year'] + + # include album cover + try: + with open(song['thumbnail_filepath'], 'rb') as img_in: + file['artwork'] = img_in.read() + except Exception as ex: + print(ex) + + # save music tag data + file.save() + + # move song to music_directory/artist/album/artist_album_title.ext + # excluding non filesafe characters + + artist_for_filename = sanitize_for_filename( + song['artists'][0].replace('$', 's')) # exception for $uicideboy$ + album_for_filename = sanitize_for_filename(song['album']) + title_for_filename = sanitize_for_filename(song['title']) + song_output_dir = os.path.join(music_directory, + artist_for_filename, + album_for_filename) + Path(song_output_dir).mkdir(parents=True, exist_ok=True) + new_filename = '{}_{}_{}.mp3'.format(artist_for_filename, + album_for_filename, + title_for_filename) + shutil.move(filename, os.path.join(song_output_dir, new_filename)) + -- cgit v1.2.3