#!/usr/bin/env python3 import argparse import datetime import glob import json import music_tag import os from pathlib import Path import sys import youtube_dl def cleanup_metadata_files(music_directory): files = glob.glob(os.path.join(music_directory, '*.json')) for file in files: os.remove(file) def format_youtube_date(date): default = "Unknown Year" try: fmt = "%Y%m%d" d = datetime.datetime.strptime(date, fmt) return d.year except Exception as ex: print(ex) return default def get_all_files(directory): things = glob.glob(os.path.join(directory, '*.mp3')) files = [] for thing in things: if os.path.isfile(thing): files.append(thing) return files def get_command_line_options(): parser = argparse.ArgumentParser( description="Download songs from YouTube Music") parser.add_argument("url", metavar="string", type=str, help="Playlist or Song URL to download") parser.add_argument("-c", "--cookies", metavar="string", type=str, help="Cookie file to use.") parser.add_argument("-o", "--output", metavar="string", type=str, help="Output directory to use") return parser.parse_args() # TODO: switch command line args to argparse def get_playlist_url(): return sys.argv[1] def get_video_urls_in_playlist(playlist_url, ytdl): videos = ytdl.extract_info(playlist_url, download=False) urls = [] for vid in videos['entries']: if 'webpage_url' in vid.keys() and vid['webpage_url'] is not None: urls.append(vid['webpage_url']) return urls def move_file(file, metadata, output_directory): artist = metadata['artist'].replace( '-', ' ').replace(' ', '_').replace("'", "").replace( "&", "and").lower() album = metadata['album'].replace( '-', ' ').replace(' ', '_').replace("'", "").replace( "&", "and").lower() title = metadata['title'].replace( '-', ' ').replace(' ', '_').replace("'", "").replace( "&", "and").lower() final_directory = os.path.join( output_directory, artist, album) Path(final_directory).mkdir(parents=True, exist_ok=True) # TODO: Include album title in filename # TODO: Research converting to mp3 instead of just naming it such. # TODO: Research better file formats over mp3? os.rename( file, os.path.join(final_directory, '{}_{}_{}.mp3'.format(artist, album, title))) def write_metadata_to_song_file(filename, metadata): file = music_tag.load_file(filename) file['name'] = metadata['title'] file['artist'] = metadata['artist'] file['album'] = metadata['album'] file['year'] = metadata['year'] file.save() def get_song_metadata_from_json(json, counter): metadata = { 'title': 'unknownsong', 'artist': 'unknownartist', 'album': 'unknownalbum', 'year': 1999, 'jamos_filename': 'jamos_unknwon_file_number_{}.mp3'.format(counter), } try: if ('artist' in json.keys()) and (json['artist'] is not None): metadata['artist'] = json['artist'] if len(metadata['artist'].split(',')) > 1: # If there are multiple artists, pick the first one # NOTE: This will break if the artist has a comma in their name metadata['artist'] = metadata['artist'].split(',')[0] if ('album' in json.keys()) and (json['album'] is not None): metadata['album'] = json['album'] if ('title' in json.keys()) and (json['title'] is not None): metadata['title'] = json['title'] if ('release_date' in json.keys()): metadata['year'] = format_youtube_date(json['release_date']) artist_for_filename = metadata['artist'].replace(' ', '_').lower() title_for_filename = metadata['title'].replace(' ', '_').lower() metadata['jamos_filename'] = '{}_{}.mp3'.format(artist_for_filename, title_for_filename) except Exception as ex: print(ex) return metadata def create_downloader(music_directory, cookies): audio_options = { 'format': 'mp3/bestaudio/best', 'cookiefile': cookies, 'outtmpl': music_directory + '%(title)s.%(ext)s', 'postprocessors': [ { 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }, {'key': 'FFmpegMetadata'}, ], 'writeinfojson': True } return youtube_dl.YoutubeDL(audio_options) def save_urls_from_playlist_to_file(filename, urls): try: f = open(filename, "w") for url in urls: f.writelines(url) f.close() except Exception as e: print(e) raise e if __name__ == "__main__": args = get_command_line_options() # Get the playlist url from the command line playlist_url = args.url music_directory = args.output or "~/Music" cookies = args.cookies or "~/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) # TODO: Save urls to file so we can start in the # middle of the playlist if needed urls = get_video_urls_in_playlist(playlist_url, ytdl) save_urls_from_playlist_to_file(os.path.join(music_directory, "jamos_urls.txt"), urls) for url in urls: try: ytdl.extract_info(url, download=True) except Exception as ex: # TODO: Handle this better print(ex) files = get_all_files(music_directory) counter = 1 for f in files: try: with open(f.replace('.mp3', '.info.json')) as json_file: json_data = json.load(json_file) metadata = get_song_metadata_from_json(json_data, counter) write_metadata_to_song_file(f, metadata) move_file(f, metadata, music_directory) counter += 1 except Exception as e: # just gonna print this and move on to the next file. print(e) cleanup_metadata_files(music_directory)