Skip to content

Commit

Permalink
New structure
Browse files Browse the repository at this point in the history
pypi
  • Loading branch information
glomatico committed Feb 4, 2023
1 parent 6f90e10 commit 92f4ed0
Show file tree
Hide file tree
Showing 25 changed files with 340 additions and 5,507 deletions.
12 changes: 3 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
/*
__pycache__/
!gamdl.py
!song_genres.py
!music_video_genres.py
!storefront_ids.py
!pywidevine
__pycache__
!gamdl
!requirements.txt
!.gitignore
device_client_id_blob
device_private_key
!pyproject.toml
83 changes: 56 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,83 @@
# 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/byjqmGF.png)
![Windows CMD usage example](https://i.imgur.com/6WeUCFh.png)

This is a rework of https://github.com/loveyoursupport/AppleMusic-Downloader/tree/661a274d62586b521feec5a7de6bee0e230fdb7d.

Some new features that I added:
- MP4Box for muxing
- Tags for music videos
- Multiple URLs input
- iTunes folder structure
- Embedded lyrics and .lrc file
- Auto set region
- Playlist support
- And much more!
* MP4Box for muxing
* Tags for music videos
* Multiple URLs input
* iTunes folder structure
* Embedded lyrics and .lrc file
* Auto set region
* Playlist support
* And much more!

## Setup
1. Install Python 3.8 or higher
2. Install the required packages using pip:
1. Install Python 3.7 or newer
2. Install gamdl with pip
```
pip install -r requirements.txt
pip install gamdl
```
3. Add MP4Box and mp4decrypt to your PATH
* 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 in the same folder as the script
4. Export your Apple Music cookies as `cookies.txt` and put it on 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 Keys (`device_client_id_blob` and `device_private_key` files) on `./pywidevine/L3/cdm/devices/android_generic` folder
* You can get your L3 Widevine Keys by using Dumper: https://github.com/Diazole/dumper
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.
6. (optional) Add aria2c to your PATH for faster downloads
* You can get it from here: https://aria2.github.io/.
* You can get it from here: https://github.com/aria2/aria2/releases.
## Usage
```
python gamdl.py [OPTIONS] [URLS]
```
Tracks are saved in `./Apple Music` by default, but the directory can be changed using `--final-path` argument.
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 ...]

Download Apple Music songs/music videos/albums/playlists

Use `--help` argument to see all available options.
positional arguments:
url Apple Music song/music video/album/playlist URL(s) (default: None)

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: .)
-f FINAL_PATH, --final-path FINAL_PATH
Final Path (default: Apple Music)
-t TEMP_PATH, --temp-path TEMP_PATH
Temp Path (default: temp)
-c COOKIES_LOCATION, --cookies-location COOKIES_LOCATION
Cookies location (default: cookies.txt)
-m, --disable-music-video-skip
Disable music video skip on playlists/albums (default: False)
-p, --prefer-hevc Prefer HEVC over AVC (default: False)
-n, --no-lrc Don't create .lrc file (default: False)
-s, --skip-cleanup Skip cleanup (default: False)
-e, --print-exceptions
Print execeptions (default: False)
-y, --print-video-playlist
Print Video M3U8 Playlist (default: False)
-v, --version show program's version number and exit
```
## Songs/Music Videos quality
* Songs:
* AAC 256kbps M4A
* 256kbps AAC
* Music Videos (varies depending on the video):
* HEVC 4K 12~20mbps M4V / AAC 256kbps (achieved by using `--prefer-hevc` argument)
* AVC 1080p 6.5~10mbps M4V / AAC 256kbps
* AVC 720p 4mbps M4V / AAC 256kbps
* AVC 480p 1.5mbps M4V / AAC 256kbps
* AVC 360p 1mbps M4V / AAC 256kbps
* 4K HEVC 20mbps / AAC 256kbps
* 4K HEVC 12mbps / AAC 256kbps
* 1080p AVC 10mbps / AAC 256kbps
* 1080p AVC 6.5bps / AAC 256kbps
* 720p AVC 4mbps / AAC 256kbps
* 480p AVC 1.5mbps / AAC 256kbps
* 360p AVC 1mbps / AAC 256kbps
Some videos may include EIA-608 captions.
Some videos may include EIA-608 closed captions.
172 changes: 172 additions & 0 deletions gamdl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import shutil
import argparse
import traceback
from .gamdl import Gamdl

__version__ = '1.0'


def main():
if not shutil.which('mp4decrypt'):
raise Exception('mp4decrypt is not on PATH')
if not shutil.which('MP4Box'):
raise Exception('MP4Box is not on PATH')
parser = argparse.ArgumentParser(
description = 'Download Apple Music songs/music videos/albums/playlists',
formatter_class = argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
'url',
help = 'Apple Music song/music video/album/playlist URL(s)',
nargs = '*'
)
parser.add_argument(
'-u',
'--urls-txt',
help = 'Read URLs from a text file',
nargs = '?'
)
parser.add_argument(
'-d',
'--device-path',
default = '.',
help = 'Widevine L3 device keys path'
)
parser.add_argument(
'-f',
'--final-path',
default = 'Apple Music',
help = 'Final Path'
)
parser.add_argument(
'-t',
'--temp-path',
default = 'temp',
help = 'Temp Path'
)
parser.add_argument(
'-c',
'--cookies-location',
default = 'cookies.txt',
help = 'Cookies location'
)
parser.add_argument(
'-m',
'--disable-music-video-skip',
action = 'store_true',
help = 'Disable music video skip on playlists/albums'
)
parser.add_argument(
'-p',
'--prefer-hevc',
action = 'store_true',
help = 'Prefer HEVC over AVC'
)
parser.add_argument(
'-n',
'--no-lrc',
action = 'store_true',
help = "Don't create .lrc file"
)
parser.add_argument(
'-s',
'--skip-cleanup',
action = 'store_true',
help = 'Skip cleanup'
)
parser.add_argument(
'-e',
'--print-exceptions',
action = 'store_true',
help = 'Print execeptions'
)
parser.add_argument(
'-y',
'--print-video-playlist',
action = 'store_true',
help = 'Print Video M3U8 Playlist'
)
parser.add_argument(
'-v',
'--version',
action = 'version',
version = f'%(prog)s {__version__}'
)
args = parser.parse_args()
if not args.url and not args.urls_txt:
parser.error('you must specify an url or a text file using -u/--urls-txt')
if args.urls_txt:
with open(args.urls_txt, 'r', encoding = 'utf8') as f:
args.url = f.read().splitlines()
dl = Gamdl(
args.device_path,
args.cookies_location,
args.disable_music_video_skip,
args.prefer_hevc,
args.temp_path,
args.final_path,
args.no_lrc,
args.skip_cleanup
)
error_count = 0
download_queue = []
for i, url in enumerate(args.url):
try:
download_queue.append(dl.get_download_queue(url.strip()))
except KeyboardInterrupt:
exit(1)
except:
error_count += 1
print(f'* Failed to check URL {i + 1}.')
if args.print_exceptions:
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})...')
track_id = track['id']
try:
webplayback = dl.get_webplayback(track_id)
if track['type'] == 'music-videos':
playlist = dl.get_playlist_music_video(webplayback)
if args.print_video_playlist:
print(playlist.dumps())
stream_url_audio = dl.get_stream_url_music_video_audio(playlist)
decryption_keys_audio = dl.get_decryption_keys_music_video(stream_url_audio, track_id)
encrypted_location_audio = dl.get_encrypted_location_audio(track_id)
dl.download(encrypted_location_audio, stream_url_audio)
decrypted_location_audio = dl.get_decrypted_location_audio(track_id)
dl.decrypt(encrypted_location_audio, decrypted_location_audio, decryption_keys_audio)
stream_url_video = dl.get_stream_url_music_video_video(playlist)
decryption_keys_video = dl.get_decryption_keys_music_video(stream_url_video, track_id)
encrypted_location_video = dl.get_encrypted_location_video(track_id)
dl.download(encrypted_location_video, stream_url_video)
decrypted_location_video = dl.get_decrypted_location_video(track_id)
dl.decrypt(encrypted_location_video, decrypted_location_video, decryption_keys_video)
tags = dl.get_tags_music_video(track['attributes']['url'].split('/')[-1])
fixed_location = dl.get_fixed_location(track_id, '.m4v')
final_location = dl.get_final_location('.m4v', tags)
dl.fixup_music_video(decrypted_location_audio, decrypted_location_video, fixed_location)
dl.make_final(final_location, fixed_location, tags)
else:
stream_url = dl.get_stream_url_song(webplayback)
decryption_keys = dl.get_decryption_keys_song(stream_url, track_id)
encrypted_location = dl.get_encrypted_location_audio(track_id)
dl.download(encrypted_location, stream_url)
decrypted_location = dl.get_decrypted_location_audio(track_id)
dl.decrypt(encrypted_location, decrypted_location, decryption_keys)
unsynced_lyrics, synced_lyrics = dl.get_lyrics(track_id)
tags = dl.get_tags_song(webplayback, unsynced_lyrics, track['attributes']['genreNames'][0])
fixed_location = dl.get_fixed_location(track_id, '.m4a')
final_location = dl.get_final_location('.m4a', tags)
dl.fixup_song(decrypted_location, fixed_location)
dl.make_final(final_location, fixed_location, tags)
dl.make_lrc(final_location, synced_lyrics)
except KeyboardInterrupt:
exit(1)
except:
error_count += 1
print(f'* Failed to download "{track["attributes"]["name"]}" (track {j + 1} from URL {i + 1}).')
if args.print_exceptions:
traceback.print_exc()
dl.cleanup()
print(f'Done ({error_count} error(s)).')
4 changes: 4 additions & 0 deletions gamdl/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import gamdl

if __name__ == "__main__":
gamdl.main()
Loading

0 comments on commit 92f4ed0

Please sign in to comment.