mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 12:15:18 +03:00
Refactor template options and add playlist file support
This commit is contained in:
+32
-32
@@ -287,51 +287,51 @@ def make_sync(func):
|
||||
help="Cover format.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-folder-album",
|
||||
"--album-folder-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_folder_album"].default,
|
||||
default=base_downloader_sig.parameters["album_folder_template"].default,
|
||||
help="Template folder for tracks that are part of an album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-folder-compilation",
|
||||
"--compilation-folder-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_folder_compilation"].default,
|
||||
default=base_downloader_sig.parameters["compilation_folder_template"].default,
|
||||
help="Template folder for tracks that are part of a compilation album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-file-single-disc",
|
||||
"--single-disc-folder-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_file_single_disc"].default,
|
||||
default=base_downloader_sig.parameters["single_disc_folder_template"].default,
|
||||
help="Template file for the tracks that are part of a single-disc album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-file-multi-disc",
|
||||
"--multi-disc-folder-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_file_multi_disc"].default,
|
||||
default=base_downloader_sig.parameters["multi_disc_folder_template"].default,
|
||||
help="Template file for the tracks that are part of a multi-disc album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-folder-no-album",
|
||||
"--no-album-folder-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_folder_no_album"].default,
|
||||
default=base_downloader_sig.parameters["no_album_folder_template"].default,
|
||||
help="Template folder for the tracks that are not part of an album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-file-no-album",
|
||||
"--no-album-file-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_file_no_album"].default,
|
||||
default=base_downloader_sig.parameters["no_album_file_template"].default,
|
||||
help="Template file for the tracks that are not part of an album.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-file-playlist",
|
||||
"--playlist-file-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_file_playlist"].default,
|
||||
default=base_downloader_sig.parameters["playlist_file_template"].default,
|
||||
help="Template file for the M3U8 playlist.",
|
||||
)
|
||||
@click.option(
|
||||
"--template-date",
|
||||
"--date-tag-template",
|
||||
type=str,
|
||||
default=base_downloader_sig.parameters["template_date"].default,
|
||||
default=base_downloader_sig.parameters["date_tag_template"].default,
|
||||
help="Date tag template.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -440,14 +440,14 @@ async def main(
|
||||
download_mode: DownloadMode,
|
||||
remux_mode: RemuxMode,
|
||||
cover_format: CoverFormat,
|
||||
template_folder_album: str,
|
||||
template_folder_compilation: str,
|
||||
template_file_single_disc: str,
|
||||
template_file_multi_disc: str,
|
||||
template_folder_no_album: str,
|
||||
template_file_no_album: str,
|
||||
template_file_playlist: str,
|
||||
template_date: str,
|
||||
album_folder_template: str,
|
||||
compilation_folder_template: str,
|
||||
single_disc_folder_template: str,
|
||||
multi_disc_folder_template: str,
|
||||
no_album_folder_template: str,
|
||||
no_album_file_template: str,
|
||||
playlist_file_template: str,
|
||||
date_tag_template: str,
|
||||
exclude_tags: list[str],
|
||||
cover_size: int,
|
||||
truncate: int,
|
||||
@@ -511,14 +511,14 @@ async def main(
|
||||
download_mode=download_mode,
|
||||
remux_mode=remux_mode,
|
||||
cover_format=cover_format,
|
||||
template_folder_album=template_folder_album,
|
||||
template_folder_compilation=template_folder_compilation,
|
||||
template_file_single_disc=template_file_single_disc,
|
||||
template_file_multi_disc=template_file_multi_disc,
|
||||
template_folder_no_album=template_folder_no_album,
|
||||
template_file_no_album=template_file_no_album,
|
||||
template_file_playlist=template_file_playlist,
|
||||
template_date=template_date,
|
||||
album_folder_template=album_folder_template,
|
||||
compilation_folder_template=compilation_folder_template,
|
||||
single_disc_folder_template=single_disc_folder_template,
|
||||
multi_disc_folder_template=multi_disc_folder_template,
|
||||
no_album_folder_template=no_album_folder_template,
|
||||
no_album_file_template=no_album_file_template,
|
||||
playlist_file_template=playlist_file_template,
|
||||
date_tag_template=date_tag_template,
|
||||
exclude_tags=exclude_tags,
|
||||
cover_size=cover_size,
|
||||
truncate=truncate,
|
||||
|
||||
@@ -9,9 +9,11 @@ TEMP_PATH_TEMPLATE = "gamdl_temp_{}"
|
||||
ILLEGAL_CHARS_RE = r'[\\/:*?"<>|;]'
|
||||
ILLEGAL_CHAR_REPLACEMENT = "_"
|
||||
|
||||
SONG_MEDIA_TYPE = {"songs", "library-songs"}
|
||||
MUSIC_VIDEO_MEDIA_TYPE = {"music-videos", "library-music-videos"}
|
||||
UPLOADED_VIDEO_MEDIA_TYPE = {"uploaded-videos"}
|
||||
SONG_MEDIA_TYPE = {"song", "songs", "library-songs"}
|
||||
ALBUM_MEDIA_TYPE = {"album", "albums", "library-albums"}
|
||||
MUSIC_VIDEO_MEDIA_TYPE = {"music-video", "music-videos", "library-music-videos"}
|
||||
UPLOADED_VIDEO_MEDIA_TYPE = {"post", "uploaded-videos"}
|
||||
PLAYLIST_MEDIA_TYPE = {"playlist", "playlists", "library-playlists"}
|
||||
|
||||
VALID_URL_PATTERN = re.compile(
|
||||
r"https://music\.apple\.com"
|
||||
|
||||
@@ -3,7 +3,9 @@ from pathlib import Path
|
||||
|
||||
from ..utils import safe_gather
|
||||
from .constants import (
|
||||
ALBUM_MEDIA_TYPE,
|
||||
MUSIC_VIDEO_MEDIA_TYPE,
|
||||
PLAYLIST_MEDIA_TYPE,
|
||||
SONG_MEDIA_TYPE,
|
||||
UPLOADED_VIDEO_MEDIA_TYPE,
|
||||
VALID_URL_PATTERN,
|
||||
@@ -12,10 +14,7 @@ from .downloader_base import AppleMusicBaseDownloader
|
||||
from .downloader_music_video import AppleMusicMusicVideoDownloader
|
||||
from .downloader_song import AppleMusicSongDownloader
|
||||
from .downloader_uploaded_video import AppleMusicUploadedVideoDownloader
|
||||
from .exceptions import (
|
||||
MediaFormatNotAvailableError,
|
||||
MediaNotStreamableError,
|
||||
)
|
||||
from .exceptions import MediaFormatNotAvailableError, MediaNotStreamableError
|
||||
from .types import DownloadItem, UrlInfo
|
||||
|
||||
|
||||
@@ -35,6 +34,7 @@ class AppleMusicDownloader:
|
||||
async def get_single_download_item(
|
||||
self,
|
||||
media_metadata: dict,
|
||||
playlist_metadata: dict = None,
|
||||
) -> DownloadItem:
|
||||
download_item = None
|
||||
|
||||
@@ -72,6 +72,11 @@ class AppleMusicDownloader:
|
||||
asyncio.create_task(
|
||||
self.song_downloader.get_download_item(
|
||||
media_metadata,
|
||||
(
|
||||
collection_metadata
|
||||
if collection_metadata["type"] in PLAYLIST_MEDIA_TYPE
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
for media_metadata in collection_metadata["relationships"]["tracks"]["data"]
|
||||
@@ -110,7 +115,7 @@ class AppleMusicDownloader:
|
||||
if url_type == "artist":
|
||||
pass
|
||||
|
||||
if url_type == "song":
|
||||
if url_type in SONG_MEDIA_TYPE:
|
||||
song_respose = await self.base_downloader.apple_music_api.get_song(id)
|
||||
|
||||
if song_respose is None:
|
||||
@@ -120,7 +125,7 @@ class AppleMusicDownloader:
|
||||
await self.get_single_download_item(song_respose["data"][0])
|
||||
)
|
||||
|
||||
if url_type in {"album", "albums"}:
|
||||
if url_type in ALBUM_MEDIA_TYPE:
|
||||
if is_library:
|
||||
album_response = (
|
||||
await self.base_downloader.apple_music_api.get_library_album(id)
|
||||
@@ -137,7 +142,7 @@ class AppleMusicDownloader:
|
||||
album_response["data"][0],
|
||||
)
|
||||
|
||||
if url_type == "playlist":
|
||||
if url_type in PLAYLIST_MEDIA_TYPE:
|
||||
if is_library:
|
||||
playlist_response = (
|
||||
await self.base_downloader.apple_music_api.get_library_playlist(id)
|
||||
@@ -154,7 +159,7 @@ class AppleMusicDownloader:
|
||||
playlist_response["data"][0],
|
||||
)
|
||||
|
||||
if url_type == "music-video":
|
||||
if url_type in MUSIC_VIDEO_MEDIA_TYPE:
|
||||
music_video_response = (
|
||||
await self.base_downloader.apple_music_api.get_music_video(id)
|
||||
)
|
||||
@@ -166,7 +171,7 @@ class AppleMusicDownloader:
|
||||
await self.get_single_download_item(music_video_response["data"][0])
|
||||
)
|
||||
|
||||
if url_type == "post":
|
||||
if url_type in UPLOADED_VIDEO_MEDIA_TYPE:
|
||||
uploaded_video = (
|
||||
await self.base_downloader.apple_music_api.get_uploaded_video(id)
|
||||
)
|
||||
@@ -180,7 +185,7 @@ class AppleMusicDownloader:
|
||||
|
||||
return download_items
|
||||
|
||||
async def download(self, download_item: DownloadItem) -> None:
|
||||
async def download(self, download_item: DownloadItem | Exception) -> None:
|
||||
try:
|
||||
if isinstance(download_item, Exception):
|
||||
raise download_item
|
||||
@@ -259,3 +264,10 @@ class AppleMusicDownloader:
|
||||
download_item.lyrics.synced,
|
||||
download_item.synced_lyrics_path,
|
||||
)
|
||||
|
||||
if download_item.playlist_tags and self.base_downloader.save_playlist:
|
||||
self.base_downloader.update_playlist_file(
|
||||
download_item.playlist_file_path,
|
||||
download_item.final_path,
|
||||
download_item.playlist_tags.playlist_track,
|
||||
)
|
||||
|
||||
@@ -37,8 +37,6 @@ class AppleMusicBaseDownloader:
|
||||
overwrite: bool = False,
|
||||
save_cover: bool = False,
|
||||
save_playlist: bool = False,
|
||||
no_synced_lyrics: bool = False,
|
||||
synced_lyrics_only: bool = False,
|
||||
nm3u8dlre_path: str = "N_m3u8DL-RE",
|
||||
mp4decrypt_path: str = "mp4decrypt",
|
||||
ffmpeg_path: str = "ffmpeg",
|
||||
@@ -46,14 +44,14 @@ class AppleMusicBaseDownloader:
|
||||
download_mode: DownloadMode = DownloadMode.YTDLP,
|
||||
remux_mode: RemuxMode = RemuxMode.FFMPEG,
|
||||
cover_format: CoverFormat = CoverFormat.JPG,
|
||||
template_folder_album: str = "{album_artist}/{album}",
|
||||
template_folder_compilation: str = "Compilations/{album}",
|
||||
template_file_single_disc: str = "{track:02d} {title}",
|
||||
template_file_multi_disc: str = "{disc}-{track:02d} {title}",
|
||||
template_folder_no_album: str = "{artist}/Unknown Album",
|
||||
template_file_no_album: str = "{title}",
|
||||
template_file_playlist: str = "Playlists/{playlist_artist}/{playlist_title}",
|
||||
template_date: str = "%Y-%m-%dT%H:%M:%SZ",
|
||||
album_folder_template: str = "{album_artist}/{album}",
|
||||
compilation_folder_template: str = "Compilations/{album}",
|
||||
single_disc_folder_template: str = "{track:02d} {title}",
|
||||
multi_disc_folder_template: str = "{disc}-{track:02d} {title}",
|
||||
no_album_folder_template: str = "{artist}/Unknown Album",
|
||||
no_album_file_template: str = "{title}",
|
||||
playlist_file_template: str = "Playlists/{playlist_artist}/{playlist_title}",
|
||||
date_tag_template: str = "%Y-%m-%dT%H:%M:%SZ",
|
||||
exclude_tags: list[str] = None,
|
||||
cover_size: int = 1200,
|
||||
truncate: int = None,
|
||||
@@ -68,8 +66,6 @@ class AppleMusicBaseDownloader:
|
||||
self.overwrite = overwrite
|
||||
self.save_cover = save_cover
|
||||
self.save_playlist = save_playlist
|
||||
self.no_synced_lyrics = no_synced_lyrics
|
||||
self.synced_lyrics_only = synced_lyrics_only
|
||||
self.nm3u8dlre_path = nm3u8dlre_path
|
||||
self.mp4decrypt_path = mp4decrypt_path
|
||||
self.ffmpeg_path = ffmpeg_path
|
||||
@@ -77,14 +73,14 @@ class AppleMusicBaseDownloader:
|
||||
self.download_mode = download_mode
|
||||
self.remux_mode = remux_mode
|
||||
self.cover_format = cover_format
|
||||
self.template_folder_album = template_folder_album
|
||||
self.template_folder_compilation = template_folder_compilation
|
||||
self.template_file_single_disc = template_file_single_disc
|
||||
self.template_file_multi_disc = template_file_multi_disc
|
||||
self.template_folder_no_album = template_folder_no_album
|
||||
self.template_file_no_album = template_file_no_album
|
||||
self.template_file_playlist = template_file_playlist
|
||||
self.template_date = template_date
|
||||
self.album_folder_template = album_folder_template
|
||||
self.compilation_folder_template = compilation_folder_template
|
||||
self.single_disc_folder_template = single_disc_folder_template
|
||||
self.multi_disc_folder_template = multi_disc_folder_template
|
||||
self.no_album_folder_template = no_album_folder_template
|
||||
self.no_album_file_template = no_album_file_template
|
||||
self.playlist_file_template = playlist_file_template
|
||||
self.date_tag_template = date_tag_template
|
||||
self.exclude_tags = exclude_tags
|
||||
self.cover_size = cover_size
|
||||
self.truncate = truncate
|
||||
@@ -98,7 +94,7 @@ class AppleMusicBaseDownloader:
|
||||
self._setup_interface()
|
||||
|
||||
def _setup_binary_paths(self):
|
||||
self.full_n3u8dlre_path = shutil.which(self.nm3u8dlre_path)
|
||||
self.full_nm3u8dlre_path = shutil.which(self.nm3u8dlre_path)
|
||||
self.full_mp4decrypt_path = shutil.which(self.mp4decrypt_path)
|
||||
self.full_ffmpeg_path = shutil.which(self.ffmpeg_path)
|
||||
self.full_mp4box_path = shutil.which(self.mp4box_path)
|
||||
@@ -208,18 +204,18 @@ class AppleMusicBaseDownloader:
|
||||
) -> str:
|
||||
if tags.album is not None:
|
||||
template_folder = (
|
||||
self.template_folder_compilation.split("/")
|
||||
self.compilation_folder_template.split("/")
|
||||
if tags.compilation
|
||||
else self.template_folder_album.split("/")
|
||||
else self.album_folder_template.split("/")
|
||||
)
|
||||
template_file = (
|
||||
self.template_file_multi_disc.split("/")
|
||||
self.multi_disc_folder_template.split("/")
|
||||
if tags.disc_total > 1
|
||||
else self.template_file_single_disc.split("/")
|
||||
else self.single_disc_folder_template.split("/")
|
||||
)
|
||||
else:
|
||||
template_folder = self.template_folder_no_album.split("/")
|
||||
template_file = self.template_file_no_album.split("/")
|
||||
template_folder = self.no_album_folder_template.split("/")
|
||||
template_file = self.no_album_file_template.split("/")
|
||||
|
||||
template_final = template_folder + template_file
|
||||
|
||||
@@ -304,7 +300,7 @@ class AppleMusicBaseDownloader:
|
||||
|
||||
download_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
||||
await async_subprocess(
|
||||
self.full_n3u8dlre_path,
|
||||
self.full_nm3u8dlre_path,
|
||||
stream_url,
|
||||
"--binary-merge",
|
||||
"--no-log",
|
||||
@@ -336,7 +332,7 @@ class AppleMusicBaseDownloader:
|
||||
if v is not None and k not in exclude_tags
|
||||
}
|
||||
)
|
||||
mp4_tags = filtered_tags.as_mp4_tags(self.template_date)
|
||||
mp4_tags = filtered_tags.as_mp4_tags(self.date_tag_template)
|
||||
skip_tagging = "all" in exclude_tags
|
||||
|
||||
mp4 = MP4(media_path)
|
||||
@@ -382,6 +378,61 @@ class AppleMusicBaseDownloader:
|
||||
Path(cover_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(cover_path).write_bytes(cover_bytes)
|
||||
|
||||
def get_playlist_file_path(
|
||||
self,
|
||||
tags: PlaylistTags,
|
||||
) -> str:
|
||||
template_file = self.playlist_file_template.split("/")
|
||||
tags_dict = tags.__dict__.copy()
|
||||
|
||||
return str(
|
||||
Path(
|
||||
self.output_path,
|
||||
*[
|
||||
self.get_sanitized_string(i.format(**tags_dict), True)
|
||||
for i in template_file[0:-1]
|
||||
],
|
||||
*[
|
||||
self.get_sanitized_string(
|
||||
template_file[-1].format(**tags_dict), False
|
||||
)
|
||||
+ ".m3u8"
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def update_playlist_file(
|
||||
self,
|
||||
playlist_file_path: str,
|
||||
final_path: str,
|
||||
playlist_track: int,
|
||||
) -> None:
|
||||
playlist_file_path_obj = Path(playlist_file_path)
|
||||
final_path_obj = Path(final_path)
|
||||
output_dir_obj = Path(self.output_path)
|
||||
|
||||
playlist_file_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
||||
playlist_file_path_parent_parts_len = len(playlist_file_path_obj.parent.parts)
|
||||
output_path_parts_len = len(output_dir_obj.parts)
|
||||
|
||||
final_path_relative = Path(
|
||||
("../" * (playlist_file_path_parent_parts_len - output_path_parts_len)),
|
||||
*final_path_obj.parts[output_path_parts_len:],
|
||||
)
|
||||
playlist_file_lines = (
|
||||
playlist_file_path_obj.open("r", encoding="utf8").readlines()
|
||||
if playlist_file_path_obj.exists()
|
||||
else []
|
||||
)
|
||||
if len(playlist_file_lines) < playlist_track:
|
||||
playlist_file_lines.extend(
|
||||
"\n" for _ in range(playlist_track - len(playlist_file_lines))
|
||||
)
|
||||
|
||||
playlist_file_lines[playlist_track - 1] = final_path_relative.as_posix() + "\n"
|
||||
with playlist_file_path_obj.open("w", encoding="utf8") as playlist_file:
|
||||
playlist_file.writelines(playlist_file_lines)
|
||||
|
||||
def cleanup_temp(self, random_uuid: str) -> None:
|
||||
temp_folder = Path(self.temp_path) / TEMP_PATH_TEMPLATE.format(random_uuid)
|
||||
if temp_folder.exists():
|
||||
|
||||
@@ -60,6 +60,9 @@ class AppleMusicSongDownloader:
|
||||
playlist_metadata,
|
||||
song_metadata,
|
||||
)
|
||||
download_item.playlist_file_path = self.downloader.get_playlist_file_path(
|
||||
download_item.playlist_tags,
|
||||
)
|
||||
|
||||
if self.codec.is_legacy():
|
||||
download_item.stream_info = (
|
||||
@@ -106,7 +109,7 @@ class AppleMusicSongDownloader:
|
||||
download_item.final_path = self.downloader.get_final_path(
|
||||
download_item.media_tags,
|
||||
".m4a",
|
||||
playlist_metadata,
|
||||
download_item.playlist_tags,
|
||||
)
|
||||
download_item.synced_lyrics_path = self.get_lyrics_synced_path(
|
||||
download_item.final_path,
|
||||
|
||||
@@ -21,6 +21,7 @@ class DownloadItem:
|
||||
cover_url_template: str = None
|
||||
staged_path: str = None
|
||||
final_path: str = None
|
||||
playlist_file_path: str = None
|
||||
synced_lyrics_path: str = None
|
||||
cover_path: str = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user