mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-09-18 20:43:54 +03:00
refactor: consolidate sync entrypoints and trim dead GUI/util code
This commit is contained in:
+45
-41
@@ -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__":
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user