mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 20:25:13 +03:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48e172a40e | |||
| fb515dc70b | |||
| 6a2d0d4f39 | |||
| aa5171a820 | |||
| 82df24b21b | |||
| 4752faa555 | |||
| e8e8373b16 | |||
| 3b8954d90d | |||
| e134814fea | |||
| 5b884743d8 | |||
| 268d9a71fc | |||
| e36a33be02 | |||
| 287df2caea | |||
| 840987b28e | |||
| abf8c4c795 | |||
| e2a96b31db | |||
| 448de3a0c0 | |||
| e1f027dcb1 | |||
| ba4e9576bc | |||
| 8c7ad61811 | |||
| e3d2cfa357 | |||
| 3680afa017 | |||
| 9f93b0e791 | |||
| ce2bdc8d61 | |||
| 30e498aeeb | |||
| 4d150c35a8 | |||
| be8eeb80c9 | |||
| b17c31d416 | |||
| 42d10d555a | |||
| 38d131a699 | |||
| 322cb7714e | |||
| 6383dd78c4 |
@@ -129,6 +129,7 @@ Config file values can be overridden using command-line arguments.
|
||||
| `--exclude-tags` / `exclude_tags` | Comma-separated tags to exclude. | `null` |
|
||||
| `--cover-size` / `cover_size` | Cover size. | `1200` |
|
||||
| `--truncate` / `truncate` | Maximum length of the file/folder names. | `null` |
|
||||
| `--database-path` / `database_path` | Path to the downloaded media database file. | `null` |
|
||||
| `--codec-song` / `codec_song` | Song codec. | `aac-legacy` |
|
||||
| `--synced-lyrics-format` / `synced_lyrics_format` | Synced lyrics format. | `lrc` |
|
||||
| `--codec-music-video` / `codec_music_video` | Comma-separated music video codec priority. | `h264,h265` |
|
||||
@@ -245,6 +246,11 @@ The following variables can be used in the template folders/files and/or in the
|
||||
- `png`: Lossless format.
|
||||
- `raw`: Raw cover without processing (requires `save_cover` to save separately).
|
||||
|
||||
### Database path
|
||||
|
||||
You can specify any path for storing a database file of downloaded media.
|
||||
This is useful if you want to avoid waiting for Gamdl to fetch metadata for checking if a media item has already been downloaded.
|
||||
|
||||
## Embedding
|
||||
|
||||
Gamdl can be used as a library in Python scripts. Here's a basic example of downloading a song by its ID:
|
||||
@@ -266,5 +272,7 @@ downloader = Downloader(
|
||||
downloader.set_cdm()
|
||||
downloader_song = DownloaderSong(downloader=downloader)
|
||||
|
||||
downloader_song.download(media_id="1624945512")
|
||||
for download_info in downloader_song.download(media_id="1624945512"):
|
||||
# Process download_info as needed
|
||||
pass
|
||||
```
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ from .downloader_post import DownloaderPost
|
||||
from .downloader_song import DownloaderSong
|
||||
from .itunes_api import ItunesApi
|
||||
|
||||
__version__ = "2.6"
|
||||
__version__ = "2.6.4"
|
||||
|
||||
+34
-10
@@ -28,6 +28,7 @@ from .enums import (
|
||||
SongCodec,
|
||||
SyncedLyricsFormat,
|
||||
)
|
||||
from .exceptions import *
|
||||
from .itunes_api import ItunesApi
|
||||
from .utils import color_text, prompt_path
|
||||
|
||||
@@ -318,6 +319,12 @@ def load_config_file(
|
||||
default=downloader_sig.parameters["truncate"].default,
|
||||
help="Maximum length of the file/folder names.",
|
||||
)
|
||||
@click.option(
|
||||
"--database-path",
|
||||
type=Path,
|
||||
default=downloader_sig.parameters["database_path"].default,
|
||||
help="Path to the downloaded media database file.",
|
||||
)
|
||||
# DownloaderSong specific options
|
||||
@click.option(
|
||||
"--codec-song",
|
||||
@@ -400,6 +407,7 @@ def main(
|
||||
exclude_tags: list[str],
|
||||
cover_size: int,
|
||||
truncate: int,
|
||||
database_path: Path,
|
||||
codec_song: SongCodec,
|
||||
synced_lyrics_format: SyncedLyricsFormat,
|
||||
codec_music_video: list[MusicVideoCodec],
|
||||
@@ -470,6 +478,7 @@ def main(
|
||||
exclude_tags,
|
||||
cover_size,
|
||||
truncate,
|
||||
database_path,
|
||||
log_level in ("WARNING", "ERROR"),
|
||||
)
|
||||
|
||||
@@ -550,22 +559,26 @@ def main(
|
||||
for url_index, url in enumerate(urls, start=1):
|
||||
url_progress = color_text(f"URL {url_index}/{len(urls)}", colorama.Style.DIM)
|
||||
try:
|
||||
logger.info(f'({url_progress}) Checking "{url}"')
|
||||
logger.info(f'({url_progress}) Processing "{url}"')
|
||||
url_info = downloader.parse_url_info(url)
|
||||
|
||||
if not url_info:
|
||||
error_count += 1
|
||||
logger.error(f"({url_progress}) Invalid URL, skipping")
|
||||
continue
|
||||
|
||||
download_queue = downloader.get_download_queue(url_info)
|
||||
download_queue_medias_metadata = download_queue.medias_metadata
|
||||
if not download_queue_medias_metadata[0]:
|
||||
|
||||
if not download_queue:
|
||||
error_count += 1
|
||||
logger.error(f"({url_progress}) Media not found, skipping")
|
||||
continue
|
||||
|
||||
download_queue_medias_metadata = download_queue.medias_metadata
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(
|
||||
f'({url_progress}) Failed to check "{url}"',
|
||||
f'({url_progress}) Failed to process URL "{url}", skipping',
|
||||
exc_info=not no_exceptions,
|
||||
)
|
||||
continue
|
||||
@@ -600,25 +613,36 @@ def main(
|
||||
continue
|
||||
|
||||
if media_metadata["type"] in {"songs", "library-songs"}:
|
||||
downloader_song.download(
|
||||
for _ in downloader_song.download(
|
||||
media_metadata=media_metadata,
|
||||
playlist_attributes=download_queue.playlist_attributes,
|
||||
playlist_track=download_index,
|
||||
)
|
||||
):
|
||||
pass
|
||||
|
||||
if media_metadata["type"] in {"music-videos", "library-music-videos"}:
|
||||
downloader_music_video.download(
|
||||
for _ in downloader_music_video.download(
|
||||
media_metadata=media_metadata,
|
||||
playlist_attributes=download_queue.playlist_attributes,
|
||||
playlist_track=download_index,
|
||||
)
|
||||
):
|
||||
pass
|
||||
|
||||
if media_metadata["type"] == "uploaded-videos":
|
||||
downloader_post.download(
|
||||
for _ in downloader_post.download(
|
||||
media_metadata=media_metadata,
|
||||
)
|
||||
):
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
except (
|
||||
MediaNotStreamableException,
|
||||
MediaFileAlreadyExistsException,
|
||||
MediaFormatNotAvailableException,
|
||||
) as e:
|
||||
logger.warning(
|
||||
f"({queue_progress}) {e}, skipping",
|
||||
)
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Database:
|
||||
INITIAL_QUERY = """
|
||||
CREATE TABLE IF NOT EXISTS media (
|
||||
media_id TEXT PRIMARY KEY,
|
||||
media_path TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
ADD_MEDIA_QUERY = """
|
||||
INSERT OR REPLACE INTO media (media_id, media_path) VALUES (?, ?)
|
||||
"""
|
||||
GET_MEDIA_QUERY = """
|
||||
SELECT media_path FROM media WHERE media_id = ?
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: Path):
|
||||
self.file_path = file_path
|
||||
self._initialize_db()
|
||||
|
||||
def _initialize_db(self):
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with sqlite3.connect(self.file_path) as conn:
|
||||
conn.execute(self.INITIAL_QUERY)
|
||||
conn.commit()
|
||||
|
||||
def add_media(self, media_id: str, media_path: Path):
|
||||
with sqlite3.connect(self.file_path) as conn:
|
||||
conn.execute(
|
||||
self.ADD_MEDIA_QUERY,
|
||||
(
|
||||
media_id,
|
||||
str(media_path.absolute()),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def get_media(self, media_id: str) -> Path | None:
|
||||
with sqlite3.connect(self.file_path) as conn:
|
||||
cursor = conn.execute(
|
||||
self.GET_MEDIA_QUERY,
|
||||
(media_id,),
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
return Path(result[0])
|
||||
return None
|
||||
+124
-29
@@ -9,6 +9,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import typing
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,6 +23,7 @@ from pywidevine import PSSH, Cdm, Device
|
||||
from yt_dlp import YoutubeDL
|
||||
|
||||
from .apple_music_api import AppleMusicApi
|
||||
from .database import Database
|
||||
from .enums import CoverFormat, DownloadMode, MediaFileFormat, RemuxMode
|
||||
from .hardcoded_wvd import HARDCODED_WVD
|
||||
from .itunes_api import ItunesApi
|
||||
@@ -45,8 +47,8 @@ class Downloader:
|
||||
r"("
|
||||
r"/(?P<storefront>[a-z]{2})"
|
||||
r"/(?P<type>artist|album|playlist|song|music-video|post)"
|
||||
r"(?:/(?P<slug>[a-z0-9-]+))?"
|
||||
r"/(?P<id>[0-9]+|pl\.[0-9a-z]{32}|pl\.u-[a-zA-Z0-9]{15})"
|
||||
r"(?:/(?P<slug>[^\s/]+))?"
|
||||
r"/(?P<id>[0-9]+|pl\.[0-9a-z]{32}|pl\.u-[a-zA-Z0-9]+)"
|
||||
r"(?:\?i=(?P<sub_id>[0-9]+))?"
|
||||
r")|("
|
||||
r"(?:/(?P<library_storefront>[a-z]{2}))?"
|
||||
@@ -89,6 +91,7 @@ class Downloader:
|
||||
exclude_tags: list[str] = None,
|
||||
cover_size: int = 1200,
|
||||
truncate: int = None,
|
||||
database_path: Path = None,
|
||||
silent: bool = False,
|
||||
skip_processing: bool = False,
|
||||
):
|
||||
@@ -120,17 +123,19 @@ class Downloader:
|
||||
self.exclude_tags = exclude_tags
|
||||
self.cover_size = cover_size
|
||||
self.truncate = truncate
|
||||
self.database_path = database_path
|
||||
self.silent = silent
|
||||
self.skip_processing = skip_processing
|
||||
self._set_temp_path()
|
||||
self._set_exclude_tags()
|
||||
self._set_binaries_path_full()
|
||||
self._set_truncate()
|
||||
self._set_database()
|
||||
self._set_subprocess_additional_args()
|
||||
|
||||
def _set_temp_path(self):
|
||||
random_suffix = uuid.uuid4().hex[:8]
|
||||
self.temp_path = self.temp_path / f"gamdl_temp_{random_suffix}"
|
||||
self.temp_path_generated = self.temp_path / f"gamdl_temp_{random_suffix}"
|
||||
|
||||
def _set_exclude_tags(self):
|
||||
self.exclude_tags = self.exclude_tags if self.exclude_tags is not None else []
|
||||
@@ -145,6 +150,12 @@ class Downloader:
|
||||
if self.truncate is not None:
|
||||
self.truncate = None if self.truncate < 4 else self.truncate
|
||||
|
||||
def _set_database(self):
|
||||
if self.database_path is not None:
|
||||
self.database = Database(self.database_path)
|
||||
else:
|
||||
self.database = None
|
||||
|
||||
def _set_subprocess_additional_args(self):
|
||||
if self.silent:
|
||||
self.subprocess_additional_args = {
|
||||
@@ -161,6 +172,8 @@ class Downloader:
|
||||
self.cdm = Cdm.from_device(Device.loads(HARDCODED_WVD))
|
||||
|
||||
def parse_url_info(self, url: str) -> UrlInfo | None:
|
||||
url = urllib.parse.unquote(url)
|
||||
|
||||
url_regex_result = re.search(
|
||||
self.VALID_URL_RE,
|
||||
url,
|
||||
@@ -184,39 +197,70 @@ class Downloader:
|
||||
url_type: str,
|
||||
id: str,
|
||||
is_library: bool,
|
||||
) -> DownloadQueue:
|
||||
) -> DownloadQueue | None:
|
||||
download_queue = DownloadQueue()
|
||||
|
||||
if url_type == "artist":
|
||||
artist = self.apple_music_api.get_artist(id)
|
||||
|
||||
if artist is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = list(
|
||||
self.get_download_queue_from_artist(artist)
|
||||
)
|
||||
elif url_type == "song":
|
||||
download_queue.medias_metadata = [self.apple_music_api.get_song(id)]
|
||||
elif url_type in {"album", "albums"}:
|
||||
|
||||
if url_type == "song":
|
||||
song = self.apple_music_api.get_song(id)
|
||||
|
||||
if song is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = [song]
|
||||
|
||||
if url_type in {"album", "albums"}:
|
||||
if is_library:
|
||||
album = self.apple_music_api.get_library_album(id)
|
||||
else:
|
||||
album = self.apple_music_api.get_album(id)
|
||||
|
||||
if album is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = [
|
||||
track for track in album["relationships"]["tracks"]["data"]
|
||||
]
|
||||
elif url_type == "playlist":
|
||||
|
||||
if url_type == "playlist":
|
||||
if is_library:
|
||||
playlist = self.apple_music_api.get_library_playlist(id)
|
||||
download_queue.medias_metadata = [
|
||||
track for track in playlist["relationships"]["tracks"]["data"]
|
||||
]
|
||||
else:
|
||||
playlist = self.apple_music_api.get_playlist(id)
|
||||
download_queue.medias_metadata = [
|
||||
track for track in playlist["relationships"]["tracks"]["data"]
|
||||
]
|
||||
|
||||
if playlist is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = [
|
||||
track for track in playlist["relationships"]["tracks"]["data"]
|
||||
]
|
||||
download_queue.playlist_attributes = playlist["attributes"]
|
||||
elif url_type == "music-video":
|
||||
download_queue.medias_metadata = [self.apple_music_api.get_music_video(id)]
|
||||
elif url_type == "post":
|
||||
download_queue.medias_metadata = [self.apple_music_api.get_post(id)]
|
||||
|
||||
if url_type == "music-video":
|
||||
music_video = self.apple_music_api.get_music_video(id)
|
||||
|
||||
if music_video is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = [music_video]
|
||||
|
||||
if url_type == "post":
|
||||
post = self.apple_music_api.get_post(id)
|
||||
|
||||
if post is None:
|
||||
return None
|
||||
|
||||
download_queue.medias_metadata = [post]
|
||||
|
||||
return download_queue
|
||||
|
||||
def get_download_queue_from_artist(
|
||||
@@ -313,6 +357,18 @@ class Downloader:
|
||||
) -> bool:
|
||||
return bool(media_metadata["attributes"].get("playParams"))
|
||||
|
||||
def get_database_final_path(self, media_id: str) -> Path | None:
|
||||
if self.database is None:
|
||||
return
|
||||
|
||||
final_path_database = self.database.get_media(media_id)
|
||||
if (
|
||||
final_path_database is not None
|
||||
and final_path_database.exists()
|
||||
and not self.overwrite
|
||||
):
|
||||
return final_path_database
|
||||
|
||||
def get_playlist_tags(
|
||||
self,
|
||||
playlist_attributes: dict,
|
||||
@@ -474,7 +530,7 @@ class Downloader:
|
||||
tag: str,
|
||||
file_extension: str,
|
||||
):
|
||||
temp_path = self.temp_path / (f"{media_id}_{tag}" + file_extension)
|
||||
temp_path = self.temp_path_generated / (f"{media_id}_{tag}" + file_extension)
|
||||
return temp_path
|
||||
|
||||
def get_final_path(
|
||||
@@ -639,9 +695,31 @@ class Downloader:
|
||||
encoding="utf8",
|
||||
)
|
||||
|
||||
def cleanup_temp_path(self):
|
||||
if self.temp_path.exists() and not self.skip_processing:
|
||||
shutil.rmtree(self.temp_path)
|
||||
def cleanup_temp_path(self) -> None:
|
||||
if self.temp_path_generated.exists():
|
||||
shutil.rmtree(self.temp_path_generated)
|
||||
|
||||
def _final_processing_wrapper(
|
||||
self,
|
||||
func,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
exception = None
|
||||
download_info = None
|
||||
try:
|
||||
for download_info in func(*args, **kwargs):
|
||||
yield download_info
|
||||
except Exception as e:
|
||||
exception = e
|
||||
finally:
|
||||
if download_info is not None and isinstance(download_info, DownloadInfo):
|
||||
self._final_processing(
|
||||
download_info,
|
||||
)
|
||||
|
||||
if exception is not None:
|
||||
raise exception
|
||||
|
||||
def _final_processing(
|
||||
self,
|
||||
@@ -650,11 +728,20 @@ class Downloader:
|
||||
if self.skip_processing:
|
||||
return
|
||||
|
||||
colored_media_id = color_text(download_info.media_id, colorama.Style.DIM)
|
||||
if download_info.media_id:
|
||||
colored_media_id = color_text(
|
||||
download_info.media_id,
|
||||
colorama.Style.DIM,
|
||||
)
|
||||
else:
|
||||
colored_media_id = color_text(
|
||||
"Unknown",
|
||||
colorama.Style.DIM,
|
||||
)
|
||||
|
||||
if download_info.staged_path:
|
||||
logger.debug(
|
||||
f"[{colored_media_id}] Applying tags to {download_info.staged_path}"
|
||||
f'[{colored_media_id}] Applying tags to "{download_info.staged_path}"'
|
||||
)
|
||||
self.apply_tags(
|
||||
download_info.staged_path,
|
||||
@@ -670,6 +757,15 @@ class Downloader:
|
||||
)
|
||||
logger.info(f"[{colored_media_id}] Download completed successfully")
|
||||
|
||||
if self.database is not None:
|
||||
logger.debug(
|
||||
f'[{colored_media_id}] Adding entry to database at "{self.database_path}"'
|
||||
)
|
||||
self.database.add_media(
|
||||
download_info.media_id,
|
||||
download_info.final_path,
|
||||
)
|
||||
|
||||
if (
|
||||
download_info.cover_path and not self.save_cover
|
||||
) or not download_info.cover_path:
|
||||
@@ -705,11 +801,8 @@ class Downloader:
|
||||
download_info.synced_lyrics_path,
|
||||
download_info.lyrics.synced,
|
||||
)
|
||||
if (
|
||||
download_info.playlist_tags
|
||||
and self.save_playlist
|
||||
and download_info.staged_path
|
||||
):
|
||||
|
||||
if download_info.playlist_tags and self.save_playlist:
|
||||
playlist_file_path = self.get_playlist_file_path(
|
||||
download_info.playlist_tags
|
||||
)
|
||||
@@ -721,3 +814,5 @@ class Downloader:
|
||||
download_info.final_path,
|
||||
download_info.playlist_tags.playlist_track,
|
||||
)
|
||||
|
||||
self.cleanup_temp_path()
|
||||
|
||||
@@ -18,6 +18,7 @@ from .enums import (
|
||||
RemuxFormatMusicVideo,
|
||||
RemuxMode,
|
||||
)
|
||||
from .exceptions import *
|
||||
from .models import (
|
||||
DecryptionKeyAv,
|
||||
DownloadInfo,
|
||||
@@ -429,24 +430,22 @@ class DownloaderMusicVideo:
|
||||
self.downloader.get_cover_file_extension(cover_format)
|
||||
)
|
||||
|
||||
import typing
|
||||
|
||||
def download(
|
||||
self,
|
||||
media_id: str = None,
|
||||
media_metadata: dict = None,
|
||||
playlist_attributes: dict = None,
|
||||
playlist_track: int = None,
|
||||
) -> DownloadInfo:
|
||||
try:
|
||||
download_info = self._download(
|
||||
media_id,
|
||||
media_metadata,
|
||||
playlist_attributes,
|
||||
playlist_track,
|
||||
)
|
||||
self.downloader._final_processing(download_info)
|
||||
finally:
|
||||
self.downloader.cleanup_temp_path()
|
||||
return download_info
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
yield from self.downloader._final_processing_wrapper(
|
||||
self._download,
|
||||
media_id,
|
||||
media_metadata,
|
||||
playlist_attributes,
|
||||
playlist_track,
|
||||
)
|
||||
|
||||
def _download(
|
||||
self,
|
||||
@@ -454,8 +453,9 @@ class DownloaderMusicVideo:
|
||||
media_metadata: dict = None,
|
||||
playlist_attributes: dict = None,
|
||||
playlist_track: int = None,
|
||||
) -> DownloadInfo:
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
download_info = DownloadInfo()
|
||||
yield download_info
|
||||
|
||||
if playlist_track is None and playlist_attributes:
|
||||
raise ValueError(
|
||||
@@ -473,25 +473,25 @@ class DownloaderMusicVideo:
|
||||
if not media_id and not media_metadata:
|
||||
raise ValueError("Either media_id or media_metadata must be provided")
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(
|
||||
f"[{color_text(media_id, colorama.Style.DIM)}] "
|
||||
"Getting Music Video metadata"
|
||||
)
|
||||
media_metadata = self.downloader.apple_music_api.get_music_video(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not media_id:
|
||||
if media_metadata:
|
||||
media_id = self.downloader.get_media_id_of_library_media(media_metadata)
|
||||
download_info.media_id = media_id
|
||||
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
||||
|
||||
database_final_path = self.downloader.get_database_final_path(media_id)
|
||||
if database_final_path:
|
||||
download_info.final_path = database_final_path
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(database_final_path)
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(f"[{colored_media_id}] Getting Music Video metadata")
|
||||
media_metadata = self.downloader.apple_music_api.get_music_video(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not self.downloader.is_media_streamable(media_metadata):
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] "
|
||||
"Music Video is not streamable or downloadable, skipping"
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
alt_media_id = self.get_music_video_id_alt(media_metadata) or media_id
|
||||
download_info.alt_media_id = alt_media_id
|
||||
@@ -518,11 +518,11 @@ class DownloaderMusicVideo:
|
||||
webplayback = self.downloader.apple_music_api.get_webplayback(media_id)
|
||||
logger.debug(f"[{colored_media_id}] Getting stream info")
|
||||
stream_info = self.get_stream_info_from_webplayback(webplayback)
|
||||
|
||||
if not stream_info:
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] Video/Audio stream with the selected codec(s) not found, skipping"
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaFormatNotAvailableException()
|
||||
|
||||
download_info.stream_info = stream_info
|
||||
|
||||
final_path = self.downloader.get_final_path(
|
||||
@@ -543,10 +543,8 @@ class DownloaderMusicVideo:
|
||||
download_info.cover_path = cover_path
|
||||
|
||||
if final_path.exists() and not self.downloader.overwrite:
|
||||
logger.warning(
|
||||
f'[{colored_media_id}] Music Video already exists at "{final_path}", skipping'
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(final_path)
|
||||
|
||||
logger.debug(f"[{colored_media_id}] Getting decryption key")
|
||||
decryption_key = self.get_decryption_key(
|
||||
@@ -599,6 +597,7 @@ class DownloaderMusicVideo:
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[{colored_media_id}] "
|
||||
"Decrypting video/audio to "
|
||||
f'{decrypted_path_video}"/"{decrypted_path_audio}" '
|
||||
f'and remuxing to "{staged_path}"'
|
||||
@@ -613,4 +612,4 @@ class DownloaderMusicVideo:
|
||||
)
|
||||
download_info.staged_path = staged_path
|
||||
|
||||
return download_info
|
||||
yield download_info
|
||||
|
||||
+29
-26
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
import colorama
|
||||
@@ -9,6 +10,7 @@ from InquirerPy.base.control import Choice
|
||||
|
||||
from .downloader import Downloader
|
||||
from .enums import PostQuality
|
||||
from .exceptions import MediaFileAlreadyExistsException, MediaNotStreamableException
|
||||
from .models import DownloadInfo, MediaTags
|
||||
from .utils import color_text
|
||||
|
||||
@@ -86,46 +88,43 @@ class DownloaderPost:
|
||||
self,
|
||||
media_id: str = None,
|
||||
media_metadata: dict = None,
|
||||
) -> DownloadInfo:
|
||||
try:
|
||||
download_info = self._download(
|
||||
media_id,
|
||||
media_metadata,
|
||||
)
|
||||
self.downloader._final_processing(download_info)
|
||||
finally:
|
||||
self.downloader.cleanup_temp_path()
|
||||
return download_info
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
yield from self.downloader._final_processing_wrapper(
|
||||
self._download,
|
||||
media_id,
|
||||
media_metadata,
|
||||
)
|
||||
|
||||
def _download(
|
||||
self,
|
||||
media_id: str = None,
|
||||
media_metadata: dict = None,
|
||||
) -> DownloadInfo:
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
download_info = DownloadInfo()
|
||||
yield download_info
|
||||
|
||||
if not media_id and not media_metadata:
|
||||
raise ValueError("Either media_id or media_metadata must be provided")
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(
|
||||
f"[{color_text(media_id, colorama.Style.DIM)}] "
|
||||
"Getting Post Video metadata"
|
||||
)
|
||||
media_metadata = self.downloader.apple_music_api.get_post(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not media_id:
|
||||
if media_metadata:
|
||||
media_id = media_metadata["id"]
|
||||
download_info.media_id = media_id
|
||||
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
||||
|
||||
database_final_path = self.downloader.get_database_final_path(media_id)
|
||||
if database_final_path:
|
||||
download_info.final_path = database_final_path
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(database_final_path)
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(f"[{colored_media_id}] Getting Post Video metadata")
|
||||
media_metadata = self.downloader.apple_music_api.get_post(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not self.downloader.is_media_streamable(media_metadata):
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] "
|
||||
"Post Video is not streamable or downloadable, skipping"
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
tags = self.get_tags(media_metadata)
|
||||
final_path = self.downloader.get_final_path(
|
||||
@@ -136,6 +135,10 @@ class DownloaderPost:
|
||||
download_info.tags = tags
|
||||
download_info.final_path = final_path
|
||||
|
||||
if final_path.exists() and not self.downloader.overwrite:
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(final_path)
|
||||
|
||||
cover_url = self.downloader.get_cover_url(media_metadata)
|
||||
cover_format = self.downloader.get_cover_format(cover_url)
|
||||
if cover_format and self.downloader.save_cover:
|
||||
@@ -162,4 +165,4 @@ class DownloaderPost:
|
||||
)
|
||||
download_info.staged_path = staged_path
|
||||
|
||||
return download_info
|
||||
yield download_info
|
||||
|
||||
+33
-43
@@ -6,6 +6,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from xml.dom import minidom
|
||||
from xml.etree import ElementTree
|
||||
@@ -19,6 +20,7 @@ from pywidevine.license_protocol_pb2 import WidevinePsshData
|
||||
|
||||
from .downloader import Downloader
|
||||
from .enums import MediaFileFormat, RemuxMode, SongCodec, SyncedLyricsFormat
|
||||
from .exceptions import *
|
||||
from .models import (
|
||||
DecryptionKey,
|
||||
DecryptionKeyAv,
|
||||
@@ -592,18 +594,14 @@ class DownloaderSong:
|
||||
media_metadata: dict = None,
|
||||
playlist_attributes: dict = None,
|
||||
playlist_track: int = None,
|
||||
) -> DownloadInfo:
|
||||
try:
|
||||
download_info = self._download(
|
||||
media_id,
|
||||
media_metadata,
|
||||
playlist_attributes,
|
||||
playlist_track,
|
||||
)
|
||||
self.downloader._final_processing(download_info)
|
||||
finally:
|
||||
self.downloader.cleanup_temp_path()
|
||||
return download_info
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
yield from self.downloader._final_processing_wrapper(
|
||||
self._download,
|
||||
media_id,
|
||||
media_metadata,
|
||||
playlist_attributes,
|
||||
playlist_track,
|
||||
)
|
||||
|
||||
def _download(
|
||||
self,
|
||||
@@ -611,8 +609,9 @@ class DownloaderSong:
|
||||
media_metadata: dict = None,
|
||||
playlist_attributes: dict = None,
|
||||
playlist_track: int = None,
|
||||
) -> DownloadInfo:
|
||||
) -> typing.Generator[DownloadInfo, None, None]:
|
||||
download_info = DownloadInfo()
|
||||
yield download_info
|
||||
|
||||
if playlist_track is None and playlist_attributes:
|
||||
raise ValueError(
|
||||
@@ -630,31 +629,24 @@ class DownloaderSong:
|
||||
if not media_id and not media_metadata:
|
||||
raise ValueError("Either media_id or media_metadata must be provided")
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(
|
||||
f"[{color_text(media_id, colorama.Style.DIM)}] Getting Song metadata"
|
||||
)
|
||||
media_metadata = self.downloader.apple_music_api.get_song(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not media_id:
|
||||
if media_metadata:
|
||||
media_id = self.downloader.get_media_id_of_library_media(media_metadata)
|
||||
download_info.media_id = media_id
|
||||
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
||||
|
||||
if not self.downloader.is_media_streamable(media_metadata):
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] "
|
||||
"Song is not streamable or downloadable, skipping"
|
||||
)
|
||||
return download_info
|
||||
database_final_path = self.downloader.get_database_final_path(media_id)
|
||||
if database_final_path:
|
||||
download_info.final_path = database_final_path
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(database_final_path)
|
||||
|
||||
if not media_metadata:
|
||||
logger.debug(f"[{colored_media_id}] Getting Song metadata")
|
||||
media_metadata = self.downloader.apple_music_api.get_song(media_id)
|
||||
download_info.media_metadata = media_metadata
|
||||
|
||||
if not self.downloader.is_media_streamable(media_metadata):
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] "
|
||||
"Track is not streamable or downloadable, skipping"
|
||||
)
|
||||
return download_info
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
logger.debug(f"[{colored_media_id}] Getting lyrics")
|
||||
lyrics = self.get_lyrics(media_metadata)
|
||||
@@ -682,7 +674,8 @@ class DownloaderSong:
|
||||
logger.info(
|
||||
f"[{colored_media_id}] Downloading synced lyrics only, skipping song download"
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
return
|
||||
|
||||
cover_url = self.downloader.get_cover_url(media_metadata)
|
||||
cover_format = self.downloader.get_cover_format(cover_url)
|
||||
@@ -695,10 +688,8 @@ class DownloaderSong:
|
||||
download_info.cover_path = cover_path
|
||||
|
||||
if final_path.exists() and not self.downloader.overwrite:
|
||||
logger.warning(
|
||||
f'[{colored_media_id}] Song already exists at "{final_path}", skipping'
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaFileAlreadyExistsException(final_path)
|
||||
|
||||
logger.debug(f"[{colored_media_id}] Getting stream info")
|
||||
if self.codec.is_legacy():
|
||||
@@ -712,12 +703,11 @@ class DownloaderSong:
|
||||
download_info.decryption_key = decryption_key
|
||||
else:
|
||||
stream_info = self.get_stream_info(media_metadata)
|
||||
|
||||
if not stream_info or not stream_info.audio_track.widevine_pssh:
|
||||
logger.warning(
|
||||
f"[{colored_media_id}] Song is not downloadable or is not "
|
||||
"available in the selected codec, skipping",
|
||||
)
|
||||
return download_info
|
||||
yield download_info
|
||||
raise MediaFormatNotAvailableException()
|
||||
|
||||
logger.debug(f"[{colored_media_id}] Getting decryption key")
|
||||
decryption_key = self.get_decryption_key(
|
||||
stream_info,
|
||||
@@ -762,4 +752,4 @@ class DownloaderSong:
|
||||
)
|
||||
download_info.staged_path = staged_path
|
||||
|
||||
return download_info
|
||||
yield download_info
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MediaNotStreamableException(Exception):
|
||||
DEFAULT_MESSAGE = "Media is not streamable"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.DEFAULT_MESSAGE)
|
||||
|
||||
|
||||
class MediaFileAlreadyExistsException(Exception):
|
||||
DEFAULT_MESSAGE = "Media file already exists at '{media_path}'"
|
||||
|
||||
def __init__(self, media_path: Path):
|
||||
super().__init__(self.DEFAULT_MESSAGE.format(media_path=media_path))
|
||||
|
||||
|
||||
class MediaFormatNotAvailableException(Exception):
|
||||
DEFAULT_MESSAGE = "Requested media format or codec not available"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(self.DEFAULT_MESSAGE)
|
||||
+3
-1
@@ -118,6 +118,8 @@ class MediaTags:
|
||||
date_mp4 = self.date.strftime(date_format)
|
||||
elif isinstance(self.date, str):
|
||||
date_mp4 = self.date
|
||||
else:
|
||||
date_mp4 = None
|
||||
|
||||
mp4_tags = {
|
||||
"\xa9alb": [self.album],
|
||||
@@ -133,7 +135,7 @@ class MediaTags:
|
||||
"cmID": [self.composer_id],
|
||||
"soco": [self.composer_sort],
|
||||
"cprt": [self.copyright],
|
||||
"\xa9day": date_mp4,
|
||||
"\xa9day": [date_mp4],
|
||||
"disk": disc_mp4,
|
||||
"pgap": [bool(self.gapless) if self.gapless is not None else None],
|
||||
"\xa9gen": [self.genre],
|
||||
|
||||
Reference in New Issue
Block a user