summaryrefslogtreecommitdiff
path: root/jamos
diff options
context:
space:
mode:
authorJackson Taylor <jackson@jacksontaylor.xyz>2023-03-24 15:48:32 -0400
committerJackson Taylor <jackson@jacksontaylor.xyz>2023-03-24 15:48:32 -0400
commit559a5d10bcca12943a1a1e71b3744216e7a46f6a (patch)
tree276ff1a89f77435f8fe847ce42dad6b620471fcb /jamos
parent95daf78a498adffc3a848c825f645c3362f6bf8c (diff)
Download songs with album art
Diffstat (limited to 'jamos')
-rwxr-xr-xjamos301
1 files 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))
+