1
0
mirror of https://github.com/darkzoul5/YoutubePlaylistSync.git synced 2026-07-03 04:23:59 +03:00

feat: add persistent logging to console + rotating file

This commit is contained in:
2026-05-16 17:29:45 +03:00
parent 8550203411
commit 5649fc17dd
4 changed files with 61 additions and 7 deletions
+3 -6
View File
@@ -2,7 +2,7 @@
## Subject Area
- Tool for downloading and synchronizing YouTube playlists.
- Tool for downloading and synchronizing local YouTube playlists.
- Focuses on batch downloading, format selection (audio and/or video), configurable quality and keeping local copies synced with playlist changes.
- Targets power users and archivists who need large-scale, repeatable playlist archiving and ongoing synchronization, with GUI interface.
@@ -14,7 +14,7 @@
## Users Definition
Individuals who need to download a large number of videos or audio files from a YouTube playlist and keep it updated
Individuals who need to have a local youtube playlist synced with a large number of videos or audio files
## Functionality Definition
@@ -38,13 +38,10 @@ Individuals who need to download a large number of videos or audio files from a
## Platforms
- Desktop: Windows (Primary), Linux
- Docker
- Possible Future: Web App, Android App (via shared FastAPI backend)
## Architecture & Languages
- Core Engine: Python (yt-dlp wrapper)
- Backend API: FastAPI (Local localhost-only boundary)
- Core Engine: Python (yt-dlp)
- Desktop Frontend: PySide6 (Qt for Python)
- Distribution: PyInstaller / Briefcase (Windows .exe, Linux AppImage)
+9
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import argparse
import asyncio
import logging
from pathlib import Path
from .config.settings import Settings
@@ -12,6 +13,7 @@ from .core.events.event_bus import EventBus
import re
from .core.utils.yt import extract_playlist_id
from .core.utils.deps import DependencyError
from .core.utils.logging_setup import configure_logging
def main(argv: list[str] | None = None) -> int:
@@ -20,8 +22,12 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--db", type=Path, default=Path("app/data/app.db"), help="Path to SQLite database")
parser.add_argument("--playlist", type=int, default=None, help="Only run for a specific playlist index (0-based)")
parser.add_argument("--verbose", action="store_true", help="Print detailed events (rename/recycle/start)")
parser.add_argument("--debug", action="store_true", help="Enable debug logging to console + app/data/app.log")
args = parser.parse_args(argv)
configure_logging(verbose=bool(args.debug), log_file=Path("app/data/app.log"))
log = logging.getLogger(__name__)
settings = Settings()
db = Database(args.db.resolve())
service = SyncService(db)
@@ -88,14 +94,17 @@ def main(argv: list[str] | None = None) -> int:
counts[a.type.name] = counts.get(a.type.name, 0) + 1
summary = ", ".join(f"{k}:{v}" for k, v in sorted(counts.items()))
print(f"Playlist {pid}: {len(actions)} actions → {summary}")
log.info("playlist=%s actions=%s summary=%s", pid, len(actions), summary)
if args.apply and actions:
try:
asyncio.run(executor.execute(actions, pl))
except DependencyError as e:
print(f"ERROR: {e}")
log.error("dependency error: %s", e)
return 2
db.set_playlist_last_sync(pid)
print(f"Applied actions for {pid}.")
log.info("playlist=%s applied_actions=%s", pid, len(actions))
return 0
+12 -1
View File
@@ -1,11 +1,13 @@
from __future__ import annotations
import asyncio
import logging
from .downloader import Downloader
from .queue_manager import DownloadJob, JobState
async def default_worker(job: DownloadJob, *, max_retries: int = 2, delay_seconds: float = 1.5):
log = logging.getLogger(__name__)
dl = Downloader(ffmpeg_path=job.ffmpeg_path)
attempt = 0
while attempt <= max_retries:
@@ -14,4 +16,13 @@ async def default_worker(job: DownloadJob, *, max_retries: int = 2, delay_second
return
attempt += 1
if attempt <= max_retries:
await asyncio.sleep(delay_seconds)
wait = delay_seconds * (2 ** (attempt - 1))
log.warning(
"retrying download attempt=%s/%s video_id=%s wait=%.1fs error=%s",
attempt,
max_retries,
getattr(getattr(job, "item", None), "video_id", None),
wait,
job.error,
)
await asyncio.sleep(wait)
+37
View File
@@ -0,0 +1,37 @@
from __future__ import annotations
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
def configure_logging(*, verbose: bool = False, log_file: Path | None = None) -> None:
"""
Configure app-wide logging.
- Console handler always enabled.
- Rotating file handler enabled when log_file is provided.
"""
root = logging.getLogger()
root.setLevel(logging.DEBUG if verbose else logging.INFO)
# Avoid duplicate handlers on repeated calls (tests, re-entrypoints).
if getattr(configure_logging, "_configured", False):
return
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
console = logging.StreamHandler()
console.setLevel(logging.DEBUG if verbose else logging.INFO)
console.setFormatter(fmt)
root.addHandler(console)
if log_file is not None:
log_file.parent.mkdir(parents=True, exist_ok=True)
file_handler = RotatingFileHandler(str(log_file), maxBytes=2_000_000, backupCount=3, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt)
root.addHandler(file_handler)
configure_logging._configured = True # type: ignore[attr-defined]