mirror of
https://github.com/glomatico/gamdl.git
synced 2026-06-13 12:15:18 +03:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a009071a8d | |||
| 64b1974232 | |||
| 37ede6572e | |||
| 2e57216c3c | |||
| 5d242c89cd | |||
| e5675f8874 | |||
| 716112c294 | |||
| 63ad0f2e07 | |||
| 939520b3f8 | |||
| df23276d3c | |||
| a9227493ea | |||
| 9375c2fccd | |||
| c83e47df0c |
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "3.2"
|
||||
__version__ = "3.4"
|
||||
|
||||
+6
-14
@@ -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
@@ -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
@@ -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}"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -281,7 +281,6 @@ class AppleMusicInterface:
|
||||
self._get_song_media(
|
||||
media_id=track["id"],
|
||||
index=index,
|
||||
total=base_media.media_metadata["attributes"]["trackCount"],
|
||||
media_metadata=track,
|
||||
playlist_metadata=base_media.media_metadata,
|
||||
)
|
||||
@@ -289,7 +288,6 @@ class AppleMusicInterface:
|
||||
else self._get_music_video_media(
|
||||
media_id=track["id"],
|
||||
index=index,
|
||||
total=base_media.media_metadata["attributes"]["trackCount"],
|
||||
media_metadata=track,
|
||||
playlist_metadata=base_media.media_metadata,
|
||||
)
|
||||
|
||||
+14
-5
@@ -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
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "gamdl"
|
||||
version = "3.2"
|
||||
version = "3.4"
|
||||
description = "A command-line app for downloading Apple Music songs, music videos and post videos."
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user