Add media download database support

This commit is contained in:
Rafael Moraes
2025-09-14 11:00:16 -03:00
parent e1f027dcb1
commit e2a96b31db
7 changed files with 117 additions and 26 deletions
+6
View File
@@ -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:
+8
View File
@@ -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"),
)
+50
View File
@@ -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
+29
View File
@@ -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:
+8 -9
View File
@@ -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()
+8 -9
View File
@@ -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()
+8 -8
View File
@@ -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()