diff --git a/README.md b/README.md index 5c01a37..62e44ba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Glomatico's ✨ Apple Music ✨ Downloader A Python script to download Apple Music songs/music videos/albums/playlists. -![Windows CMD usage example](https://i.imgur.com/6WeUCFh.png) +![Windows CMD usage example](https://i.imgur.com/18Azlg4.png) This is a rework of https://github.com/loveyoursupport/AppleMusic-Downloader/tree/661a274d62586b521feec5a7de6bee0e230fdb7d. @@ -25,19 +25,26 @@ Some new features that I added: * You can get them from here: * MP4Box: https://gpac.wp.imt.fr/downloads/ * mp4decrypt: https://www.bento4.com/downloads/ -4. Export your Apple Music cookies as `cookies.txt` and put it on the same folder that you will run the script +4. Export your Apple Music cookies as `cookies.txt` to the same folder that you will run the script * You can export your cookies by using this Google Chrome extension on Apple Music website: https://chrome.google.com/webstore/detail/open-cookiestxt/gdocmgbfkjnnpapoeobnolbbkoibbcif. Make sure to be logged in. -5. Put your L3 Widevine Device Keys (`device_client_id_blob` and `device_private_key` files) on the same folder that you will run the script - * You can get your L3 Widevine Device Keys by using Dumper: https://github.com/Diazole/dumper - * The generated `private_key.pem` and `client_id.bin` files should be renamed to `device_private_key` and `device_client_id_blob` respectively. +5. Put your Widevine Device file (.wvd) in the same folder that you will run the script + * You can use Dumper to dump your phone's L3 CDM: https://github.com/Diazole/dumper. Once you have the L3 CDM, you can use pywidevine to create the .wvd file from it. + 1. Install pywidevine with pip + ``` + pip install pywidevine pyyaml + ``` + 2. Create the .wvd file + ``` + pywidevine create-device -t ANDROID -l 3 -k private_key.pem -c client_id.bin -o . + ``` 6. (optional) Add aria2c to your PATH for faster downloads * You can get it from here: https://github.com/aria2/aria2/releases. ## Usage ``` -usage: gamdl [-h] [-u [URLS_TXT]] [-d DEVICE_PATH] [-f FINAL_PATH] [-t TEMP_PATH] [-c COOKIES_LOCATION] [-m] - [-p] [-n] [-s] [-e] [-y] [-v] - [url ...] +usage: gamdl [-h] [-u [URLS_TXT]] [-w WVD_LOCATION] [-f FINAL_PATH] [-t TEMP_PATH] [-c COOKIES_LOCATION] [-m] [-p] + [-n] [-s] [-e] [-y] [-v] + [url ...] Download Apple Music songs/music videos/albums/playlists @@ -48,8 +55,8 @@ options: -h, --help show this help message and exit -u [URLS_TXT], --urls-txt [URLS_TXT] Read URLs from a text file (default: None) - -d DEVICE_PATH, --device-path DEVICE_PATH - Widevine L3 device keys path (default: .) + -w WVD_LOCATION, --wvd-location WVD_LOCATION + .wvd file location (default: *.wvd) -f FINAL_PATH, --final-path FINAL_PATH Final Path (default: Apple Music) -t TEMP_PATH, --temp-path TEMP_PATH diff --git a/gamdl/__init__.py b/gamdl/__init__.py index 3c11314..159e6a7 100644 --- a/gamdl/__init__.py +++ b/gamdl/__init__.py @@ -3,7 +3,7 @@ import traceback from .gamdl import Gamdl -__version__ = '1.0' +__version__ = '1.1' def main(): @@ -27,10 +27,10 @@ def main(): nargs = '?' ) parser.add_argument( - '-d', - '--device-path', - default = '.', - help = 'Widevine L3 device keys path' + '-w', + '--wvd-location', + default = '*.wvd', + help = '.wvd file location' ) parser.add_argument( '-f', @@ -99,7 +99,7 @@ def main(): with open(args.urls_txt, 'r', encoding = 'utf8') as f: args.url = f.read().splitlines() dl = Gamdl( - args.device_path, + args.wvd_location, args.cookies_location, args.disable_music_video_skip, args.prefer_hevc, @@ -122,7 +122,7 @@ def main(): traceback.print_exc() for i, url in enumerate(download_queue): for j, track in enumerate(url): - print(f'Downloading "{track["attributes"]["name"]}" (track {j + 1} from URL {i + 1})...') + print(f'Downloading "{track["attributes"]["name"]}" (track {j + 1}/{len(url)} from URL {i + 1}/{len(download_queue)})') track_id = track['id'] try: webplayback = dl.get_webplayback(track_id) @@ -165,8 +165,8 @@ def main(): exit(1) except: error_count += 1 - print(f'* Failed to download "{track["attributes"]["name"]}" (track {j + 1} from URL {i + 1}).') + print(f'Failed to download "{track["attributes"]["name"]}" (track {j + 1}/{len(url)} from URL {i + 1}/{len(download_queue)})') if args.print_exceptions: traceback.print_exc() dl.cleanup() - print(f'Done ({error_count} error(s)).') + print(f'Done ({error_count} error(s))') diff --git a/gamdl/gamdl.py b/gamdl/gamdl.py index ddd76aa..f033b9b 100644 --- a/gamdl/gamdl.py +++ b/gamdl/gamdl.py @@ -1,4 +1,5 @@ from pathlib import Path +import glob from http.cookiejar import MozillaCookieJar import re import base64 @@ -19,23 +20,17 @@ class Gamdl: - def __init__(self, device_path, cookies_location, disable_music_video_skip, prefer_hevc, temp_path, final_path, no_lrc, skip_cleanup): + def __init__(self, wvd_location, cookies_location, disable_music_video_skip, prefer_hevc, temp_path, final_path, no_lrc, skip_cleanup): self.disable_music_video_skip = disable_music_video_skip self.prefer_hevc = prefer_hevc self.temp_path = Path(temp_path) self.final_path = Path(final_path) self.no_lrc = no_lrc self.skip_cleanup = skip_cleanup - with open(Path(device_path) / 'device_client_id_blob', 'rb') as client_id, open(Path(device_path) / 'device_private_key', 'rb') as private_key: - self.cdm = Cdm.from_device( - Device( - type_ = 'ANDROID', - security_level = 3, - flags = None, - private_key = private_key.read(), - client_id = client_id.read() - ) - ) + wvd_location = glob.glob(wvd_location) + if not wvd_location: + raise Exception('.wvd file not found') + self.cdm = Cdm.from_device(Device.load(Path(wvd_location[0]))) self.cdm_session = self.cdm.open() cookies = MozillaCookieJar(Path(cookies_location)) cookies.load(ignore_discard = True, ignore_expires = True) @@ -393,12 +388,11 @@ def make_lrc(self, final_location, synced_lyrics): def make_final(self, final_location, fixed_location, tags): final_location.parent.mkdir(parents = True, exist_ok = True) shutil.copy(fixed_location, final_location) - file = MP4(final_location).tags + file = MP4(final_location) file.update(tags) - file.save(final_location) + file.save() def cleanup(self): if self.temp_path.exists() and not self.skip_cleanup: shutil.rmtree(self.temp_path) -