mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 04:05:14 +03:00
Add media download database support
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -319,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",
|
||||
@@ -401,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],
|
||||
@@ -471,6 +478,7 @@ def main(
|
||||
exclude_tags,
|
||||
cover_size,
|
||||
truncate,
|
||||
database_path,
|
||||
log_level in ("WARNING", "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
|
||||
)
|
||||
"""
|
||||
WRITE_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 write_media(self, media_id: str, media_path: Path):
|
||||
with sqlite3.connect(self.file_path) as conn:
|
||||
conn.execute(
|
||||
self.WRITE_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
|
||||
@@ -23,7 +23,9 @@ 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 .exceptions import MediaFileAlreadyExistsException
|
||||
from .hardcoded_wvd import HARDCODED_WVD
|
||||
from .itunes_api import ItunesApi
|
||||
from .models import (
|
||||
@@ -90,6 +92,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,
|
||||
):
|
||||
@@ -121,12 +124,14 @@ 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):
|
||||
@@ -146,6 +151,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 = {
|
||||
@@ -347,6 +358,18 @@ class Downloader:
|
||||
) -> bool:
|
||||
return bool(media_metadata["attributes"].get("playParams"))
|
||||
|
||||
def check_database_and_raise(self, media_id: str) -> 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
|
||||
):
|
||||
raise MediaFileAlreadyExistsException(final_path_database)
|
||||
|
||||
def get_playlist_tags(
|
||||
self,
|
||||
playlist_attributes: dict,
|
||||
@@ -707,6 +730,12 @@ class Downloader:
|
||||
)
|
||||
logger.info(f"[{colored_media_id}] Download completed successfully")
|
||||
|
||||
if self.database is not None:
|
||||
self.database.write_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:
|
||||
|
||||
@@ -474,19 +474,18 @@ 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)
|
||||
|
||||
self.downloader.check_database_and_raise(media_id)
|
||||
|
||||
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):
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
|
||||
@@ -108,19 +108,18 @@ class DownloaderPost:
|
||||
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)
|
||||
|
||||
self.downloader.check_database_and_raise(media_id)
|
||||
|
||||
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):
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
|
||||
@@ -631,18 +631,18 @@ 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)
|
||||
|
||||
self.downloader.check_database_and_raise(media_id)
|
||||
|
||||
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):
|
||||
raise MediaNotStreamableException()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user