mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 12:15:18 +03:00
@@ -54,14 +54,14 @@ gamdl can be configured using the command line arguments or the config file. The
|
||||
| `--template-file-music-video` / `template_file_music_video` | Template of the music video files as a format string. | `{title}` |
|
||||
| `--cover-size` / `cover_size` | Size of the cover. | `1200` |
|
||||
| `--cover-format` / `cover_format` | Format of the cover. | `jpg` |
|
||||
| `--remux-mode` / `remux_mode` | Remuxing mode. | `ffmpeg` |
|
||||
| `--download-mode` / `download_mode` | Download mode. | `yt-dlp` |
|
||||
| `--remux-mode` / `remux_mode` | Remux mode. | `ffmpeg` |
|
||||
| `--download-mode` / `download_mode` | Download mode. | `ytdlp` |
|
||||
| `-e`, `--exclude-tags` / `exclude_tags` | List of tags to exclude from file tagging separated by commas. | `null` |
|
||||
| `--truncate` / `truncate` | Maximum length of the file/folder names. | `40` |
|
||||
| `-l`, `--log-level` / `log_level` | Log level. | `INFO` |
|
||||
| `--prefer-hevc` / `prefer_hevc` | Prefer HEVC over AVC when downloading music videos. | `false` |
|
||||
| `--ask-video-format` / `ask_video_format` | Ask for the video format when downloading music videos. | `false` |
|
||||
| `--disable-music-video-album-skip` / `disable_music_video_album_skip` | Don't skip downloading music videos in albums. | `false` |
|
||||
| `--disable-music-video-skip` / `disable_music_video_skip` | Don't skip downloading music videos in albums/playlists. | `false` |
|
||||
| `-l`, `--lrc-only` / `lrc_only` | Download only the synced lyrics. | `false` |
|
||||
| `-n`, `--no-lrc` / `no_lrc` | Don't download the synced lyrics. | `false` |
|
||||
| `-s`, `--save-cover` / `save_cover` | Save cover as a separate file. | `false` |
|
||||
@@ -87,6 +87,7 @@ The following variables can be used in the template folders/files and/or in the
|
||||
* `composer_sort`
|
||||
* `copyright`
|
||||
* `cover`
|
||||
* `date`
|
||||
* `disc`
|
||||
* `disc_total`
|
||||
* `gapless`
|
||||
@@ -95,7 +96,6 @@ The following variables can be used in the template folders/files and/or in the
|
||||
* `lyrics`
|
||||
* `media_type`
|
||||
* `rating`
|
||||
* `release_date`
|
||||
* `storefront`
|
||||
* `title`
|
||||
* `title_id`
|
||||
@@ -105,10 +105,10 @@ The following variables can be used in the template folders/files and/or in the
|
||||
* `xid`
|
||||
|
||||
### Remux mode
|
||||
Can be either `ffmpeg` or `mp4box`. `mp4decrypt` is required for music videos and remuxing with `mp4box`. `mp4box` is slower but will keep the closed captions track in music videos that have one. `mp4box` can be obtained from [here](https://gpac.wp.imt.fr/downloads).
|
||||
Can be either `ffmpeg` or `mp4box`. `mp4decrypt` is required for music videos and remuxing with `mp4box`. `mp4box` is slower but will not convert the closed captions track in music videos that have one. `mp4box` can be obtained from [here](https://gpac.wp.imt.fr/downloads).
|
||||
|
||||
### Download mode
|
||||
Can be either `yt-dlp` or `nm3u8dlre`. `nm3u8dlre` is faster but requires `ffmpeg`. `nm3u8dlre` can be obtained from [here](https://github.com/nilaoda/N_m3u8DL-RE/releases).
|
||||
Can be either `ytdlp` or `nm3u8dlre`. `nm3u8dlre` is faster but requires `ffmpeg`. `nm3u8dlre` can be obtained from [here](https://github.com/nilaoda/N_m3u8DL-RE/releases).
|
||||
|
||||
## Songs quality
|
||||
Songs will be downloaded in AAC 256kbps by default or in HE-AAC 64kbps if the `songs_heaac` option is enabled.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.9.5"
|
||||
__version__ = "1.9.6"
|
||||
|
||||
+207
-168
@@ -5,26 +5,16 @@ from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from .dl import Dl
|
||||
|
||||
EXCLUDED_PARAMS = (
|
||||
"urls",
|
||||
"config_location",
|
||||
"url_txt",
|
||||
"no_config_file",
|
||||
"version",
|
||||
"help",
|
||||
)
|
||||
|
||||
X_NOT_FOUND_STRING = '{} not found at "{}"'
|
||||
from .constants import *
|
||||
from .downloader import Downloader
|
||||
|
||||
|
||||
def write_default_config_file(ctx: click.Context):
|
||||
def write_default_config_file(ctx: click.Context) -> None:
|
||||
ctx.params["config_location"].parent.mkdir(parents=True, exist_ok=True)
|
||||
config_file = {
|
||||
param.name: param.default
|
||||
for param in ctx.command.params
|
||||
if param.name not in EXCLUDED_PARAMS
|
||||
if param.name not in EXCLUDED_CONFIG_FILE_PARAMS
|
||||
}
|
||||
with open(ctx.params["config_location"], "w") as f:
|
||||
f.write(json.dumps(config_file, indent=4))
|
||||
@@ -32,7 +22,7 @@ def write_default_config_file(ctx: click.Context):
|
||||
|
||||
def no_config_callback(
|
||||
ctx: click.Context, param: click.Parameter, no_config_file: bool
|
||||
):
|
||||
) -> click.Context:
|
||||
if no_config_file:
|
||||
return ctx
|
||||
if not ctx.params["config_location"].exists():
|
||||
@@ -166,12 +156,12 @@ def no_config_callback(
|
||||
"--remux-mode",
|
||||
type=click.Choice(["ffmpeg", "mp4box"]),
|
||||
default="ffmpeg",
|
||||
help="Remuxing mode.",
|
||||
help="Remux mode.",
|
||||
)
|
||||
@click.option(
|
||||
"--download-mode",
|
||||
type=click.Choice(["nm3u8dlre", "yt-dlp"]),
|
||||
default="yt-dlp",
|
||||
type=click.Choice(["ytdlp", "nm3u8dlre"]),
|
||||
default="ytdlp",
|
||||
help="Download mode.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -205,9 +195,9 @@ def no_config_callback(
|
||||
help="Ask for the video format when downloading music videos.",
|
||||
)
|
||||
@click.option(
|
||||
"--disable-music-video-album-skip",
|
||||
"--disable-music-video-skip",
|
||||
is_flag=True,
|
||||
help="Don't skip downloading music videos in albums.",
|
||||
help="Don't skip downloading music videos in albums/playlists.",
|
||||
)
|
||||
@click.option(
|
||||
"--lrc-only",
|
||||
@@ -284,7 +274,7 @@ def main(
|
||||
log_level: str,
|
||||
prefer_hevc: bool,
|
||||
ask_video_format: bool,
|
||||
disable_music_video_album_skip: bool,
|
||||
disable_music_video_skip: bool,
|
||||
lrc_only: bool,
|
||||
no_lrc: bool,
|
||||
save_cover: bool,
|
||||
@@ -300,12 +290,43 @@ def main(
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(log_level)
|
||||
if not wvd_location.exists() and not lrc_only:
|
||||
logger.critical(X_NOT_FOUND_STRING.format(".wvd file", wvd_location))
|
||||
return
|
||||
logger.debug("Starting downloader")
|
||||
downloader = Downloader(**locals())
|
||||
if not cookies_location.exists():
|
||||
logger.critical(X_NOT_FOUND_STRING.format("Cookies file", cookies_location))
|
||||
logger.critical(X_NOT_FOUND_STR.format("Cookies file", cookies_location))
|
||||
return
|
||||
if not wvd_location.exists() and not lrc_only:
|
||||
logger.critical(X_NOT_FOUND_STR.format(".wvd file", wvd_location))
|
||||
return
|
||||
if remux_mode == "ffmpeg" and not lrc_only:
|
||||
if not downloader.ffmpeg_location:
|
||||
logger.critical(X_NOT_FOUND_STR.format("FFmpeg", ffmpeg_location))
|
||||
return
|
||||
if not downloader.mp4decrypt_location:
|
||||
logger.warning(
|
||||
X_NOT_FOUND_STR.format("mp4decrypt", mp4decrypt_location)
|
||||
+ ", music videos videos will not be downloaded"
|
||||
)
|
||||
if remux_mode == "mp4box" and not lrc_only:
|
||||
if not downloader.mp4box_location:
|
||||
logger.critical(X_NOT_FOUND_STR.format("MP4Box", mp4box_location))
|
||||
return
|
||||
if not downloader.mp4decrypt_location:
|
||||
logger.critical(X_NOT_FOUND_STR.format("mp4decrypt", mp4decrypt_location))
|
||||
return
|
||||
if download_mode == "nm3u8dlre" and not lrc_only:
|
||||
if not downloader.nm3u8dlre_location:
|
||||
logger.critical(X_NOT_FOUND_STR.format("N_m3u8DL-RE", nm3u8dlre_location))
|
||||
return
|
||||
if not downloader.ffmpeg_location:
|
||||
logger.critical(X_NOT_FOUND_STR.format("FFmpeg", ffmpeg_location))
|
||||
return
|
||||
logger.debug("Setting up session")
|
||||
downloader.setup_session()
|
||||
logger.debug("Setting up CDM")
|
||||
downloader.setup_cdm()
|
||||
error_count = 0
|
||||
download_queue = []
|
||||
if url_txt:
|
||||
logger.debug("Reading URLs from text files")
|
||||
_urls = []
|
||||
@@ -313,163 +334,185 @@ def main(
|
||||
with open(url, "r") as f:
|
||||
_urls.extend(f.read().splitlines())
|
||||
urls = tuple(_urls)
|
||||
logger.debug("Starting downloader")
|
||||
dl = Dl(**locals())
|
||||
if remux_mode == "ffmpeg" and not lrc_only:
|
||||
if not dl.ffmpeg_location:
|
||||
logger.critical(X_NOT_FOUND_STRING.format("FFmpeg", ffmpeg_location))
|
||||
return
|
||||
if not dl.mp4decrypt_location:
|
||||
logger.warning(
|
||||
X_NOT_FOUND_STRING.format("mp4decrypt", mp4decrypt_location)
|
||||
+ ", music videos videos will not be downloaded"
|
||||
)
|
||||
if remux_mode == "mp4box" and not lrc_only:
|
||||
if not dl.mp4box_location:
|
||||
logger.critical(X_NOT_FOUND_STRING.format("MP4Box", mp4box_location))
|
||||
return
|
||||
if not dl.mp4decrypt_location:
|
||||
logger.critical(
|
||||
X_NOT_FOUND_STRING.format("mp4decrypt", mp4decrypt_location)
|
||||
)
|
||||
return
|
||||
if download_mode == "nm3u8dlre" and not lrc_only:
|
||||
if not dl.nm3u8dlre_location:
|
||||
logger.critical(
|
||||
X_NOT_FOUND_STRING.format("N_m3u8DL-RE", nm3u8dlre_location)
|
||||
)
|
||||
return
|
||||
if not dl.ffmpeg_location:
|
||||
logger.critical(X_NOT_FOUND_STRING.format("FFmpeg", ffmpeg_location))
|
||||
return
|
||||
if not dl.session.cookies.get_dict().get("media-user-token"):
|
||||
logger.critical("Invalid cookies file")
|
||||
return
|
||||
download_queue = []
|
||||
for i, url in enumerate(urls, start=1):
|
||||
for url_index, url in enumerate(urls, start=1):
|
||||
try:
|
||||
logger.debug(f'Checking "{url}" (URL {i}/{len(urls)})')
|
||||
download_queue.append(dl.get_download_queue(url))
|
||||
logger.debug(f'Checking "{url}" (URL {url_index}/{len(urls)})')
|
||||
download_queue.append(downloader.get_download_queue(url))
|
||||
except Exception:
|
||||
logger.error(
|
||||
f"Failed to check URL {i}/{len(urls)}", exc_info=print_exceptions
|
||||
f'Failed to check "{url}" (URL {url_index}/{len(urls)})',
|
||||
exc_info=print_exceptions,
|
||||
)
|
||||
error_count = 0
|
||||
for i, url in enumerate(download_queue, start=1):
|
||||
for j, track in enumerate(url, start=1):
|
||||
if track["type"] == "music-videos" and (not dl.mp4decrypt_location or lrc_only):
|
||||
continue
|
||||
error_count += 1
|
||||
for queue_item_index, queue_item in enumerate(download_queue, start=1):
|
||||
download_type, tracks = queue_item
|
||||
for track_index, track in enumerate(tracks, start=1):
|
||||
logger.info(
|
||||
f'Downloading "{track["attributes"]["name"]}" (track {j}/{len(url)} from URL {i}/{len(download_queue)})'
|
||||
f'Downloading "{track["attributes"]["name"]}" (track {track_index}/{len(tracks)} '
|
||||
f"from URL {queue_item_index}/{len(download_queue)})"
|
||||
)
|
||||
try:
|
||||
if not track["attributes"].get("playParams"):
|
||||
logger.warning("Track is not streamable, skipping")
|
||||
continue
|
||||
track_id = track["id"]
|
||||
webplayback = dl.get_webplayback(track_id)
|
||||
logger.debug("Getting webplayback")
|
||||
webplayback = downloader.get_webplayback(track_id)
|
||||
cover_url = downloader.get_cover_url(webplayback)
|
||||
if track["type"] == "songs":
|
||||
unsynced_lyrics, synced_lyrics = dl.get_lyrics(track_id)
|
||||
tags = dl.get_tags_song(webplayback, unsynced_lyrics)
|
||||
final_location = dl.get_final_location(tags)
|
||||
logger.debug("Getting lyrics")
|
||||
lyrics_unsynced, lyrics_synced = downloader.get_lyrics(track_id)
|
||||
logger.debug("Getting tags")
|
||||
tags = downloader.get_tags_song(webplayback, lyrics_unsynced)
|
||||
final_location = downloader.get_final_location(tags)
|
||||
cover_location = downloader.get_cover_location_song(final_location)
|
||||
lrc_location = downloader.get_lrc_location(final_location)
|
||||
logger.debug(f'Final location is "{final_location}"')
|
||||
if not lrc_only:
|
||||
if not final_location.exists() or overwrite:
|
||||
logger.debug("Getting stream URL")
|
||||
stream_url = dl.get_stream_url_song(webplayback)
|
||||
logger.debug("Getting decryption key")
|
||||
decryption_key = dl.get_decryption_key_song(
|
||||
stream_url, track_id
|
||||
if lrc_only:
|
||||
pass
|
||||
elif final_location.exists() and not overwrite:
|
||||
logger.warning(
|
||||
f"File already exists at {final_location}, skipping"
|
||||
)
|
||||
else:
|
||||
logger.debug("Getting stream URL")
|
||||
stream_url = downloader.get_stream_url_song(webplayback)
|
||||
logger.debug("Getting decryption key")
|
||||
decryption_key = downloader.get_decryption_key_song(
|
||||
stream_url, track_id
|
||||
)
|
||||
encrypted_location = downloader.get_encrypted_location_audio(
|
||||
track_id
|
||||
)
|
||||
logger.debug(f'Downloading to "{encrypted_location}"')
|
||||
if download_mode == "ytdlp":
|
||||
downloader.download_ytdlp(encrypted_location, stream_url)
|
||||
if download_mode == "nm3u8dlre":
|
||||
downloader.download_nm3u8dlre(
|
||||
encrypted_location, stream_url
|
||||
)
|
||||
encrypted_location = dl.get_encrypted_location_audio(
|
||||
track_id
|
||||
decrypted_location = downloader.get_decrypted_location_audio(
|
||||
track_id
|
||||
)
|
||||
fixed_location = downloader.get_fixed_location(track_id, ".m4a")
|
||||
if remux_mode == "ffmpeg":
|
||||
logger.debug(
|
||||
f'Decrypting and remuxing to "{fixed_location}"'
|
||||
)
|
||||
logger.debug(f'Downloading to "{encrypted_location}"')
|
||||
dl.download(encrypted_location, stream_url)
|
||||
decrypted_location = dl.get_decrypted_location_audio(
|
||||
track_id
|
||||
downloader.fixup_song_ffmpeg(
|
||||
encrypted_location, decryption_key, fixed_location
|
||||
)
|
||||
fixed_location = dl.get_fixed_location(track_id, ".m4a")
|
||||
if remux_mode == "ffmpeg":
|
||||
logger.debug(
|
||||
f'Decrypting and remuxing to "{fixed_location}"'
|
||||
)
|
||||
dl.fixup_song_ffmpeg(
|
||||
encrypted_location, decryption_key, fixed_location
|
||||
)
|
||||
if remux_mode == "mp4box":
|
||||
logger.debug(f'Decrypting to "{decrypted_location}"')
|
||||
dl.decrypt(
|
||||
encrypted_location,
|
||||
decrypted_location,
|
||||
decryption_key,
|
||||
)
|
||||
logger.debug(f'Remuxing to "{fixed_location}"')
|
||||
dl.fixup_song_mp4box(decrypted_location, fixed_location)
|
||||
logger.debug("Applying tags")
|
||||
dl.apply_tags(fixed_location, tags)
|
||||
logger.debug("Moving to final location")
|
||||
dl.move_to_final_location(fixed_location, final_location)
|
||||
else:
|
||||
logger.warning(
|
||||
f"File already exists at {final_location}, skipping"
|
||||
if remux_mode == "mp4box":
|
||||
logger.debug(f'Decrypting to "{decrypted_location}"')
|
||||
downloader.decrypt(
|
||||
encrypted_location,
|
||||
decrypted_location,
|
||||
decryption_key,
|
||||
)
|
||||
if save_cover:
|
||||
cover_location = dl.get_cover_location_song(final_location)
|
||||
if not cover_location.exists() or overwrite:
|
||||
logger.debug(f'Saving cover to "{cover_location}"')
|
||||
dl.save_cover(tags, cover_location)
|
||||
else:
|
||||
logger.debug(
|
||||
f'File already exists at "{cover_location}", skipping'
|
||||
)
|
||||
if not no_lrc and synced_lyrics:
|
||||
lrc_location = dl.get_lrc_location(final_location)
|
||||
if overwrite or not lrc_location.exists():
|
||||
logger.debug(f'Saving synced lyrics to "{lrc_location}"')
|
||||
dl.make_lrc(lrc_location, synced_lyrics)
|
||||
else:
|
||||
logger.warning(f'"{lrc_location}" already exists, skipping')
|
||||
logger.debug(f'Remuxing to "{fixed_location}"')
|
||||
downloader.fixup_song_mp4box(
|
||||
decrypted_location, fixed_location
|
||||
)
|
||||
logger.debug("Applying tags")
|
||||
downloader.apply_tags(fixed_location, tags, cover_url)
|
||||
logger.debug("Moving to final location")
|
||||
downloader.move_to_final_location(
|
||||
fixed_location, final_location
|
||||
)
|
||||
if no_lrc or not lyrics_synced:
|
||||
pass
|
||||
elif lrc_location.exists() and not overwrite:
|
||||
logger.warning(f'"{lrc_location}" already exists, skipping')
|
||||
else:
|
||||
logger.debug(f'Saving synced lyrics to "{lrc_location}"')
|
||||
downloader.make_lrc(lrc_location, lyrics_synced)
|
||||
if not save_cover or lrc_only:
|
||||
pass
|
||||
elif cover_location.exists() and not overwrite:
|
||||
logger.debug(
|
||||
f'File already exists at "{cover_location}", skipping'
|
||||
)
|
||||
else:
|
||||
logger.debug(f'Saving cover to "{cover_location}"')
|
||||
downloader.save_cover(cover_location, cover_url)
|
||||
if track["type"] == "music-videos":
|
||||
tags = dl.get_tags_music_video(
|
||||
if (
|
||||
not disable_music_video_skip
|
||||
and download_type in ("albums", "playlists")
|
||||
or lrc_only
|
||||
or not downloader.mp4decrypt_location
|
||||
):
|
||||
logger.warning(
|
||||
"Music video is not downloadable with current settings, skipping"
|
||||
)
|
||||
continue
|
||||
tags = downloader.get_tags_music_video(
|
||||
track["attributes"]["url"].split("/")[-1].split("?")[0]
|
||||
)
|
||||
final_location = dl.get_final_location(tags)
|
||||
final_location = downloader.get_final_location(tags)
|
||||
cover_location = downloader.get_cover_location_music_video(
|
||||
final_location
|
||||
)
|
||||
logger.debug(f'Final location is "{final_location}"')
|
||||
if not final_location.exists() or overwrite:
|
||||
if final_location.exists() and not overwrite:
|
||||
logger.warning(
|
||||
f'File already exists at "{final_location}", skipping'
|
||||
)
|
||||
else:
|
||||
logger.debug("Getting stream URLs")
|
||||
(
|
||||
stream_url_video,
|
||||
stream_url_audio,
|
||||
) = dl.get_stream_url_music_video(webplayback)
|
||||
) = downloader.get_stream_url_music_video(webplayback)
|
||||
logger.debug("Getting decryption keys")
|
||||
decryption_key_video = dl.get_decryption_key_music_video(
|
||||
stream_url_video, track_id
|
||||
decryption_key_video = (
|
||||
downloader.get_decryption_key_music_video(
|
||||
stream_url_video, track_id
|
||||
)
|
||||
)
|
||||
decryption_key_audio = dl.get_decryption_key_music_video(
|
||||
stream_url_audio, track_id
|
||||
decryption_key_audio = (
|
||||
downloader.get_decryption_key_music_video(
|
||||
stream_url_audio, track_id
|
||||
)
|
||||
)
|
||||
encrypted_location_video = dl.get_encrypted_location_video(
|
||||
track_id
|
||||
encrypted_location_video = (
|
||||
downloader.get_encrypted_location_video(track_id)
|
||||
)
|
||||
decrypted_location_video = dl.get_decrypted_location_video(
|
||||
track_id
|
||||
encrypted_location_audio = (
|
||||
downloader.get_encrypted_location_audio(track_id)
|
||||
)
|
||||
decrypted_location_video = (
|
||||
downloader.get_decrypted_location_video(track_id)
|
||||
)
|
||||
decrypted_location_audio = (
|
||||
downloader.get_decrypted_location_audio(track_id)
|
||||
)
|
||||
logger.debug(
|
||||
f'Downloading video to "{encrypted_location_video}"'
|
||||
)
|
||||
dl.download(encrypted_location_video, stream_url_video)
|
||||
encrypted_location_audio = dl.get_encrypted_location_audio(
|
||||
track_id
|
||||
)
|
||||
decrypted_location_audio = dl.get_decrypted_location_audio(
|
||||
track_id
|
||||
)
|
||||
if download_mode == "ytdlp":
|
||||
downloader.download_ytdlp(
|
||||
encrypted_location_video, stream_url_video
|
||||
)
|
||||
if download_mode == "nm3u8dlre":
|
||||
downloader.download_nm3u8dlre(
|
||||
encrypted_location_video, stream_url_video
|
||||
)
|
||||
logger.debug(
|
||||
f'Downloading audio to "{encrypted_location_audio}"'
|
||||
)
|
||||
dl.download(encrypted_location_audio, stream_url_audio)
|
||||
if download_mode == "ytdlp":
|
||||
downloader.download_ytdlp(
|
||||
encrypted_location_audio, stream_url_audio
|
||||
)
|
||||
if download_mode == "nm3u8dlre":
|
||||
downloader.download_nm3u8dlre(
|
||||
encrypted_location_audio, stream_url_audio
|
||||
)
|
||||
logger.debug(
|
||||
f'Decrypting video to "{decrypted_location_video}"'
|
||||
)
|
||||
dl.decrypt(
|
||||
downloader.decrypt(
|
||||
encrypted_location_audio,
|
||||
decrypted_location_audio,
|
||||
decryption_key_audio,
|
||||
@@ -477,49 +520,45 @@ def main(
|
||||
logger.debug(
|
||||
f'Decrypting audio to "{decrypted_location_audio}"'
|
||||
)
|
||||
dl.decrypt(
|
||||
downloader.decrypt(
|
||||
encrypted_location_video,
|
||||
decrypted_location_video,
|
||||
decryption_key_video,
|
||||
)
|
||||
fixed_location = dl.get_fixed_location(track_id, ".m4v")
|
||||
fixed_location = downloader.get_fixed_location(track_id, ".m4v")
|
||||
logger.debug(f'Remuxing to "{fixed_location}"')
|
||||
if remux_mode == "ffmpeg":
|
||||
dl.fixup_music_video_ffmpeg(
|
||||
downloader.fixup_music_video_ffmpeg(
|
||||
decrypted_location_video,
|
||||
decrypted_location_audio,
|
||||
fixed_location,
|
||||
)
|
||||
if remux_mode == "mp4box":
|
||||
dl.fixup_music_video_mp4box(
|
||||
downloader.fixup_music_video_mp4box(
|
||||
decrypted_location_audio,
|
||||
decrypted_location_video,
|
||||
fixed_location,
|
||||
)
|
||||
logger.debug("Applying tags")
|
||||
dl.apply_tags(fixed_location, tags)
|
||||
downloader.apply_tags(fixed_location, tags, cover_url)
|
||||
logger.debug("Moving to final location")
|
||||
dl.move_to_final_location(fixed_location, final_location)
|
||||
downloader.move_to_final_location(
|
||||
fixed_location, final_location
|
||||
)
|
||||
if not save_cover:
|
||||
pass
|
||||
elif cover_location.exists() and not overwrite:
|
||||
logger.debug(
|
||||
f'File already exists at "{cover_location}", skipping'
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f'File already exists at "{final_location}", skipping'
|
||||
)
|
||||
if save_cover:
|
||||
cover_location = dl.get_cover_location_music_video(
|
||||
final_location
|
||||
)
|
||||
if not cover_location.exists() or overwrite:
|
||||
logger.debug(f'Saving cover to "{cover_location}"')
|
||||
dl.save_cover(tags, cover_location)
|
||||
else:
|
||||
logger.debug(
|
||||
f'File already exists at "{cover_location}", skipping'
|
||||
)
|
||||
logger.debug(f'Saving cover to "{cover_location}"')
|
||||
downloader.save_cover(cover_location, cover_url)
|
||||
except Exception:
|
||||
error_count += 1
|
||||
logger.error(
|
||||
f'Failed to download "{track["attributes"]["name"]}" (track {j}/{len(url)} from URL '
|
||||
+ f"{i}/{len(download_queue)})",
|
||||
f'Failed to download "{track["attributes"]["name"]}" (track {track_index}/{len(tracks)} '
|
||||
f"from URL {queue_item_index}/{len(download_queue)})",
|
||||
exc_info=print_exceptions,
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
STOREFRONT_IDS = {
|
||||
"AE": "143481-2,32",
|
||||
"AG": "143540-2,32",
|
||||
"AI": "143538-2,32",
|
||||
"AL": "143575-2,32",
|
||||
"AM": "143524-2,32",
|
||||
"AO": "143564-2,32",
|
||||
"AR": "143505-28,32",
|
||||
"AT": "143445-4,32",
|
||||
"AU": "143460-27,32",
|
||||
"AZ": "143568-2,32",
|
||||
"BB": "143541-2,32",
|
||||
"BE": "143446-2,32",
|
||||
"BF": "143578-2,32",
|
||||
"BG": "143526-2,32",
|
||||
"BH": "143559-2,32",
|
||||
"BJ": "143576-2,32",
|
||||
"BM": "143542-2,32",
|
||||
"BN": "143560-2,32",
|
||||
"BO": "143556-28,32",
|
||||
"BR": "143503-15,32",
|
||||
"BS": "143539-2,32",
|
||||
"BT": "143577-2,32",
|
||||
"BW": "143525-2,32",
|
||||
"BY": "143565-2,32",
|
||||
"BZ": "143555-2,32",
|
||||
"CA": "143455-6,32",
|
||||
"CG": "143582-2,32",
|
||||
"CH": "143459-57,32",
|
||||
"CL": "143483-28,32",
|
||||
"CN": "143465-19,32",
|
||||
"CO": "143501-28,32",
|
||||
"CR": "143495-28,32",
|
||||
"CV": "143580-2,32",
|
||||
"CY": "143557-2,32",
|
||||
"CZ": "143489-2,32",
|
||||
"DE": "143443-4,32",
|
||||
"DK": "143458-2,32",
|
||||
"DM": "143545-2,32",
|
||||
"DO": "143508-28,32",
|
||||
"DZ": "143563-2,32",
|
||||
"EC": "143509-28,32",
|
||||
"EE": "143518-2,32",
|
||||
"EG": "143516-2,32",
|
||||
"ES": "143454-8,32",
|
||||
"FI": "143447-2,32",
|
||||
"FJ": "143583-2,32",
|
||||
"FM": "143591-2,32",
|
||||
"FR": "143442-3,32",
|
||||
"GB": "143444-2,32",
|
||||
"GD": "143546-2,32",
|
||||
"GH": "143573-2,32",
|
||||
"GM": "143584-2,32",
|
||||
"GR": "143448-2,32",
|
||||
"GT": "143504-28,32",
|
||||
"GW": "143585-2,32",
|
||||
"GY": "143553-2,32",
|
||||
"HK": "143463-45,32",
|
||||
"HN": "143510-28,32",
|
||||
"HR": "143494-2,32",
|
||||
"HU": "143482-2,32",
|
||||
"ID": "143476-2,32",
|
||||
"IE": "143449-2,32",
|
||||
"IL": "143491-2,32",
|
||||
"IN": "143467-2,32",
|
||||
"IS": "143558-2,32",
|
||||
"IT": "143450-7,32",
|
||||
"JM": "143511-2,32",
|
||||
"JO": "143528-2,32",
|
||||
"JP": "143462-9,32",
|
||||
"KE": "143529-2,32",
|
||||
"KG": "143586-2,32",
|
||||
"KH": "143579-2,32",
|
||||
"KN": "143548-2,32",
|
||||
"KR": "143466-13,32",
|
||||
"KW": "143493-2,32",
|
||||
"KY": "143544-2,32",
|
||||
"KZ": "143517-2,32",
|
||||
"LA": "143587-2,32",
|
||||
"LB": "143497-2,32",
|
||||
"LC": "143549-2,32",
|
||||
"LK": "143486-2,32",
|
||||
"LR": "143588-2,32",
|
||||
"LT": "143520-2,32",
|
||||
"LU": "143451-2,32",
|
||||
"LV": "143519-2,32",
|
||||
"MD": "143523-2,32",
|
||||
"MG": "143531-2,32",
|
||||
"MK": "143530-2,32",
|
||||
"ML": "143532-2,32",
|
||||
"MN": "143592-2,32",
|
||||
"MO": "143515-45,32",
|
||||
"MR": "143590-2,32",
|
||||
"MS": "143547-2,32",
|
||||
"MT": "143521-2,32",
|
||||
"MU": "143533-2,32",
|
||||
"MW": "143589-2,32",
|
||||
"MX": "143468-28,32",
|
||||
"MY": "143473-2,32",
|
||||
"MZ": "143593-2,32",
|
||||
"NA": "143594-2,32",
|
||||
"NE": "143534-2,32",
|
||||
"NG": "143561-2,32",
|
||||
"NI": "143512-28,32",
|
||||
"NL": "143452-10,32",
|
||||
"NO": "143457-2,32",
|
||||
"NP": "143484-2,32",
|
||||
"NZ": "143461-27,32",
|
||||
"OM": "143562-2,32",
|
||||
"PA": "143485-28,32",
|
||||
"PE": "143507-28,32",
|
||||
"PG": "143597-2,32",
|
||||
"PH": "143474-2,32",
|
||||
"PK": "143477-2,32",
|
||||
"PL": "143478-2,32",
|
||||
"PT": "143453-24,32",
|
||||
"PW": "143595-2,32",
|
||||
"PY": "143513-28,32",
|
||||
"QA": "143498-2,32",
|
||||
"RO": "143487-2,32",
|
||||
"RU": "143469-16,32",
|
||||
"SA": "143479-2,32",
|
||||
"SB": "143601-2,32",
|
||||
"SC": "143599-2,32",
|
||||
"SE": "143456-17,32",
|
||||
"SG": "143464-19,32",
|
||||
"SI": "143499-2,32",
|
||||
"SK": "143496-2,32",
|
||||
"SL": "143600-2,32",
|
||||
"SN": "143535-2,32",
|
||||
"SR": "143554-2,32",
|
||||
"ST": "143598-2,32",
|
||||
"SV": "143506-28,32",
|
||||
"SZ": "143602-2,32",
|
||||
"TC": "143552-2,32",
|
||||
"TD": "143581-2,32",
|
||||
"TH": "143475-2,32",
|
||||
"TJ": "143603-2,32",
|
||||
"TM": "143604-2,32",
|
||||
"TN": "143536-2,32",
|
||||
"TR": "143480-2,32",
|
||||
"TT": "143551-2,32",
|
||||
"TW": "143470-18,32",
|
||||
"TZ": "143572-2,32",
|
||||
"UA": "143492-2,32",
|
||||
"UG": "143537-2,32",
|
||||
"US": "143441-1,32",
|
||||
"UY": "143514-2,32",
|
||||
"UZ": "143566-2,32",
|
||||
"VC": "143550-2,32",
|
||||
"VE": "143502-28,32",
|
||||
"VG": "143543-2,32",
|
||||
"VN": "143471-2,32",
|
||||
"YE": "143571-2,32",
|
||||
"ZA": "143472-2,32",
|
||||
"ZW": "143605-2,32",
|
||||
}
|
||||
|
||||
MP4_TAGS_MAP = {
|
||||
"album": "\xa9alb",
|
||||
"album_artist": "aART",
|
||||
"album_id": "plID",
|
||||
"album_sort": "soal",
|
||||
"artist": "\xa9ART",
|
||||
"artist_id": "atID",
|
||||
"artist_sort": "soar",
|
||||
"comment": "\xa9cmt",
|
||||
"composer": "\xa9wrt",
|
||||
"composer_id": "cmID",
|
||||
"composer_sort": "soco",
|
||||
"copyright": "cprt",
|
||||
"date": "\xa9day",
|
||||
"genre": "\xa9gen",
|
||||
"genre_id": "geID",
|
||||
"lyrics": "\xa9lyr",
|
||||
"media_type": "stik",
|
||||
"rating": "rtng",
|
||||
"storefront": "sfID",
|
||||
"title": "\xa9nam",
|
||||
"title_id": "cnID",
|
||||
"title_sort": "sonm",
|
||||
"xid": "xid ",
|
||||
}
|
||||
|
||||
EXCLUDED_CONFIG_FILE_PARAMS = (
|
||||
"urls",
|
||||
"config_location",
|
||||
"url_txt",
|
||||
"no_config_file",
|
||||
"version",
|
||||
"help",
|
||||
)
|
||||
|
||||
X_NOT_FOUND_STR = '{} not found at "{}"'
|
||||
+127
-152
@@ -14,36 +14,10 @@ from mutagen.mp4 import MP4, MP4Cover
|
||||
from pywidevine import PSSH, Cdm, Device, WidevinePsshData
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
import gamdl.storefronts
|
||||
|
||||
MP4_TAGS_MAP = {
|
||||
"album": "\xa9alb",
|
||||
"album_artist": "aART",
|
||||
"album_id": "plID",
|
||||
"album_sort": "soal",
|
||||
"artist": "\xa9ART",
|
||||
"artist_id": "atID",
|
||||
"artist_sort": "soar",
|
||||
"comment": "\xa9cmt",
|
||||
"composer": "\xa9wrt",
|
||||
"composer_id": "cmID",
|
||||
"composer_sort": "soco",
|
||||
"copyright": "cprt",
|
||||
"genre": "\xa9gen",
|
||||
"genre_id": "geID",
|
||||
"lyrics": "\xa9lyr",
|
||||
"media_type": "stik",
|
||||
"rating": "rtng",
|
||||
"release_date": "\xa9day",
|
||||
"storefront": "sfID",
|
||||
"title": "\xa9nam",
|
||||
"title_id": "cnID",
|
||||
"title_sort": "sonm",
|
||||
"xid": "xid ",
|
||||
}
|
||||
from gamdl.constants import MP4_TAGS_MAP, STOREFRONT_IDS
|
||||
|
||||
|
||||
class Dl:
|
||||
class Downloader:
|
||||
def __init__(
|
||||
self,
|
||||
final_path: Path = None,
|
||||
@@ -62,23 +36,29 @@ class Dl:
|
||||
template_file_music_video: str = None,
|
||||
cover_size: int = None,
|
||||
cover_format: str = None,
|
||||
remux_mode: str = None,
|
||||
download_mode: str = None,
|
||||
exclude_tags: str = None,
|
||||
truncate: int = None,
|
||||
prefer_hevc: bool = None,
|
||||
ask_video_format: bool = None,
|
||||
disable_music_video_album_skip: bool = None,
|
||||
lrc_only: bool = None,
|
||||
songs_heaac: bool = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.final_path = final_path
|
||||
self.temp_path = temp_path
|
||||
self.ffmpeg_location = shutil.which(ffmpeg_location)
|
||||
self.mp4box_location = shutil.which(mp4box_location)
|
||||
self.mp4decrypt_location = shutil.which(mp4decrypt_location)
|
||||
self.nm3u8dlre_location = shutil.which(nm3u8dlre_location)
|
||||
self.cookies_location = cookies_location
|
||||
self.wvd_location = wvd_location
|
||||
self.ffmpeg_location = (
|
||||
shutil.which(ffmpeg_location) if ffmpeg_location else None
|
||||
)
|
||||
self.mp4box_location = (
|
||||
shutil.which(mp4box_location) if mp4box_location else None
|
||||
)
|
||||
self.mp4decrypt_location = (
|
||||
shutil.which(mp4decrypt_location) if mp4decrypt_location else None
|
||||
)
|
||||
self.nm3u8dlre_location = (
|
||||
shutil.which(nm3u8dlre_location) if nm3u8dlre_location else None
|
||||
)
|
||||
self.template_folder_album = template_folder_album
|
||||
self.template_folder_compilation = template_folder_compilation
|
||||
self.template_file_single_disc = template_file_single_disc
|
||||
@@ -87,8 +67,6 @@ class Dl:
|
||||
self.template_file_music_video = template_file_music_video
|
||||
self.cover_size = cover_size
|
||||
self.cover_format = cover_format
|
||||
self.remux_mode = remux_mode
|
||||
self.download_mode = download_mode
|
||||
self.exclude_tags = (
|
||||
[i.lower() for i in exclude_tags.split(",")]
|
||||
if exclude_tags is not None
|
||||
@@ -97,12 +75,10 @@ class Dl:
|
||||
self.truncate = None if truncate is not None and truncate < 4 else truncate
|
||||
self.prefer_hevc = prefer_hevc
|
||||
self.ask_video_format = ask_video_format
|
||||
self.disable_music_video_album_skip = disable_music_video_album_skip
|
||||
self.songs_flavor = "32:ctrp64" if songs_heaac else "28:ctrp256"
|
||||
if not lrc_only:
|
||||
self.cdm = Cdm.from_device(Device.load(wvd_location))
|
||||
self.cdm_session = self.cdm.open()
|
||||
cookies = MozillaCookieJar(cookies_location)
|
||||
|
||||
def setup_session(self):
|
||||
cookies = MozillaCookieJar(self.cookies_location)
|
||||
cookies.load(ignore_discard=True, ignore_expires=True)
|
||||
self.session = requests.Session()
|
||||
self.session.cookies.update(cookies)
|
||||
@@ -133,9 +109,13 @@ class Dl:
|
||||
token = re.search('(?=eyJh)(.*?)(?=")', index_js_page).group(1)
|
||||
self.session.headers.update({"authorization": f"Bearer {token}"})
|
||||
self.country = self.session.cookies.get_dict()["itua"]
|
||||
self.storefront = getattr(gamdl.storefronts, self.country.upper())
|
||||
self.storefront = STOREFRONT_IDS[self.country.upper()]
|
||||
|
||||
def get_download_queue(self, url):
|
||||
def setup_cdm(self):
|
||||
self.cdm = Cdm.from_device(Device.load(self.wvd_location))
|
||||
self.cdm_session = self.cdm.open()
|
||||
|
||||
def get_download_queue(self, url: str) -> tuple[str, list[dict]]:
|
||||
download_queue = []
|
||||
track_id = url.split("/")[-1].split("i=")[-1].split("&")[0].split("?")[0]
|
||||
response = self.session.get(
|
||||
@@ -147,28 +127,15 @@ class Dl:
|
||||
"ids[music-videos]": track_id,
|
||||
},
|
||||
).json()["data"][0]
|
||||
if response["type"] == "songs" and "playParams" in response["attributes"]:
|
||||
download_queue.append(response)
|
||||
if (
|
||||
response["type"] == "music-videos"
|
||||
and "playParams" in response["attributes"]
|
||||
):
|
||||
if response["type"] in ("songs", "music-videos"):
|
||||
download_queue.append(response)
|
||||
if response["type"] in ("albums", "playlists"):
|
||||
for track in response["relationships"]["tracks"]["data"]:
|
||||
if "playParams" in track["attributes"]:
|
||||
if (
|
||||
track["type"] == "music-videos"
|
||||
and self.disable_music_video_album_skip
|
||||
):
|
||||
download_queue.append(track)
|
||||
if track["type"] == "songs":
|
||||
download_queue.append(track)
|
||||
download_queue.extend(response["relationships"]["tracks"]["data"])
|
||||
if not download_queue:
|
||||
raise Exception("Criteria not met")
|
||||
return download_queue
|
||||
return response["type"], download_queue
|
||||
|
||||
def get_webplayback(self, track_id):
|
||||
def get_webplayback(self, track_id: str) -> dict:
|
||||
response = self.session.post(
|
||||
"https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/webPlayback",
|
||||
json={
|
||||
@@ -178,12 +145,12 @@ class Dl:
|
||||
).json()["songList"][0]
|
||||
return response
|
||||
|
||||
def get_stream_url_song(self, webplayback):
|
||||
def get_stream_url_song(self, webplayback: dict) -> str:
|
||||
return next(
|
||||
i for i in webplayback["assets"] if i["flavor"] == self.songs_flavor
|
||||
)["URL"]
|
||||
|
||||
def get_stream_url_music_video(self, webplayback):
|
||||
def get_stream_url_music_video(self, webplayback: dict) -> tuple[str, str]:
|
||||
ydl = YoutubeDL(
|
||||
{
|
||||
"allow_unplayable_formats": True,
|
||||
@@ -233,63 +200,64 @@ class Dl:
|
||||
)
|
||||
return stream_url_video, stream_url_audio
|
||||
|
||||
def get_encrypted_location_video(self, track_id):
|
||||
def get_encrypted_location_video(self, track_id: str) -> Path:
|
||||
return self.temp_path / f"{track_id}_encrypted_video.mp4"
|
||||
|
||||
def get_encrypted_location_audio(self, track_id):
|
||||
def get_encrypted_location_audio(self, track_id: str) -> Path:
|
||||
return self.temp_path / f"{track_id}_encrypted_audio.m4a"
|
||||
|
||||
def get_decrypted_location_video(self, track_id):
|
||||
def get_decrypted_location_video(self, track_id: str) -> Path:
|
||||
return self.temp_path / f"{track_id}_decrypted_video.mp4"
|
||||
|
||||
def get_decrypted_location_audio(self, track_id):
|
||||
def get_decrypted_location_audio(self, track_id: str) -> Path:
|
||||
return self.temp_path / f"{track_id}_decrypted_audio.m4a"
|
||||
|
||||
def get_fixed_location(self, track_id, file_extension):
|
||||
def get_fixed_location(self, track_id: str, file_extension: str) -> Path:
|
||||
return self.temp_path / f"{track_id}_fixed{file_extension}"
|
||||
|
||||
def get_cover_location_song(self, final_location):
|
||||
def get_cover_location_song(self, final_location: Path) -> Path:
|
||||
return final_location.parent / f"Cover.{self.cover_format}"
|
||||
|
||||
def get_cover_location_music_video(self, final_location):
|
||||
def get_cover_location_music_video(self, final_location: Path) -> Path:
|
||||
return final_location.with_suffix(f".{self.cover_format}")
|
||||
|
||||
def get_lrc_location(self, final_location):
|
||||
def get_lrc_location(self, final_location: Path) -> Path:
|
||||
return final_location.with_suffix(".lrc")
|
||||
|
||||
def download(self, encrypted_location, stream_url):
|
||||
if self.download_mode == "yt-dlp":
|
||||
params = {
|
||||
def download_ytdlp(self, encrypted_location: Path, stream_url: str) -> None:
|
||||
with YoutubeDL(
|
||||
{
|
||||
"quiet": True,
|
||||
"no_warnings": True,
|
||||
"outtmpl": str(encrypted_location),
|
||||
"allow_unplayable_formats": True,
|
||||
"fixup": "never",
|
||||
}
|
||||
with YoutubeDL(params) as ydl:
|
||||
ydl.download(stream_url)
|
||||
else:
|
||||
subprocess.run(
|
||||
[
|
||||
self.nm3u8dlre_location,
|
||||
stream_url,
|
||||
"--binary-merge",
|
||||
"--no-log",
|
||||
"--log-level",
|
||||
"off",
|
||||
"--ffmpeg-binary-path",
|
||||
self.ffmpeg_location,
|
||||
"--save-name",
|
||||
encrypted_location.stem,
|
||||
"--save-dir",
|
||||
encrypted_location.parent,
|
||||
"--tmp-dir",
|
||||
encrypted_location.parent,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
) as ydl:
|
||||
ydl.download(stream_url)
|
||||
|
||||
def get_license_b64(self, challenge, track_uri, track_id):
|
||||
def download_nm3u8dlre(self, encrypted_location: Path, stream_url: str) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
self.nm3u8dlre_location,
|
||||
stream_url,
|
||||
"--binary-merge",
|
||||
"--no-log",
|
||||
"--log-level",
|
||||
"off",
|
||||
"--ffmpeg-binary-path",
|
||||
self.ffmpeg_location,
|
||||
"--save-name",
|
||||
encrypted_location.stem,
|
||||
"--save-dir",
|
||||
encrypted_location.parent,
|
||||
"--tmp-dir",
|
||||
encrypted_location.parent,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def get_license_b64(self, challenge: str, track_uri: str, track_id: str) -> str:
|
||||
return self.session.post(
|
||||
"https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense",
|
||||
json={
|
||||
@@ -302,7 +270,7 @@ class Dl:
|
||||
},
|
||||
).json()["license"]
|
||||
|
||||
def get_decryption_key_music_video(self, stream_url, track_id):
|
||||
def get_decryption_key_music_video(self, stream_url: str, track_id: str) -> str:
|
||||
playlist = m3u8.load(stream_url)
|
||||
track_uri = next(
|
||||
i
|
||||
@@ -319,7 +287,7 @@ class Dl:
|
||||
i for i in self.cdm.get_keys(self.cdm_session) if i.type == "CONTENT"
|
||||
).key.hex()
|
||||
|
||||
def get_decryption_key_song(self, stream_url, track_id):
|
||||
def get_decryption_key_song(self, stream_url: str, track_id: str) -> str:
|
||||
track_uri = m3u8.load(stream_url).keys[0].uri
|
||||
widevine_pssh_data = WidevinePsshData()
|
||||
widevine_pssh_data.algorithm = 1
|
||||
@@ -334,7 +302,7 @@ class Dl:
|
||||
i for i in self.cdm.get_keys(self.cdm_session) if i.type == "CONTENT"
|
||||
).key.hex()
|
||||
|
||||
def get_synced_lyrics_lrc_timestamp(self, ttml_timestamp):
|
||||
def get_lyrics_synced_lrc_timestamp(self, ttml_timestamp: str) -> str:
|
||||
mins = int(ttml_timestamp.split(":")[-2]) if ":" in ttml_timestamp else 0
|
||||
secs, ms = str(
|
||||
float(ttml_timestamp.split(":")[-1])
|
||||
@@ -354,7 +322,7 @@ class Dl:
|
||||
)
|
||||
return lrc_timestamp.strftime("%M:%S.%f")[:-4]
|
||||
|
||||
def get_lyrics(self, track_id):
|
||||
def get_lyrics(self, track_id: str) -> tuple[str, str]:
|
||||
try:
|
||||
lyrics_ttml = ElementTree.fromstring(
|
||||
self.session.get(
|
||||
@@ -363,30 +331,32 @@ class Dl:
|
||||
)
|
||||
except:
|
||||
return None, None
|
||||
unsynced_lyrics = ""
|
||||
synced_lyrics = ""
|
||||
lyrics_unsynced = ""
|
||||
lyrics_synced = ""
|
||||
for div in lyrics_ttml.iter("{http://www.w3.org/ns/ttml}div"):
|
||||
for p in div.iter("{http://www.w3.org/ns/ttml}p"):
|
||||
if p.attrib.get("begin"):
|
||||
synced_lyrics += f'[{self.get_synced_lyrics_lrc_timestamp(p.attrib.get("begin"))}]{p.text}\n'
|
||||
lyrics_synced += f'[{self.get_lyrics_synced_lrc_timestamp(p.attrib.get("begin"))}]{p.text}\n'
|
||||
if p.text is not None:
|
||||
unsynced_lyrics += p.text + "\n"
|
||||
unsynced_lyrics += "\n"
|
||||
return unsynced_lyrics[:-2], synced_lyrics
|
||||
lyrics_unsynced += p.text + "\n"
|
||||
lyrics_unsynced += "\n"
|
||||
return lyrics_unsynced[:-2], lyrics_synced
|
||||
|
||||
def get_cover_url(self, webplayback: dict) -> str:
|
||||
return (
|
||||
webplayback["artwork-urls"]["default"]["url"].rsplit("/", 1)[0]
|
||||
+ f"/{self.cover_size}x{self.cover_size}bb.{self.cover_format}"
|
||||
)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_cover(self, cover_url):
|
||||
def get_cover(self, cover_url: str) -> bytes:
|
||||
return requests.get(cover_url).content
|
||||
|
||||
def get_tags_song(self, webplayback, unsynced_lyrics):
|
||||
def get_tags_song(self, webplayback: dict, lyrics_unsynced: str) -> dict:
|
||||
flavor = next(
|
||||
i for i in webplayback["assets"] if i["flavor"] == self.songs_flavor
|
||||
)
|
||||
metadata = flavor["metadata"]
|
||||
cover_url = flavor["artworkURL"].replace(
|
||||
"600x600bb.jpg",
|
||||
f"{self.cover_size}x{self.cover_size}bb.{self.cover_format}",
|
||||
)
|
||||
tags = {
|
||||
"album": metadata["playlistName"],
|
||||
"album_artist": metadata["playlistArtistName"],
|
||||
@@ -395,13 +365,21 @@ class Dl:
|
||||
"artist": metadata["artistName"],
|
||||
"artist_id": int(metadata["artistId"]),
|
||||
"artist_sort": metadata["sort-artist"],
|
||||
"comments": metadata.get("comments"),
|
||||
"compilation": metadata["compilation"],
|
||||
"cover_url": cover_url,
|
||||
"composer": metadata.get("composerName"),
|
||||
"composer_id": int(metadata.get("composerId"))
|
||||
if metadata.get("composerId")
|
||||
else None,
|
||||
"composer_sort": metadata.get("sort-composer"),
|
||||
"copyright": metadata.get("copyright"),
|
||||
"date": metadata.get("releaseDate"),
|
||||
"disc": metadata["discNumber"],
|
||||
"disc_total": metadata["discCount"],
|
||||
"gapless": metadata["gapless"],
|
||||
"genre": metadata["genre"],
|
||||
"genre_id": metadata["genreId"],
|
||||
"lyrics": lyrics_unsynced if lyrics_unsynced else None,
|
||||
"media_type": 1,
|
||||
"rating": metadata["explicit"],
|
||||
"storefront": metadata["s"],
|
||||
@@ -410,24 +388,11 @@ class Dl:
|
||||
"title_sort": metadata["sort-name"],
|
||||
"track": metadata["trackNumber"],
|
||||
"track_total": metadata["trackCount"],
|
||||
"xid": metadata.get("xid"),
|
||||
}
|
||||
if "comments" in metadata:
|
||||
tags["comment"] = metadata["comments"]
|
||||
if "composerId" in metadata:
|
||||
tags["composer"] = metadata["composerName"]
|
||||
tags["composer_id"] = int(metadata["composerId"])
|
||||
tags["composer_sort"] = metadata["sort-composer"]
|
||||
if "copyright" in metadata:
|
||||
tags["copyright"] = metadata["copyright"]
|
||||
if "releaseDate" in metadata:
|
||||
tags["release_date"] = metadata["releaseDate"]
|
||||
if "xid" in metadata:
|
||||
tags["xid"] = metadata["xid"]
|
||||
if unsynced_lyrics:
|
||||
tags["lyrics"] = unsynced_lyrics
|
||||
return tags
|
||||
|
||||
def get_tags_music_video(self, track_id):
|
||||
def get_tags_music_video(self, track_id: str) -> dict:
|
||||
metadata = requests.get(
|
||||
f"https://itunes.apple.com/lookup",
|
||||
params={
|
||||
@@ -446,20 +411,15 @@ class Dl:
|
||||
tags = {
|
||||
"artist": metadata[0]["artistName"],
|
||||
"artist_id": metadata[0]["artistId"],
|
||||
"cover_url": metadata[0]["artworkUrl30"].replace(
|
||||
"30x30bb.jpg",
|
||||
f"{self.cover_size}x{self.cover_size}bb.{self.cover_format}",
|
||||
),
|
||||
"copyright": extra_metadata.get("copyright"),
|
||||
"date": metadata[0]["releaseDate"],
|
||||
"genre": metadata[0]["primaryGenreName"],
|
||||
"genre_id": int(extra_metadata["genres"][0]["genreId"]),
|
||||
"media_type": 6,
|
||||
"release_date": metadata[0]["releaseDate"],
|
||||
"storefront": int(self.storefront.split("-")[0]),
|
||||
"title": metadata[0]["trackCensoredName"],
|
||||
"title_id": metadata[0]["trackId"],
|
||||
}
|
||||
if "copyright" in extra_metadata:
|
||||
tags["copyright"] = extra_metadata["copyright"]
|
||||
if metadata[0]["trackExplicitness"] == "notExplicit":
|
||||
tags["rating"] = 0
|
||||
elif metadata[0]["trackExplicitness"] == "explicit":
|
||||
@@ -476,7 +436,7 @@ class Dl:
|
||||
tags["track_total"] = metadata[0]["trackCount"]
|
||||
return tags
|
||||
|
||||
def get_sanitized_string(self, dirty_string, is_folder):
|
||||
def get_sanitized_string(self, dirty_string: str, is_folder: bool) -> str:
|
||||
dirty_string = re.sub(r'[\\/:*?"<>|;]', "_", dirty_string)
|
||||
if is_folder:
|
||||
dirty_string = dirty_string[: self.truncate]
|
||||
@@ -487,7 +447,7 @@ class Dl:
|
||||
dirty_string = dirty_string[: self.truncate - 4]
|
||||
return dirty_string.strip()
|
||||
|
||||
def get_final_location(self, tags):
|
||||
def get_final_location(self, tags: dict) -> Path:
|
||||
if "album" in tags:
|
||||
final_location_folder = (
|
||||
self.template_folder_compilation.split("/")
|
||||
@@ -518,7 +478,9 @@ class Dl:
|
||||
*final_location_file
|
||||
)
|
||||
|
||||
def decrypt(self, encrypted_location, decrypted_location, decryption_key):
|
||||
def decrypt(
|
||||
self, encrypted_location: Path, decrypted_location: Path, decryption_key: str
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
self.mp4decrypt_location,
|
||||
@@ -529,7 +491,7 @@ class Dl:
|
||||
],
|
||||
)
|
||||
|
||||
def fixup_song_mp4box(self, decrypted_location, fixed_location):
|
||||
def fixup_song_mp4box(self, decrypted_location: Path, fixed_location: Path) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
self.mp4box_location,
|
||||
@@ -544,7 +506,10 @@ class Dl:
|
||||
)
|
||||
|
||||
def fixup_music_video_mp4box(
|
||||
self, decrypted_location_audio, decrypted_location_video, fixed_location
|
||||
self,
|
||||
decrypted_location_audio: Path,
|
||||
decrypted_location_video: Path,
|
||||
fixed_location: Path,
|
||||
):
|
||||
subprocess.run(
|
||||
[
|
||||
@@ -562,7 +527,9 @@ class Dl:
|
||||
check=True,
|
||||
)
|
||||
|
||||
def fixup_song_ffmpeg(self, encrypted_location, decryption_key, fixed_location):
|
||||
def fixup_song_ffmpeg(
|
||||
self, encrypted_location: Path, decryption_key: str, fixed_location: Path
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
self.ffmpeg_location,
|
||||
@@ -583,8 +550,11 @@ class Dl:
|
||||
)
|
||||
|
||||
def fixup_music_video_ffmpeg(
|
||||
self, decrypted_location_video, decrypted_location_audio, fixed_location
|
||||
):
|
||||
self,
|
||||
decrypted_location_video: Path,
|
||||
decrypted_location_audio: Path,
|
||||
fixed_location: Path,
|
||||
) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
self.ffmpeg_location,
|
||||
@@ -601,12 +571,14 @@ class Dl:
|
||||
"mp4",
|
||||
"-c",
|
||||
"copy",
|
||||
"-c:s",
|
||||
"mov_text",
|
||||
fixed_location,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def apply_tags(self, fixed_location, tags):
|
||||
def apply_tags(self, fixed_location: Path, tags: dict, cover_url: str) -> None:
|
||||
mp4_tags = {
|
||||
v: [tags[k]]
|
||||
for k, v in MP4_TAGS_MAP.items()
|
||||
@@ -621,7 +593,7 @@ class Dl:
|
||||
if "cover" not in self.exclude_tags:
|
||||
mp4_tags["covr"] = [
|
||||
MP4Cover(
|
||||
self.get_cover(tags["cover_url"]),
|
||||
self.get_cover(cover_url),
|
||||
imageformat=MP4Cover.FORMAT_JPEG
|
||||
if self.cover_format == "jpg"
|
||||
else MP4Cover.FORMAT_PNG,
|
||||
@@ -642,15 +614,18 @@ class Dl:
|
||||
mp4.update(mp4_tags)
|
||||
mp4.save()
|
||||
|
||||
def move_to_final_location(self, fixed_location, final_location):
|
||||
def move_to_final_location(
|
||||
self, fixed_location: Path, final_location: Path
|
||||
) -> None:
|
||||
final_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(fixed_location, final_location)
|
||||
|
||||
def save_cover(self, tags, cover_location):
|
||||
@functools.lru_cache()
|
||||
def save_cover(self, cover_location: Path, cover_url: str) -> None:
|
||||
with open(cover_location, "wb") as f:
|
||||
f.write(self.get_cover(tags["cover_url"]))
|
||||
f.write(self.get_cover(cover_url))
|
||||
|
||||
def make_lrc(self, lrc_location, synced_lyrics):
|
||||
def make_lrc(self, lrc_location: Path, lyrics_synced: str) -> None:
|
||||
lrc_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(lrc_location, "w", encoding="utf8") as f:
|
||||
f.write(synced_lyrics)
|
||||
f.write(lyrics_synced)
|
||||
@@ -1,155 +0,0 @@
|
||||
AE = "143481-2,32"
|
||||
AG = "143540-2,32"
|
||||
AI = "143538-2,32"
|
||||
AL = "143575-2,32"
|
||||
AM = "143524-2,32"
|
||||
AO = "143564-2,32"
|
||||
AR = "143505-28,32"
|
||||
AT = "143445-4,32"
|
||||
AU = "143460-27,32"
|
||||
AZ = "143568-2,32"
|
||||
BB = "143541-2,32"
|
||||
BE = "143446-2,32"
|
||||
BF = "143578-2,32"
|
||||
BG = "143526-2,32"
|
||||
BH = "143559-2,32"
|
||||
BJ = "143576-2,32"
|
||||
BM = "143542-2,32"
|
||||
BN = "143560-2,32"
|
||||
BO = "143556-28,32"
|
||||
BR = "143503-15,32"
|
||||
BS = "143539-2,32"
|
||||
BT = "143577-2,32"
|
||||
BW = "143525-2,32"
|
||||
BY = "143565-2,32"
|
||||
BZ = "143555-2,32"
|
||||
CA = "143455-6,32"
|
||||
CG = "143582-2,32"
|
||||
CH = "143459-57,32"
|
||||
CL = "143483-28,32"
|
||||
CN = "143465-19,32"
|
||||
CO = "143501-28,32"
|
||||
CR = "143495-28,32"
|
||||
CV = "143580-2,32"
|
||||
CY = "143557-2,32"
|
||||
CZ = "143489-2,32"
|
||||
DE = "143443-4,32"
|
||||
DK = "143458-2,32"
|
||||
DM = "143545-2,32"
|
||||
DO = "143508-28,32"
|
||||
DZ = "143563-2,32"
|
||||
EC = "143509-28,32"
|
||||
EE = "143518-2,32"
|
||||
EG = "143516-2,32"
|
||||
ES = "143454-8,32"
|
||||
FI = "143447-2,32"
|
||||
FJ = "143583-2,32"
|
||||
FM = "143591-2,32"
|
||||
FR = "143442-3,32"
|
||||
GB = "143444-2,32"
|
||||
GD = "143546-2,32"
|
||||
GH = "143573-2,32"
|
||||
GM = "143584-2,32"
|
||||
GR = "143448-2,32"
|
||||
GT = "143504-28,32"
|
||||
GW = "143585-2,32"
|
||||
GY = "143553-2,32"
|
||||
HK = "143463-45,32"
|
||||
HN = "143510-28,32"
|
||||
HR = "143494-2,32"
|
||||
HU = "143482-2,32"
|
||||
ID = "143476-2,32"
|
||||
IE = "143449-2,32"
|
||||
IL = "143491-2,32"
|
||||
IN = "143467-2,32"
|
||||
IS = "143558-2,32"
|
||||
IT = "143450-7,32"
|
||||
JM = "143511-2,32"
|
||||
JO = "143528-2,32"
|
||||
JP = "143462-9,32"
|
||||
KE = "143529-2,32"
|
||||
KG = "143586-2,32"
|
||||
KH = "143579-2,32"
|
||||
KN = "143548-2,32"
|
||||
KR = "143466-13,32"
|
||||
KW = "143493-2,32"
|
||||
KY = "143544-2,32"
|
||||
KZ = "143517-2,32"
|
||||
LA = "143587-2,32"
|
||||
LB = "143497-2,32"
|
||||
LC = "143549-2,32"
|
||||
LK = "143486-2,32"
|
||||
LR = "143588-2,32"
|
||||
LT = "143520-2,32"
|
||||
LU = "143451-2,32"
|
||||
LV = "143519-2,32"
|
||||
MD = "143523-2,32"
|
||||
MG = "143531-2,32"
|
||||
MK = "143530-2,32"
|
||||
ML = "143532-2,32"
|
||||
MN = "143592-2,32"
|
||||
MO = "143515-45,32"
|
||||
MR = "143590-2,32"
|
||||
MS = "143547-2,32"
|
||||
MT = "143521-2,32"
|
||||
MU = "143533-2,32"
|
||||
MW = "143589-2,32"
|
||||
MX = "143468-28,32"
|
||||
MY = "143473-2,32"
|
||||
MZ = "143593-2,32"
|
||||
NA = "143594-2,32"
|
||||
NE = "143534-2,32"
|
||||
NG = "143561-2,32"
|
||||
NI = "143512-28,32"
|
||||
NL = "143452-10,32"
|
||||
NO = "143457-2,32"
|
||||
NP = "143484-2,32"
|
||||
NZ = "143461-27,32"
|
||||
OM = "143562-2,32"
|
||||
PA = "143485-28,32"
|
||||
PE = "143507-28,32"
|
||||
PG = "143597-2,32"
|
||||
PH = "143474-2,32"
|
||||
PK = "143477-2,32"
|
||||
PL = "143478-2,32"
|
||||
PT = "143453-24,32"
|
||||
PW = "143595-2,32"
|
||||
PY = "143513-28,32"
|
||||
QA = "143498-2,32"
|
||||
RO = "143487-2,32"
|
||||
RU = "143469-16,32"
|
||||
SA = "143479-2,32"
|
||||
SB = "143601-2,32"
|
||||
SC = "143599-2,32"
|
||||
SE = "143456-17,32"
|
||||
SG = "143464-19,32"
|
||||
SI = "143499-2,32"
|
||||
SK = "143496-2,32"
|
||||
SL = "143600-2,32"
|
||||
SN = "143535-2,32"
|
||||
SR = "143554-2,32"
|
||||
ST = "143598-2,32"
|
||||
SV = "143506-28,32"
|
||||
SZ = "143602-2,32"
|
||||
TC = "143552-2,32"
|
||||
TD = "143581-2,32"
|
||||
TH = "143475-2,32"
|
||||
TJ = "143603-2,32"
|
||||
TM = "143604-2,32"
|
||||
TN = "143536-2,32"
|
||||
TR = "143480-2,32"
|
||||
TT = "143551-2,32"
|
||||
TW = "143470-18,32"
|
||||
TZ = "143572-2,32"
|
||||
UA = "143492-2,32"
|
||||
UG = "143537-2,32"
|
||||
US = "143441-1,32"
|
||||
UY = "143514-2,32"
|
||||
UZ = "143566-2,32"
|
||||
VC = "143550-2,32"
|
||||
VE = "143502-28,32"
|
||||
VG = "143543-2,32"
|
||||
VN = "143471-2,32"
|
||||
YE = "143571-2,32"
|
||||
ZA = "143472-2,32"
|
||||
ZW = "143605-2,32"
|
||||
Reference in New Issue
Block a user