Compare commits

...

24 Commits

Author SHA1 Message Date
alacleaker f6d726e466 bump version 2024-04-09 07:16:16 -03:00
alacleaker 61b1bf1e55 fix song not downloadable detection 2024-04-09 07:16:07 -03:00
alacleaker 3ae6709ccb bump version 2024-04-09 06:54:07 -03:00
alacleaker 1f00e4fb9f adjust non-legacy message 2024-04-09 06:53:53 -03:00
alacleaker 714d47bb13 Update README.md 2024-04-09 06:52:01 -03:00
alacleaker 46e3a92d4f adjust non-legacy warn 2024-04-09 06:51:56 -03:00
alacleaker 42b536d271 warn about non-legacy 2024-04-09 06:49:43 -03:00
alacleaker dac8d5eed9 add LEGACY_CODECS 2024-04-09 06:49:32 -03:00
alacleaker 2956f20dfa Update README.md 2024-04-08 21:18:53 -03:00
alacleaker 8f76743a3b Update README.md 2024-04-08 21:13:17 -03:00
alacleaker 3096bbc79d adjust song is not available message 2024-04-08 21:06:57 -03:00
alacleaker ed49d7bd5f adjust song is not available message 2024-04-08 21:06:47 -03:00
alacleaker 0ea72d0b78 adjust song is not available message 2024-04-08 21:06:37 -03:00
alacleaker ae490320ad bump version 2024-04-08 21:04:57 -03:00
alacleaker e40668e6ec adjust song is not available message 2024-04-08 21:04:22 -03:00
alacleaker 62c695b5ff bump version 2024-04-08 19:50:50 -03:00
alacleaker 5d9c8c1f0b add from __future__ import annotations 2024-04-08 19:50:39 -03:00
alacleaker 54d640230a bump version 2024-04-08 18:25:10 -03:00
alacleaker 3d272a6891 use m4v with ffmpeg when compatible codecs 2024-04-08 17:29:45 -03:00
alacleaker e99ed0eb5a fix wrong format and rename to use_mp4_format 2024-04-08 17:00:20 -03:00
alacleaker 86b5029773 add stream info codec to remux 2024-04-08 16:59:44 -03:00
alacleaker 3df0a91d3f adjust mp4 flag codecs 2024-04-08 16:54:17 -03:00
alacleaker d356596cf4 add codec to StreamInfo 2024-04-08 16:54:03 -03:00
alacleaker cbd2df79b7 add -f mp4 for atmos 2024-04-08 16:46:36 -03:00
7 changed files with 84 additions and 50 deletions
+4 -2
View File
@@ -29,7 +29,7 @@ A Python script to download Apple Music songs/music videos/albums/playlists/post
```bash
pip install gamdl
```
2. Place your cookies in the same directory you will run the script from and name it as `cookies.txt`
2. Place your cookies in the same directory you will run gamdl from and name it as `cookies.txt`
## Usage
* Download a song
@@ -144,8 +144,10 @@ The following codecs are available:
* `aac-he-downmix`
* `alac`
* `atmos`
* `ask`
* When using this option, the script will ask you which **non-legacy** codec to use.
**Support for non-legacy codecs are not guaranteed, as most of the songs cannot be decrypted when using non-legacy codecs.**
**Support for non-legacy codecs are not guaranteed, as most of the songs cannot be downloaded when using non-legacy codecs.**
### Music videos codecs
The following codecs are available:
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.0"
__version__ = "2.1.4"
+17 -16
View File
@@ -419,6 +419,11 @@ def main(
skip_mv = True
else:
skip_mv = False
if codec_song not in LEGACY_CODECS:
logger.warn(
"You have chosen a non-legacy codec. Support for non-legacy codecs are not guaranteed, "
"as most of the songs cannot be downloaded when using non-legacy codecs."
)
error_count = 0
if read_urls_as_txt:
urls = [url.strip() for url in Path(urls[0]).read_text().splitlines()]
@@ -477,10 +482,7 @@ def main(
f'({queue_progress}) Song already exists at "{final_path}", skipping'
)
else:
if codec_song in (
SongCodec.AAC_LEGACY,
SongCodec.AAC_HE_LEGACY,
):
if codec_song in LEGACY_CODECS:
logger.debug("Getting stream info")
stream_info = downloader_song_legacy.get_stream_info(
webplayback
@@ -491,14 +493,10 @@ def main(
)
else:
stream_info = downloader_song.get_stream_info(track)
if not stream_info.pssh:
if not stream_info.stream_url or not stream_info.pssh:
logger.warning(
f"({queue_progress}) Song does not contain Widevine DRM, skipping"
)
continue
elif not stream_info.stream_url:
logger.warning(
f"({queue_progress}) Song is not available with the selected codec, skipping"
f"({queue_progress}) Song is not downloadable or is not"
" available in the chosen codec, skipping"
)
continue
logger.debug("Getting decryption key")
@@ -510,10 +508,7 @@ def main(
remuxed_path = downloader_song.get_remuxed_path(track["id"])
logger.debug(f"Downloading to {encrypted_path}")
downloader.download(encrypted_path, stream_info.stream_url)
if codec_song in (
SongCodec.AAC_LEGACY,
SongCodec.AAC_HE_LEGACY,
):
if codec_song in LEGACY_CODECS:
logger.debug(f"Remuxing/Decrypting to {remuxed_path}")
downloader_song_legacy.remux(
encrypted_path,
@@ -527,7 +522,11 @@ def main(
encrypted_path, decrypted_path, decryption_key
)
logger.debug(f"Remuxing to {final_path}")
downloader_song.remux(decrypted_path, remuxed_path)
downloader_song.remux(
decrypted_path,
remuxed_path,
stream_info.codec,
)
logger.debug("Applying tags")
downloader.apply_tags(remuxed_path, tags, cover_url)
logger.debug(f"Moving to {final_path}")
@@ -635,6 +634,8 @@ def main(
decrypted_path_video,
decrypted_path_audio,
remuxed_path,
stream_info_video.codec,
stream_info_audio.codec,
)
logger.debug("Applying tags")
downloader.apply_tags(remuxed_path, tags, cover_url)
+4 -1
View File
@@ -218,4 +218,7 @@ EXCLUDED_CONFIG_FILE_PARAMS = (
X_NOT_FOUND_STRING = '{} not found at "{}"'
AMP_API_HOSTNAME = "https://amp-api.music.apple.com"
LEGACY_CODECS = [
SongCodec.AAC_LEGACY,
SongCodec.AAC_HE_LEGACY,
]
+35 -25
View File
@@ -1,3 +1,4 @@
import re
import subprocess
import urllib.parse
from pathlib import Path
@@ -13,6 +14,8 @@ from .models import StreamInfo
class DownloaderMusicVideo:
MP4_FORMAT_CODECS = ["hvc1", "ec-3"]
def __init__(
self,
downloader: Downloader,
@@ -33,10 +36,10 @@ class DownloaderMusicVideo:
).geturl()
return m3u8.load(stream_url_master_new).data
def get_stream_url_video(
def get_playlist_video(
self,
playlists: list[dict],
):
) -> dict:
playlists_filtered = [
playlist
for playlist in playlists
@@ -53,12 +56,12 @@ class DownloaderMusicVideo:
)
]
playlists_filtered.sort(key=lambda x: x["stream_info"]["bandwidth"])
return playlists_filtered[-1]["uri"]
return playlists_filtered[-1]
def get_stream_url_video_from_user(
def get_playlist_video_from_user(
self,
playlists: list[dict],
):
) -> dict:
table = [
[
i,
@@ -76,12 +79,12 @@ class DownloaderMusicVideo:
)
except click.exceptions.Abort:
raise KeyboardInterrupt()
return playlists[choice]["uri"]
return playlists[choice]
def get_stream_url_audio(
def get_playlist_audio(
self,
playlists: list[dict],
) -> str:
) -> dict:
stream_url = next(
(
playlist
@@ -89,13 +92,13 @@ class DownloaderMusicVideo:
if playlist["group_id"] == "audio-stereo-256"
),
None,
)["uri"]
)
return stream_url
def get_stream_url_audio_from_user(
def get_playlist_audio_from_user(
self,
playlists: list[dict],
):
) -> dict:
table = [
[
i,
@@ -113,7 +116,7 @@ class DownloaderMusicVideo:
)
except click.exceptions.Abort:
raise KeyboardInterrupt()
return playlists[choice]["uri"]
return playlists[choice]
def get_pssh(self, m3u8_data: dict):
return next(
@@ -128,13 +131,11 @@ class DownloaderMusicVideo:
def get_stream_info_video(self, m3u8_master_data: dict) -> StreamInfo:
stream_info = StreamInfo()
if self.codec != MusicVideoCodec.ASK:
stream_info.stream_url = self.get_stream_url_video(
m3u8_master_data["playlists"]
)
playlist = self.get_playlist_video(m3u8_master_data["playlists"])
else:
stream_info.stream_url = self.get_stream_url_video_from_user(
m3u8_master_data["playlists"]
)
playlist = self.get_playlist_video_from_user(m3u8_master_data["playlists"])
stream_info.stream_url = playlist["uri"]
stream_info.codec = playlist["stream_info"]["codecs"]
m3u8_data = m3u8.load(stream_info.stream_url).data
stream_info.pssh = self.get_pssh(m3u8_data)
return stream_info
@@ -142,13 +143,13 @@ class DownloaderMusicVideo:
def get_stream_info_audio(self, m3u8_master_data: dict) -> StreamInfo:
stream_info = StreamInfo()
if self.codec != MusicVideoCodec.ASK:
stream_info.stream_url = self.get_stream_url_audio(
m3u8_master_data["media"]
)
playlist = self.get_playlist_audio(m3u8_master_data["media"])
else:
stream_info.stream_url = self.get_stream_url_audio_from_user(
m3u8_master_data["media"]
)
playlist = self.get_playlist_audio_from_user(m3u8_master_data["media"])
stream_info.stream_url = playlist["uri"]
stream_info.codec = re.search(r"_([^_]+)\.m3u8", stream_info.stream_url).group(
1
)
m3u8_data = m3u8.load(stream_info.stream_url).data
stream_info.pssh = self.get_pssh(m3u8_data)
return stream_info
@@ -255,7 +256,12 @@ class DownloaderMusicVideo:
decrypted_path_video: Path,
decrypte_path_audio: Path,
fixed_path: Path,
codec_video: str,
codec_audio: str,
):
use_mp4_flag = any(
codec_video.startswith(codec) for codec in self.MP4_FORMAT_CODECS
) or any(codec_audio.startswith(codec) for codec in self.MP4_FORMAT_CODECS)
subprocess.run(
[
self.downloader.ffmpeg_path_full,
@@ -269,7 +275,7 @@ class DownloaderMusicVideo:
"-movflags",
"+faststart",
"-f",
"mp4",
"mp4" if use_mp4_flag else "ipod",
"-c",
"copy",
"-c:s",
@@ -284,6 +290,8 @@ class DownloaderMusicVideo:
decrypted_path_video: Path,
decrypted_path_audio: Path,
remuxed_path: Path,
codec_video: str,
codec_audio: str,
):
if self.downloader.remux_mode == RemuxMode.MP4BOX:
self.remux_mp4box(
@@ -296,6 +304,8 @@ class DownloaderMusicVideo:
decrypted_path_video,
decrypted_path_audio,
remuxed_path,
codec_video,
codec_audio,
)
def get_cover_path(self, final_path: Path) -> Path:
+22 -5
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import base64
import datetime
import json
@@ -19,6 +21,7 @@ from .models import Lyrics, StreamInfo
class DownloaderSong:
DEFAULT_DECRYPTION_KEY = "32b8ade1769e26b1ffb8986352793fc6"
MP4_FORMAT_CODECS = ["ec-3"]
def __init__(
self,
@@ -40,7 +43,7 @@ class DownloaderSong:
None,
)
if not drm_info_raw:
raise Exception("DRM info not found")
return None
return json.loads(base64.b64decode(drm_info_raw["value"]).decode("utf-8"))
def get_asset_infos(self, m3u8_data: dict) -> dict:
@@ -111,6 +114,8 @@ class DownloaderSong:
m3u8_obj = m3u8.load(m3u8_url)
m3u8_data = m3u8_obj.data
drm_infos = self.get_drm_infos(m3u8_data)
if not drm_infos:
return stream_info
asset_infos = self.get_asset_infos(m3u8_data)
if self.codec == SongCodec.ASK:
playlist = self.get_playlist_from_user(m3u8_data)
@@ -123,6 +128,7 @@ class DownloaderSong:
drm_ids = asset_infos[variant_id]["AUDIO-SESSION-KEY-IDS"]
pssh = self.get_pssh(drm_infos, drm_ids)
stream_info.pssh = pssh
stream_info.codec = playlist["stream_info"]["codecs"]
return stream_info
@staticmethod
@@ -294,13 +300,13 @@ class DownloaderSong:
check=True,
)
def remux(self, decrypted_path: Path, remuxed_path: Path) -> None:
def remux(self, decrypted_path: Path, remuxed_path: Path, codec: str):
if self.downloader.remux_mode == RemuxMode.MP4BOX:
self.remux_mp4box(decrypted_path, remuxed_path)
elif self.downloader.remux_mode == RemuxMode.FFMPEG:
self.remux_ffmpeg(decrypted_path, remuxed_path)
self.remux_ffmpeg(decrypted_path, remuxed_path, codec)
def remux_mp4box(self, decrypted_path: Path, remuxed_path: Path) -> None:
def remux_mp4box(self, decrypted_path: Path, remuxed_path: Path):
subprocess.run(
[
self.downloader.mp4box_path_full,
@@ -315,7 +321,16 @@ class DownloaderSong:
check=True,
)
def remux_ffmpeg(self, decrypted_path: Path, remuxed_path: Path) -> None:
def remux_ffmpeg(
self,
decrypted_path: Path,
remuxed_path: Path,
codec: str,
):
use_mp4_format = any(
codec.startswith(possible_codec)
for possible_codec in self.MP4_FORMAT_CODECS
)
subprocess.run(
[
self.downloader.ffmpeg_path_full,
@@ -326,6 +341,8 @@ class DownloaderSong:
decrypted_path,
"-c",
"copy",
"-f",
"mp4" if use_mp4_format else "ipod",
"-movflags",
"+faststart",
remuxed_path,
+1
View File
@@ -23,3 +23,4 @@ class Lyrics:
class StreamInfo:
stream_url: str = None
pssh: str = None
codec: str = None