mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-09-18 20:43:54 +03:00
feat: add persistent logging to console + rotating file
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Subject Area
|
## 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.
|
- 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.
|
- Targets power users and archivists who need large-scale, repeatable playlist archiving and ongoing synchronization, with GUI interface.
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
## Users Definition
|
## 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
|
## Functionality Definition
|
||||||
|
|
||||||
@@ -38,13 +38,10 @@ Individuals who need to download a large number of videos or audio files from a
|
|||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
- Desktop: Windows (Primary), Linux
|
- Desktop: Windows (Primary), Linux
|
||||||
- Docker
|
|
||||||
- Possible Future: Web App, Android App (via shared FastAPI backend)
|
|
||||||
|
|
||||||
## Architecture & Languages
|
## Architecture & Languages
|
||||||
|
|
||||||
- Core Engine: Python (yt-dlp wrapper)
|
- Core Engine: Python (yt-dlp)
|
||||||
- Backend API: FastAPI (Local localhost-only boundary)
|
|
||||||
- Desktop Frontend: PySide6 (Qt for Python)
|
- Desktop Frontend: PySide6 (Qt for Python)
|
||||||
- Distribution: PyInstaller / Briefcase (Windows .exe, Linux AppImage)
|
- Distribution: PyInstaller / Briefcase (Windows .exe, Linux AppImage)
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .config.settings import Settings
|
from .config.settings import Settings
|
||||||
@@ -12,6 +13,7 @@ from .core.events.event_bus import EventBus
|
|||||||
import re
|
import re
|
||||||
from .core.utils.yt import extract_playlist_id
|
from .core.utils.yt import extract_playlist_id
|
||||||
from .core.utils.deps import DependencyError
|
from .core.utils.deps import DependencyError
|
||||||
|
from .core.utils.logging_setup import configure_logging
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
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("--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("--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("--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)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
configure_logging(verbose=bool(args.debug), log_file=Path("app/data/app.log"))
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
db = Database(args.db.resolve())
|
db = Database(args.db.resolve())
|
||||||
service = SyncService(db)
|
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
|
counts[a.type.name] = counts.get(a.type.name, 0) + 1
|
||||||
summary = ", ".join(f"{k}:{v}" for k, v in sorted(counts.items()))
|
summary = ", ".join(f"{k}:{v}" for k, v in sorted(counts.items()))
|
||||||
print(f"Playlist {pid}: {len(actions)} actions → {summary}")
|
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:
|
if args.apply and actions:
|
||||||
try:
|
try:
|
||||||
asyncio.run(executor.execute(actions, pl))
|
asyncio.run(executor.execute(actions, pl))
|
||||||
except DependencyError as e:
|
except DependencyError as e:
|
||||||
print(f"ERROR: {e}")
|
print(f"ERROR: {e}")
|
||||||
|
log.error("dependency error: %s", e)
|
||||||
return 2
|
return 2
|
||||||
db.set_playlist_last_sync(pid)
|
db.set_playlist_last_sync(pid)
|
||||||
print(f"Applied actions for {pid}.")
|
print(f"Applied actions for {pid}.")
|
||||||
|
log.info("playlist=%s applied_actions=%s", pid, len(actions))
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from .downloader import Downloader
|
from .downloader import Downloader
|
||||||
from .queue_manager import DownloadJob, JobState
|
from .queue_manager import DownloadJob, JobState
|
||||||
|
|
||||||
|
|
||||||
async def default_worker(job: DownloadJob, *, max_retries: int = 2, delay_seconds: float = 1.5):
|
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)
|
dl = Downloader(ffmpeg_path=job.ffmpeg_path)
|
||||||
attempt = 0
|
attempt = 0
|
||||||
while attempt <= max_retries:
|
while attempt <= max_retries:
|
||||||
@@ -14,4 +16,13 @@ async def default_worker(job: DownloadJob, *, max_retries: int = 2, delay_second
|
|||||||
return
|
return
|
||||||
attempt += 1
|
attempt += 1
|
||||||
if attempt <= max_retries:
|
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