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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user