mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-14 04:35:23 +03:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7459d95df0 | |||
| b2521e2933 | |||
| f87bee7732 | |||
| 3aa36c1323 | |||
| 7c60b2cd31 | |||
| 68ff155a9e | |||
| 0d41ef0895 | |||
| 575f652813 | |||
| 11db7154a1 | |||
| 8285c41617 |
@@ -43,7 +43,7 @@ Some new features that I added:
|
||||
## Usage
|
||||
```
|
||||
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]
|
||||
[-o] [-n] [-s] [-e] [-i] [-v]
|
||||
[url ...]
|
||||
|
||||
Download Apple Music songs/music videos/albums/playlists
|
||||
@@ -66,12 +66,13 @@ options:
|
||||
-m, --disable-music-video-skip
|
||||
Disable music video skip on playlists/albums (default: False)
|
||||
-p, --prefer-hevc Prefer HEVC over AVC (default: False)
|
||||
-o, --overwrite Overwrite existing files (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)
|
||||
-i, --print-video-m3u8-url
|
||||
Print Video M3U8 URL (default: False)
|
||||
-v, --version show program's version number and exit
|
||||
```
|
||||
|
||||
@@ -84,6 +85,7 @@ options:
|
||||
* 1080p AVC 10mbps / AAC 256kbps
|
||||
* 1080p AVC 6.5bps / AAC 256kbps
|
||||
* 720p AVC 4mbps / AAC 256kbps
|
||||
* 480p AVC 2mbps / AAC 256kbps
|
||||
* 480p AVC 1.5mbps / AAC 256kbps
|
||||
* 360p AVC 1mbps / AAC 256kbps
|
||||
|
||||
|
||||
+24
-15
@@ -3,7 +3,7 @@ import argparse
|
||||
import traceback
|
||||
from .gamdl import Gamdl
|
||||
|
||||
__version__ = '1.1'
|
||||
__version__ = '1.4'
|
||||
|
||||
|
||||
def main():
|
||||
@@ -62,6 +62,12 @@ def main():
|
||||
action = 'store_true',
|
||||
help = 'Prefer HEVC over AVC'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--overwrite',
|
||||
action = 'store_true',
|
||||
help = 'Overwrite existing files'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--no-lrc',
|
||||
@@ -81,10 +87,10 @@ def main():
|
||||
help = 'Print execeptions'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-y',
|
||||
'--print-video-playlist',
|
||||
'-i',
|
||||
'--print-video-m3u8-url',
|
||||
action = 'store_true',
|
||||
help = 'Print Video M3U8 Playlist'
|
||||
help = 'Print Video M3U8 URL'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
@@ -106,6 +112,7 @@ def main():
|
||||
args.temp_path,
|
||||
args.final_path,
|
||||
args.no_lrc,
|
||||
args.overwrite,
|
||||
args.skip_cleanup
|
||||
)
|
||||
error_count = 0
|
||||
@@ -117,7 +124,7 @@ def main():
|
||||
exit(1)
|
||||
except:
|
||||
error_count += 1
|
||||
print(f'* Failed to check URL {i + 1}.')
|
||||
print(f'Failed to check URL {i + 1}/{len(args.url)}')
|
||||
if args.print_exceptions:
|
||||
traceback.print_exc()
|
||||
for i, url in enumerate(download_queue):
|
||||
@@ -127,37 +134,39 @@ def main():
|
||||
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)
|
||||
if args.print_video_m3u8_url:
|
||||
print(webplayback['hls-playlist-url'])
|
||||
tags = dl.get_tags_music_video(track['attributes']['url'].split('/')[-1].split('?')[0])
|
||||
final_location = dl.get_final_location('.m4v', tags)
|
||||
if dl.check_exists(final_location) and not args.overwrite:
|
||||
continue
|
||||
stream_url_video, stream_url_audio = dl.get_stream_url_music_video(webplayback)
|
||||
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:
|
||||
unsynced_lyrics, synced_lyrics = dl.get_lyrics(track_id)
|
||||
tags = dl.get_tags_song(webplayback, unsynced_lyrics)
|
||||
final_location = dl.get_final_location('.m4a', tags)
|
||||
if dl.check_exists(final_location) and not args.overwrite:
|
||||
continue
|
||||
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)
|
||||
|
||||
+38
-21
@@ -9,23 +9,21 @@ import functools
|
||||
import subprocess
|
||||
import shutil
|
||||
import gamdl.storefront_ids
|
||||
from pywidevine import Cdm
|
||||
from pywidevine import Device
|
||||
from pywidevine import Cdm, Device, PSSH, WidevinePsshData
|
||||
import requests
|
||||
import m3u8
|
||||
from yt_dlp import YoutubeDL
|
||||
from pywidevine.pssh import PSSH
|
||||
from pywidevine.license_protocol_pb2 import WidevinePsshData
|
||||
from mutagen.mp4 import MP4, MP4Cover
|
||||
|
||||
|
||||
class Gamdl:
|
||||
def __init__(self, wvd_location, 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, overwrite, 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.overwrite = overwrite
|
||||
self.skip_cleanup = skip_cleanup
|
||||
wvd_location = glob.glob(wvd_location)
|
||||
if not wvd_location:
|
||||
@@ -63,7 +61,7 @@ class Gamdl:
|
||||
def get_download_queue(self, url):
|
||||
download_queue = []
|
||||
product_id = url.split('/')[-1].split('i=')[-1].split('&')[0].split('?')[0]
|
||||
response = self.session.get(f'https://amp-api.music.apple.com/v1/catalog/{self.country}/?ids[songs]={product_id}&ids[albums]={product_id}&ids[playlists]={product_id}&ids[music-videos]={product_id}&l=en').json()['data'][0]
|
||||
response = self.session.get(f'https://amp-api.music.apple.com/v1/catalog/{self.country}/?ids[songs]={product_id}&ids[albums]={product_id}&ids[playlists]={product_id}&ids[music-videos]={product_id}').json()['data'][0]
|
||||
if response['type'] in ('songs', 'music-videos') and 'playParams' in response['attributes']:
|
||||
download_queue.append(response)
|
||||
if response['type'] == 'albums' or response['type'] == 'playlists':
|
||||
@@ -82,16 +80,13 @@ class Gamdl:
|
||||
response = self.session.post(
|
||||
'https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/webPlayback',
|
||||
json = {
|
||||
'salableAdamId': track_id
|
||||
'salableAdamId': track_id,
|
||||
'language': 'en-US'
|
||||
}
|
||||
).json()["songList"][0]
|
||||
return response
|
||||
|
||||
|
||||
def get_playlist_music_video(self, webplayback):
|
||||
return m3u8.load(webplayback['hls-playlist-url'])
|
||||
|
||||
|
||||
def get_stream_url_song(self, webplayback):
|
||||
return next(i for i in webplayback["assets"] if i["flavor"] == "28:ctrp256")['URL']
|
||||
|
||||
@@ -107,6 +102,25 @@ class Gamdl:
|
||||
return [i for i in playlist.playlists if 'avc' in i.stream_info.codecs][-1].uri
|
||||
|
||||
|
||||
def get_stream_url_music_video(self, webplayback):
|
||||
with YoutubeDL({
|
||||
'allow_unplayable_formats': True,
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
}) as ydl:
|
||||
playlist = ydl.extract_info(webplayback['hls-playlist-url'], download = False)
|
||||
if self.prefer_hevc:
|
||||
stream_url_video = playlist['formats'][-1]['url']
|
||||
else:
|
||||
stream_url_video = [i['url'] for i in playlist['formats'] if i['vcodec'] is not None and 'avc1' in i['vcodec']][-1]
|
||||
stream_url_audio = next(i['url'] for i in playlist['formats'] if 'audio-stereo-256' in i['format_id'])
|
||||
return stream_url_video, stream_url_audio
|
||||
|
||||
|
||||
def check_exists(self, final_location):
|
||||
return Path(final_location).exists()
|
||||
|
||||
|
||||
def get_encrypted_location_video(self, track_id):
|
||||
return self.temp_path / f'{track_id}_encrypted_video.mp4'
|
||||
|
||||
@@ -134,7 +148,7 @@ class Gamdl:
|
||||
'outtmpl': str(encrypted_location),
|
||||
'allow_unplayable_formats': True,
|
||||
'fixup': 'never',
|
||||
'overwrites': True,
|
||||
'overwrites': self.overwrite,
|
||||
'external_downloader': 'aria2c'
|
||||
}) as ydl:
|
||||
ydl.download(stream_url)
|
||||
@@ -166,10 +180,10 @@ class Gamdl:
|
||||
|
||||
def get_decryption_keys_song(self, stream_url, track_id):
|
||||
track_uri = m3u8.load(stream_url).keys[0].uri
|
||||
wvpsshdata = WidevinePsshData()
|
||||
wvpsshdata.algorithm = 1
|
||||
wvpsshdata.key_ids.append(base64.b64decode(track_uri.split(",")[1]))
|
||||
pssh = PSSH(base64.b64encode(wvpsshdata.SerializeToString()).decode('utf-8'))
|
||||
widevine_pssh_data = WidevinePsshData()
|
||||
widevine_pssh_data.algorithm = 1
|
||||
widevine_pssh_data.key_ids.append(base64.b64decode(track_uri.split(",")[1]))
|
||||
pssh = PSSH(base64.b64encode(widevine_pssh_data.SerializeToString()).decode('utf-8'))
|
||||
challenge = base64.b64encode(self.cdm.get_license_challenge(self.cdm_session, pssh)).decode('utf-8')
|
||||
license_b64 = self.get_license_b64(challenge, track_uri, track_id)
|
||||
self.cdm.parse_license(self.cdm_session, license_b64)
|
||||
@@ -230,12 +244,12 @@ class Gamdl:
|
||||
return requests.get(url).content
|
||||
|
||||
|
||||
def get_tags_song(self, webplayback, unsynced_lyrics, genre):
|
||||
def get_tags_song(self, webplayback, unsynced_lyrics):
|
||||
metadata = next(i for i in webplayback["assets"] if i["flavor"] == "28:ctrp256")['metadata']
|
||||
cover_url = next(i for i in webplayback["assets"] if i["flavor"] == "28:ctrp256")['artworkURL']
|
||||
tags = {
|
||||
'\xa9nam': [metadata['itemName']],
|
||||
'\xa9gen': [genre],
|
||||
'\xa9gen': [metadata['genre']],
|
||||
'aART': [metadata['playlistArtistName']],
|
||||
'\xa9alb': [metadata['playlistName']],
|
||||
'soar': [metadata['sort-artist']],
|
||||
@@ -273,13 +287,12 @@ class Gamdl:
|
||||
|
||||
|
||||
def get_tags_music_video(self, track_id):
|
||||
metadata = requests.get(f'https://itunes.apple.com/lookup?id={track_id}&entity=album&limit=200&country={self.country}&lang=en_US').json()['results']
|
||||
metadata = requests.get(f'https://itunes.apple.com/lookup?id={track_id}&entity=album&country={self.country}&lang=en_US').json()['results']
|
||||
extra_metadata = requests.get(f'https://music.apple.com/music-video/{metadata[0]["trackId"]}', headers = {'X-Apple-Store-Front': f'{self.storefront} t:music31'}).json()['storePlatformData']['product-dv']['results'][str(metadata[0]['trackId'])]
|
||||
tags = {
|
||||
'\xa9ART': [metadata[0]["artistName"]],
|
||||
'\xa9nam': [metadata[0]["trackCensoredName"]],
|
||||
'\xa9day': [metadata[0]["releaseDate"]],
|
||||
'cprt': [extra_metadata['copyright']],
|
||||
'\xa9gen': [metadata[0]['primaryGenreName']],
|
||||
'stik': [6],
|
||||
'atID': [metadata[0]['artistId']],
|
||||
@@ -288,6 +301,8 @@ class Gamdl:
|
||||
'sfID': [int(self.storefront.split('-')[0])],
|
||||
'covr': [MP4Cover(self.get_cover(metadata[0]["artworkUrl30"].replace('30x30bb.jpg', '600x600bb.jpg')), MP4Cover.FORMAT_JPEG)]
|
||||
}
|
||||
if 'copyright' in extra_metadata:
|
||||
tags['cprt'] = [extra_metadata['copyright']]
|
||||
if metadata[0]['trackExplicitness'] == 'notExplicit':
|
||||
tags['rtng'] = [0]
|
||||
elif metadata[0]['trackExplicitness'] == 'explicit':
|
||||
@@ -340,7 +355,7 @@ class Gamdl:
|
||||
final_location /= f'{self.get_sanizated_string(tags["©ART"][0], True)}/Unknown Album/'
|
||||
final_location /= f'{file_name}{file_extension}'
|
||||
try:
|
||||
if file_extension == '.m4v' and final_location.exists() and MP4(final_location).tags['cnID'][0] != tags['cnID'][0]:
|
||||
if file_extension == '.m4v' and final_location.exists() and MP4(final_location)['cnID'][0] != tags['cnID'][0]:
|
||||
final_location = self.get_final_location_overwrite_prevented_music_video(final_location)
|
||||
except:
|
||||
pass
|
||||
@@ -358,6 +373,7 @@ class Gamdl:
|
||||
decrypted_location_video,
|
||||
'-itags',
|
||||
'artist=placeholder',
|
||||
'-new',
|
||||
fixed_location
|
||||
],
|
||||
check = True
|
||||
@@ -373,6 +389,7 @@ class Gamdl:
|
||||
decrypted_location,
|
||||
'-itags',
|
||||
'artist=placeholder',
|
||||
'-new',
|
||||
fixed_location
|
||||
],
|
||||
check = True
|
||||
|
||||
Reference in New Issue
Block a user