Compare commits

...

11 Commits

Author SHA1 Message Date
Rafael Moraes a009071a8d Bump version to 3.4 2026-04-27 06:35:39 -03:00
Rafael Moraes 64b1974232 Include filter result in exclusion error message 2026-04-27 06:35:00 -03:00
Rafael Moraes 37ede6572e Add overwrite flag to Database 2026-04-27 06:34:51 -03:00
Rafael Moraes 2e57216c3c Strip size suffix from Apple Music cover URLs 2026-04-27 06:25:55 -03:00
Rafael Moraes 5d242c89cd Remove 'level' and 'event' from event_dict 2026-04-26 11:41:47 -03:00
Rafael Moraes e5675f8874 Use CustomOutputWriter for structlog output 2026-04-26 00:38:08 -03:00
Rafael Moraes 716112c294 Use default_factory for DownloadItem uuid 2026-04-25 14:52:19 -03:00
Rafael Moraes 63ad0f2e07 Respect skip_cleanup when removing temp files 2026-04-25 14:28:09 -03:00
Rafael Moraes 939520b3f8 Stringify subprocess args in error message 2026-04-25 14:02:52 -03:00
Rafael Moraes df23276d3c Improve subprocess error message 2026-04-25 13:56:55 -03:00
Rafael Moraes a9227493ea Include subprocess output in async errors 2026-04-25 13:03:48 -03:00
11 changed files with 76 additions and 35 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "3.3"
__version__ = "3.4"
+6 -14
View File
@@ -1,5 +1,4 @@
import asyncio
import logging
from functools import wraps
from pathlib import Path
@@ -38,7 +37,7 @@ from .cli_config import CliConfig
from .config_file import ConfigFile
from .database import Database
from .interactive_prompts import InteractivePrompts
from .utils import custom_structlog_formatter, prompt_path
from .utils import CustomOutputWriter, custom_structlog_formatter, prompt_path
logger = structlog.get_logger(__name__)
@@ -60,18 +59,10 @@ def make_sync(func):
async def main(config: CliConfig):
colorama.just_fix_windows_console()
root_logger = logging.getLogger(__name__.split(".")[0])
root_logger.setLevel(config.log_level)
root_logger.propagate = False
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter("%(message)s"))
root_logger.addHandler(stream_handler)
log_output = CustomOutputWriter()
if config.log_file:
file_handler = logging.FileHandler(config.log_file, encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(message)s"))
root_logger.addHandler(file_handler)
log_output.add_file(config.log_file)
structlog.configure(
processors=[
@@ -79,7 +70,8 @@ async def main(config: CliConfig):
structlog.processors.ExceptionPrettyPrinter(),
custom_structlog_formatter,
],
logger_factory=structlog.stdlib.LoggerFactory(),
logger_factory=structlog.PrintLoggerFactory(file=log_output),
wrapper_class=structlog.make_filtering_bound_logger(config.log_level),
)
logger.info(f"Starting Gamdl {__version__}")
@@ -127,7 +119,7 @@ async def main(config: CliConfig):
)
if config.database_path:
database = Database(config.database_path)
database = Database(config.database_path, config.overwrite)
flat_filter = database.flat_filter
else:
database = None
+12 -2
View File
@@ -3,7 +3,13 @@ from pathlib import Path
class Database:
def __init__(self, path: Path):
def __init__(
self,
path: Path,
overwrite: bool,
):
self.overwrite = overwrite
self.connection = sqlite3.connect(path)
self.cursor = self.connection.cursor()
self._create_tables()
@@ -45,4 +51,8 @@ class Database:
if not result:
return None
return result if Path(result).exists() else None
return (
"Registered in database"
if Path(result).exists() and not self.overwrite
else None
)
+25 -2
View File
@@ -1,3 +1,5 @@
import atexit
import sys
from datetime import datetime
from enum import Enum
from pathlib import Path
@@ -39,12 +41,33 @@ class Csv(click.ParamType):
return result
class CustomOutputWriter:
def __init__(
self,
streams: list[Any] = [sys.stdout],
):
self.streams = streams
def add_file(self, path: str):
file_stream = open(path, "a")
atexit.register(file_stream.close)
self.streams.append(file_stream)
def write(self, message: str):
for stream in self.streams:
stream.write(message)
def flush(self):
for stream in self.streams:
stream.flush()
def custom_structlog_formatter(
logger: Any,
name: str,
event_dict: dict[str, Any],
) -> str:
level = event_dict.get("level", "INFO").upper()
level = event_dict.pop("level", "INFO").upper()
timestamp = datetime.now().strftime("%H:%M:%S")
level_colors = {
@@ -63,7 +86,7 @@ def custom_structlog_formatter(
prefix += click.style(f" [{action}]", dim=True)
if level in {"INFO", "WARNING", "ERROR", "CRITICAL"}:
message = event_dict.get("event", "")
message = event_dict.pop("event", "")
return f"{prefix} {message}"
else:
return f"{prefix} {event_dict}"
+3 -2
View File
@@ -88,7 +88,8 @@ class AppleMusicDownloader:
await self._download(item)
await self._final_processing(item)
finally:
self._cleanup_temp(item.uuid_)
if not self.skip_cleanup:
self._cleanup_temp(item.uuid_)
def _update_playlist_file(
self,
@@ -263,6 +264,6 @@ class AppleMusicDownloader:
log = logger.bind(action="cleanup_temp", folder_tag=folder_tag)
temp_path = Path(self.base.temp_path) / TEMP_PATH_TEMPLATE.format(folder_tag)
if temp_path.exists() and temp_path.is_dir() and not self.skip_cleanup:
if temp_path.exists() and temp_path.is_dir():
shutil.rmtree(temp_path, ignore_errors=True)
log.debug("success")
+2 -2
View File
@@ -1,5 +1,5 @@
import uuid
from dataclasses import dataclass
from dataclasses import dataclass, field
from ..interface.types import AppleMusicMedia
@@ -7,7 +7,7 @@ from ..interface.types import AppleMusicMedia
@dataclass
class DownloadItem:
media: AppleMusicMedia
uuid_: str = uuid.uuid4().hex[:8]
uuid_: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
staged_path: str = None
final_path: str = None
playlist_file_path: str = None
+8 -4
View File
@@ -223,12 +223,16 @@ class AppleMusicBaseInterface:
def _get_raw_cover_url(self, cover_url_template: str) -> str:
return re.sub(
r"image/thumb/",
r"/\{w\}x\{h\}bb\.jpg",
"",
re.sub(
r"is1-ssl",
"a1",
cover_url_template,
r"image/thumb/",
"",
re.sub(
r"is1-ssl",
"a1",
cover_url_template,
),
),
)
+3 -1
View File
@@ -46,6 +46,8 @@ class GamdlInterfaceArtistMediaTypeError(GamdlInterfaceError):
class GamdlInterfaceFlatFilterExcludedError(GamdlInterfaceError):
def __init__(self, media_id: str, result: Any):
super().__init__(f"Media excluded by flat filter: {media_id}")
super().__init__(
f"Media excluded by flat filter (media ID: {media_id}): {result}"
)
self.result = result
+14 -5
View File
@@ -1,14 +1,13 @@
import asyncio
import string
import subprocess
import typing
async def async_subprocess(*args: str, silent: bool = False) -> None:
if silent:
additional_args = {
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
"stdout": asyncio.subprocess.PIPE,
"stderr": asyncio.subprocess.PIPE,
}
else:
additional_args = {}
@@ -17,10 +16,20 @@ async def async_subprocess(*args: str, silent: bool = False) -> None:
*args,
**additional_args,
)
await proc.communicate()
stdout, stderr = await proc.communicate()
if proc.returncode != 0:
raise Exception(f'"{args[0]}" exited with code {proc.returncode}')
msg = (
f"Exited with code {proc.returncode}: {' '.join(str(arg) for arg in args)}"
)
if stdout:
msg += f"\nstdout:\n{stdout.decode()}"
if stderr:
msg += f"\nstderr:\n{stderr.decode()}"
raise Exception(msg)
async def safe_gather(
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "gamdl"
version = "3.3"
version = "3.4"
description = "A command-line app for downloading Apple Music songs, music videos and post videos."
readme = "README.md"
license = "MIT"
Generated
+1 -1
View File
@@ -223,7 +223,7 @@ wheels = [
[[package]]
name = "gamdl"
version = "3.3"
version = "3.4"
source = { virtual = "." }
dependencies = [
{ name = "async-lru" },