Last active
September 30, 2024 08:19
-
-
Save Yepoleb/2122a671fc7793f6e303f1dbea95e372 to your computer and use it in GitHub Desktop.
Youtube Music downloader with automatic ID3 tagging. Uses mutagen and requests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# License: GPLv3 | |
import re | |
import json | |
import shutil | |
import subprocess | |
import sys | |
import urllib.parse | |
import requests | |
import mutagen | |
import mutagen.id3 | |
REFERER = "https://music.youtube.com/watch?v=" | |
URL = "https://music.youtube.com/youtubei/v1/next?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30" | |
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0" | |
NEXT_TEMPLATE = { | |
"context": { | |
"client": { | |
"clientName": "WEB_REMIX", | |
"clientVersion": "0.1", | |
"hl": "en", | |
"gl": "AT", | |
"experimentIds": [], | |
"utcOffsetMinutes": 120, | |
"locationInfo": { | |
"locationPermissionAuthorizationStatus": "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED" | |
}, | |
"musicAppInfo": { | |
"musicActivityMasterSwitch": "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", | |
"musicLocationMasterSwitch": "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE" | |
} | |
}, | |
"capabilities": {}, | |
"request": { | |
"internalExperimentFlags": [{ | |
"key": "client_streamz_web_flush_count", | |
"value": "2" | |
}, { | |
"key": "music_web_show_player_bezel", | |
"value": "true" | |
}, { | |
"key": "polymer_simple_endpoint", | |
"value": "true" | |
}, { | |
"key": "music_web_service_worker_caching_strategy", | |
"value": "" | |
}, { | |
"key": "music_web_enable_signed_out_playlist_sharing", | |
"value": "true" | |
}, { | |
"key": "music_web_like_buttons_on_album_detail_page", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_impressum", | |
"value": "true" | |
}, { | |
"key": "kevlar_enable_vimio_callback", | |
"value": "true" | |
}, { | |
"key": "ad_to_video_use_gel", | |
"value": "true" | |
}, { | |
"key": "music_show_buffering_spinner_in_player_bar", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_prelive_bylines", | |
"value": "true" | |
}, { | |
"key": "kevlar_import_vimio_behavior", | |
"value": "true" | |
}, { | |
"key": "music_enable_see_all_expand_buttons", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_scraper_metadata_for_detail_pages", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_kav_prompt", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_library_v2", | |
"value": "true" | |
}, { | |
"key": "music_enable_mix_free_tier_curated_playlists", | |
"value": "true" | |
}, { | |
"key": "web_logging_max_batch", | |
"value": "100" | |
}, { | |
"key": "music_display_empty_playlists", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_rtl", | |
"value": "true" | |
}, { | |
"key": "music_enable_album_page_alternate_releases_shelf", | |
"value": "true" | |
}, { | |
"key": "kevlar_use_vimio_behavior", | |
"value": "true" | |
}, { | |
"key": "enable_video_list_browse_for_topical_mixes", | |
"value": "true" | |
}, { | |
"key": "music_web_playlist_id_in_responsive_list_play_button", | |
"value": "true" | |
}, { | |
"key": "music_web_confirm_add_existing_song_to_playlist", | |
"value": "true" | |
}, { | |
"key": "music_mobile_web_app_install_page", | |
"value": "true" | |
}, { | |
"key": "music_web_serve_app_for_scrapers", | |
"value": "true" | |
}, { | |
"key": "force_music_enable_outertube_tastebuilder_browse", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_madison_account_support", | |
"value": "true" | |
}, { | |
"key": "music_web_session_check_interval_millis", | |
"value": "120000" | |
}, { | |
"key": "music_enable_share_for_search_albums", | |
"value": "true" | |
}, { | |
"key": "enable_web_music_player_error_message_renderer", | |
"value": "true" | |
}, { | |
"key": "music_web_fill_page_type_in_playlist_endpoints", | |
"value": "true" | |
}, { | |
"key": "music_web_handle_active_session_changes", | |
"value": "true" | |
}, { | |
"key": "flush_onbeforeunload", | |
"value": "true" | |
}, { | |
"key": "music_web_play_buttons_on_album_detail_page", | |
"value": "true" | |
}, { | |
"key": "music_enable_playlist_context_menu_share_endpoint", | |
"value": "true" | |
}, { | |
"key": "music_web_ignore_display_config_promotions", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_start_radio_from_album", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_prebuffering", | |
"value": "true" | |
}, { | |
"key": "music_library_shelf_item_fetch_count_override", | |
"value": "25" | |
}, { | |
"key": "web_gel_debounce_ms", | |
"value": "10000" | |
}, { | |
"key": "music_enable_start_radio_for_search_albums", | |
"value": "true" | |
}, { | |
"key": "music_web_track_focus_via_tab_key_in_carousels", | |
"value": "true" | |
}, { | |
"key": "enable_playability_filtering_in_entity_manager", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_audio_only_playback", | |
"value": "true" | |
}, { | |
"key": "music_enable_improve_your_recommendations_setting", | |
"value": "true" | |
}, { | |
"key": "music_playlist_detail_use_country_restrictions_from_ytms", | |
"value": "true" | |
}, { | |
"key": "kevlar_attach_vimio_behavior", | |
"value": "true" | |
}, { | |
"key": "music_enable_responsive_list_item_mobile_web_treatment", | |
"value": "true" | |
}, { | |
"key": "client_streamz_web_flush_interval_seconds", | |
"value": "60" | |
}, { | |
"key": "music_web_autoplay_blocked_prompt", | |
"value": "true" | |
}, { | |
"key": "music_entitlement_subscription_reads", | |
"value": "true" | |
}, { | |
"key": "music_web_signup_canonical_base_urls", | |
"value": "true" | |
}, { | |
"key": "music_web_disable_home_background_fetch", | |
"value": "true" | |
}, { | |
"key": "attach_child_on_gel_web", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_player_attestation", | |
"value": "true" | |
}, { | |
"key": "force_music_enable_outertube_artist_detail_channel_browse", | |
"value": "true" | |
}, { | |
"key": "music_web_send_initial_endpoint_for_landing_page_urls", | |
"value": "true" | |
}, { | |
"key": "music_enable_responsive_list_items_for_search", | |
"value": "true" | |
}, { | |
"key": "user_preference_collection_initial_browse_id", | |
"value": "FEmusic_tastebuilder" | |
}, { | |
"key": "music_web_app_non_teamfood", | |
"value": "true" | |
}, { | |
"key": "music_enable_album_entity_getbrowse", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_scraper_metadata", | |
"value": "true" | |
}, { | |
"key": "music_enable_prelive_bylines_localization", | |
"value": "true" | |
}, { | |
"key": "music_enable_navigation_client_streamz", | |
"value": "true" | |
}, { | |
"key": "log_foreground_heartbeat_music_web", | |
"value": "true" | |
}, { | |
"key": "music_web_disable_mobile_miniplayer", | |
"value": "true" | |
}, { | |
"key": "music_enable_prelive_bylines", | |
"value": "true" | |
}, { | |
"key": "music_fill_text_endpoints_in_two_row_item_renderer", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_corrected_tastebuilder_ordering", | |
"value": "true" | |
}, { | |
"key": "music_web_title_line_height", | |
"value": "1.2" | |
}, { | |
"key": "music_enable_you_there", | |
"value": "true" | |
}, { | |
"key": "music_web_enable_channel_alerts", | |
"value": "true" | |
}, { | |
"key": "video_to_ad_use_gel", | |
"value": "true" | |
}, { | |
"key": "music_web_red_subscriber_icon_border", | |
"value": "true" | |
}, { | |
"key": "music_web_send_initial_endpoint_for_playlist_urls", | |
"value": "true" | |
}, { | |
"key": "music_enable_nitrate_based_tastebuilder_onboarding_flow", | |
"value": "true" | |
}, { | |
"key": "enable_client_streamz_web", | |
"value": "true" | |
}, { | |
"key": "polymer2_not_shady_build", | |
"value": "true" | |
}, { | |
"key": "enable_premium_voluntary_pause", | |
"value": "true" | |
}, { | |
"key": "music_web_teamfood_dogfood_logos", | |
"value": "true" | |
}, { | |
"key": "music_web_hide_carousel_buttons_on_non_hover_devices", | |
"value": "true" | |
}, { | |
"key": "kevlar_enable_vimio_logging", | |
"value": "true" | |
}, { | |
"key": "debug_forced_promo_id", | |
"value": "" | |
}, { | |
"key": "music_web_enable_album_details_more_button", | |
"value": "true" | |
}, { | |
"key": "music_web_body_line_height", | |
"value": "1.4" | |
}], | |
"sessionIndex": 0 | |
}, | |
"user": { | |
"enableSafetyMode": False | |
} | |
}, | |
"enablePersistentPlaylistPanel": True, | |
"tunerSettingValue": "AUTOMIX_SETTING_NORMAL", | |
"videoId": "VIDEO_ID", | |
"isAudioOnly": True | |
} | |
if len(sys.argv) > 1: | |
video_id = sys.argv[1] | |
else: | |
video_id = input("Video ID or URL: ") | |
if len(video_id) != 11: | |
match = re.search("v=([\\w\\-_]+)", video_id) | |
if match is None: | |
print("Could not parse video id from URL") | |
exit(1) | |
video_id = match.group(1) | |
session = requests.Session() | |
session.headers["User-Agent"] = USER_AGENT | |
next_req = NEXT_TEMPLATE.copy() | |
next_req["videoId"] = video_id | |
resp = session.post( | |
URL, | |
data=json.dumps(next_req), | |
headers={"Content-Type": "application/json", "Referer": REFERER + video_id}) | |
resp_json = resp.json() | |
# ~ with open("resp_cache.json", "w") as f: | |
# ~ json.dump(resp_json, f, indent=2) | |
# ~ with open("resp_cache.json", "r") as f: | |
# ~ resp_json = json.load(f) | |
section_list = resp_json["contents"] \ | |
["singleColumnMusicWatchNextResultsRenderer"]["metadataScreen"] \ | |
["sectionListRenderer"]["contents"] | |
for section in section_list: | |
if "musicWatchMetadataRenderer" in section["itemSectionRenderer"]["contents"][0]: | |
watch_meta = section["itemSectionRenderer"]["contents"][0]["musicWatchMetadataRenderer"] | |
title = watch_meta["title"]["runs"][0]["text"] | |
second_row = [""] | |
for run in watch_meta["byline"]["runs"]: | |
text = run["text"] | |
if text == " • ": | |
second_row.append("") | |
else: | |
second_row[-1] += text | |
artist = second_row[0] | |
album = second_row[1] | |
year = second_row[2] | |
playlist = resp_json["contents"]["singleColumnMusicWatchNextResultsRenderer"] \ | |
["playlist"]["playlistPanelRenderer"]["contents"] | |
for video in playlist: | |
video_renderer = video["playlistPanelVideoRenderer"] | |
if not video_renderer["selected"]: | |
continue | |
thumbnail1 = video_renderer["thumbnail"]["thumbnails"][0]["url"] | |
image_base = thumbnail1.split("=")[0] | |
cover_url = image_base + "=w1000-h1000-l90-rj" | |
print("Title:", title) | |
print("Artist:", artist) | |
print("Album:", album) | |
print("Year:", year) | |
print("Cover:", cover_url) | |
cover = requests.get(cover_url).content | |
filename = "/tmp/youtubedl_{}.%(ext)s".format(video_id) | |
filename_mp3 = "/tmp/youtubedl_{}.mp3".format(video_id) | |
subprocess.run([ | |
"youtube-dl", "-f", "bestaudio", "-x", "--audio-format", "mp3", | |
"--audio-quality", "0", "-o", filename, video_id]) | |
mp3 = mutagen.File(filename_mp3) | |
mp3.tags.add(mutagen.id3.TIT2(text=[title])) | |
mp3.tags.add(mutagen.id3.TPE1(text=[artist])) | |
mp3.tags.add(mutagen.id3.TALB(text=[album])) | |
mp3.tags.add(mutagen.id3.TDRC(text=[year])) | |
mp3.tags.add(mutagen.id3.APIC( | |
mime="image/jpeg", type=mutagen.id3.PictureType.COVER_FRONT, | |
desc="Cover", data=cover)) | |
mp3.save() | |
final_name = (artist + " - " + title + ".mp3").replace("/", "_") | |
shutil.move(filename_mp3, final_name) | |
print("Renamed to", final_name) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment