refactor: consolidate sync entrypoints and trim dead GUI/util code

This commit is contained in:
2026-06-19 15:10:29 +03:00
parent c1a2227da8
commit 8ec24d04f6
5 changed files with 156 additions and 84 deletions
+45 -41
View File
@@ -1,18 +1,12 @@
from __future__ import annotations
import argparse
import asyncio
import logging
from pathlib import Path
from .config.settings import Settings
from .core.database.db import Database
from .core.sync.service import SyncService
from .core.sync.executor import ActionExecutor
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.sync.runner import build_sync_stack, format_action_summary, run_sync_batch
from .core.utils.logging_setup import configure_logging
@@ -28,11 +22,8 @@ def main(argv: list[str] | None = None) -> int:
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)
bus = EventBus()
executor = ActionExecutor(db, event_bus=bus)
settings, db, service, executor = build_sync_stack(args.db, event_bus=bus)
seen_errors: set[str] = set()
@@ -73,39 +64,52 @@ def main(argv: list[str] | None = None) -> int:
bus.subscribe("RenameApplied", on_rename)
bus.subscribe("FileRecycled", on_recycle)
playlists = settings.playlists
selected_playlists = settings.playlists
if args.playlist is not None:
playlists = [playlists[args.playlist]] if 0 <= args.playlist < len(playlists) else []
selected_playlists = [selected_playlists[args.playlist]] if 0 <= args.playlist < len(selected_playlists) else []
for pl in playlists:
url = pl.get("url")
pid = extract_playlist_id(url) or (url or "")
try:
actions = service.sync_from_config(pl)
except ImportError as e:
msg = str(e)
if "yt_dlp" in msg or "yt-dlp" in msg:
print("yt-dlp Python package is required. Install with: pip install -U yt-dlp")
return 2
raise
counts: dict[str, int] = {}
for a in actions:
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))
def on_plan(pl: dict, playlist_id: str, actions, counts: dict[str, int]) -> None:
summary = format_action_summary(counts)
print(f"Playlist {playlist_id}: {len(actions)} actions → {summary}")
log.info("playlist=%s actions=%s summary=%s", playlist_id, len(actions), summary)
return 0
def on_no_actions(pl: dict, playlist_id: str) -> None:
del pl
print(f"Playlist {playlist_id}: 0 actions →")
log.info("playlist=%s actions=0 summary=", playlist_id)
def on_applied(pl: dict, playlist_id: str) -> None:
del pl
print(f"Applied actions for {playlist_id}.")
log.info("playlist=%s applied_actions=done", playlist_id)
def on_import_error(pl: dict, exc: Exception) -> bool:
del pl
msg = str(exc)
if "yt_dlp" in msg or "yt-dlp" in msg:
print("yt-dlp Python package is required. Install with: pip install -U yt-dlp")
else:
print(f"ERROR: {exc}")
return False
def on_dependency_error(pl: dict, exc: Exception) -> bool:
del pl
print(f"ERROR: {exc}")
log.error("dependency error: %s", exc)
return False
return run_sync_batch(
selected_playlists,
db=db,
service=service,
executor=executor,
apply=bool(args.apply),
on_plan=on_plan,
on_no_actions=on_no_actions,
on_applied=on_applied,
on_import_error=on_import_error,
on_dependency_error=on_dependency_error,
)
if __name__ == "__main__":
+2 -2
View File
@@ -52,7 +52,7 @@ class QueueManager:
async def start(self, worker_coro):
"""Start the worker tasks that drain the queue."""
async def runner(idx: int):
async def runner():
while not self._stopped.is_set():
job = await self._queue.get()
try:
@@ -60,7 +60,7 @@ class QueueManager:
finally:
self._queue.task_done()
self._workers = [asyncio.create_task(runner(i)) for i in range(self._concurrency)]
self._workers = [asyncio.create_task(runner()) for _ in range(self._concurrency)]
async def stop(self):
"""Cancel all worker tasks and mark the queue as stopped."""
-3
View File
@@ -16,9 +16,6 @@ class PlaylistScanner:
still start in environments where yt-dlp is unavailable.
"""
def __init__(self) -> None:
pass
def scan(self, playlist_url: str, playlist_id: str, *, ffmpeg_path: Optional[str] = None) -> List[PlaylistItem]:
"""Return the current remote playlist entries as `PlaylistItem` records."""
try:
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Any, Callable, Sequence
from ..database.db import Database
from ..events.event_bus import EventBus
from ..models import SyncAction
from ..utils.deps import DependencyError
from ..utils.yt import extract_playlist_id
from .executor import ActionExecutor
from .service import SyncService
from ...config.settings import Settings
def build_sync_stack(db_path: Path | None = None, *, event_bus: EventBus | None = None) -> tuple[Settings, Database, SyncService, ActionExecutor]:
settings = Settings()
db = Database((db_path or Path("db/app.db")).resolve())
service = SyncService(db)
executor = ActionExecutor(db, event_bus=event_bus)
return settings, db, service, executor
def format_action_summary(counts: dict[str, int]) -> str:
return ", ".join(f"{name}:{count}" for name, count in sorted(counts.items()))
def run_sync_batch(
playlists: Sequence[dict[str, Any]],
*,
db: Database,
service: SyncService,
executor: ActionExecutor,
apply: bool,
on_plan: Callable[[dict[str, Any], str, list[SyncAction], dict[str, int]], None] | None = None,
on_no_actions: Callable[[dict[str, Any], str], None] | None = None,
on_applied: Callable[[dict[str, Any], str], None] | None = None,
on_import_error: Callable[[dict[str, Any], Exception], bool] | None = None,
on_dependency_error: Callable[[dict[str, Any], Exception], bool] | None = None,
) -> int:
for playlist_cfg in playlists:
playlist_url = str(playlist_cfg.get("url") or "")
playlist_id = extract_playlist_id(playlist_url) or playlist_url
try:
actions = service.sync_from_config(playlist_cfg)
except ImportError as exc:
if on_import_error is not None and on_import_error(playlist_cfg, exc):
continue
return 2
counts: dict[str, int] = {}
for action in actions:
counts[action.type.name] = counts.get(action.type.name, 0) + 1
if on_plan is not None:
on_plan(playlist_cfg, playlist_id, actions, counts)
if apply and actions:
try:
asyncio.run(executor.execute(actions, playlist_cfg))
except DependencyError as exc:
if on_dependency_error is not None and on_dependency_error(playlist_cfg, exc):
continue
return 2
db.set_playlist_last_sync(playlist_id)
if on_applied is not None:
on_applied(playlist_cfg, playlist_id)
elif on_no_actions is not None:
on_no_actions(playlist_cfg, playlist_id)
return 0
+36 -38
View File
@@ -7,50 +7,48 @@ Future iterations will wire up scheduler and a GUI.
from __future__ import annotations
import asyncio
from pathlib import Path
from .config.settings import Settings
from .core.database.db import Database
from .core.sync.service import SyncService
from .core.sync.executor import ActionExecutor
from .core.utils.yt import extract_playlist_id
from .core.utils.deps import DependencyError
from .core.sync.runner import build_sync_stack, format_action_summary, run_sync_batch
def bootstrap(db_path: Path | None = None) -> None:
settings = Settings()
db = Database((db_path or Path("db/app.db")).resolve())
service = SyncService(db)
executor = ActionExecutor(db)
settings, db, service, executor = build_sync_stack(db_path)
# Iterate configured playlists and compute actions (no execution yet)
for pl in settings.playlists:
try:
actions = service.sync_from_config(pl)
# Apply actions now
if actions:
print(f"Applying {len(actions)} actions for: {pl.get('url')}")
# Summarize before applying
counts = {}
for a in actions:
counts[a.type] = counts.get(a.type, 0) + 1
summary = ", ".join(f"{k.name}:{v}" for k, v in counts.items())
print(f"Plan → {summary}")
# Execute
try:
asyncio.run(executor.execute(actions, pl))
except DependencyError as e:
print(f"ERROR: {e}")
continue
# Post summary (no DB readback yet)
pid = extract_playlist_id(pl.get('url', '')) or pl.get('url', '')
db.set_playlist_last_sync(pid)
print("Applied actions.")
else:
print(f"No actions needed for: {pl.get('url')}")
except Exception as exc: # keep bootstrap resilient during early dev
print(f"Failed to sync playlist {pl.get('url')}: {exc}")
def on_plan(pl: dict, playlist_id: str, actions, counts: dict[str, int]) -> None:
del playlist_id
print(f"Applying {len(actions)} actions for: {pl.get('url')}")
print(f"Plan → {format_action_summary(counts)}")
def on_no_actions(pl: dict, playlist_id: str) -> None:
del playlist_id
print(f"No actions needed for: {pl.get('url')}")
def on_applied(pl: dict, playlist_id: str) -> None:
del pl, playlist_id
print("Applied actions.")
def on_import_error(pl: dict, exc: Exception) -> bool:
print(f"Failed to sync playlist {pl.get('url')}: {exc}")
return True
def on_dependency_error(pl: dict, exc: Exception) -> bool:
del pl
print(f"ERROR: {exc}")
return True
run_sync_batch(
settings.playlists,
db=db,
service=service,
executor=executor,
apply=True,
on_plan=on_plan,
on_no_actions=on_no_actions,
on_applied=on_applied,
on_import_error=on_import_error,
on_dependency_error=on_dependency_error,
)
if __name__ == "__main__":