feat: Implement sync manager and file watcher for automated playlist synchronization
This commit is contained in:
+87
-2
@@ -4,7 +4,8 @@ from typing import Sequence
|
||||
|
||||
from fastapi import FastAPI, Form, HTTPException, Query, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
import asyncio
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -15,11 +16,13 @@ from app.utils.logger import logger
|
||||
from app.utils.playlist_merge import SyncMode, TEST_PLAYLIST_DIR, sync_all_playlists
|
||||
from app.utils.plex_client import plex_client
|
||||
from app.utils.scheduler import start_scheduler, update_scheduler_job, get_next_run_time, validate_cron_expression
|
||||
from app.utils.sync_manager import sync_manager
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
sync_manager.set_event_loop(asyncio.get_running_loop())
|
||||
start_scheduler()
|
||||
|
||||
app.add_middleware(
|
||||
@@ -450,6 +453,75 @@ async def api_playlists(server: str = Query(..., pattern="(?i)^(local|cloud)$"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/sync/status")
|
||||
async def get_sync_status():
|
||||
return sync_manager.status
|
||||
|
||||
@app.get("/api/sync/events")
|
||||
async def sync_events(request: Request):
|
||||
async def event_generator():
|
||||
q = await sync_manager.subscribe()
|
||||
try:
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
data = await q.get()
|
||||
yield f"data: {data}\n\n"
|
||||
finally:
|
||||
sync_manager.unsubscribe(q)
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
@app.post("/api/sync")
|
||||
async def api_sync(payload: SyncRequest):
|
||||
server_config.load()
|
||||
try:
|
||||
sync_mode = payload.mode or SyncMode(server_config.sync_mode)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
# Update config temporarily for this sync if needed, but sync_manager reads from config.
|
||||
# If payload overrides config, we might need to handle that.
|
||||
# However, sync_manager._perform_sync reads from server_config.
|
||||
# If we want to support one-off sync with custom params via sync_manager, we need to update sync_manager.
|
||||
|
||||
# For now, let's assume payload params should be saved or used.
|
||||
# But sync_manager is designed to run background tasks too.
|
||||
|
||||
# If we want to keep the existing behavior of api_sync (blocking and returning stats),
|
||||
# we can use sync_manager.run_sync(wait=True).
|
||||
# But we need to make sure sync_manager uses the params from payload if provided.
|
||||
|
||||
# Since sync_manager reads from server_config, let's update server_config if payload has values.
|
||||
# Or better, pass params to sync_manager.run_sync?
|
||||
# sync_manager._perform_sync currently hardcodes reading from server_config.
|
||||
|
||||
# Let's stick to the requirement: "watchdog当发现更改时,执行同步,同步时UI页面也会显示正在同步状态。"
|
||||
# This implies we need a shared state.
|
||||
|
||||
# If I change api_sync to use sync_manager, I need to ensure it supports the custom params.
|
||||
# But payload.local_path and payload.mode are optional.
|
||||
|
||||
# Let's modify sync_manager to accept overrides.
|
||||
# But wait, sync_manager is a singleton.
|
||||
|
||||
# For this task, I will just wrap the existing logic in sync_manager.run_sync(wait=True)
|
||||
# AND I will modify sync_manager to allow passing explicit args to _perform_sync.
|
||||
|
||||
# But first, let's update api_sync to use sync_manager.run_sync(wait=True)
|
||||
# AND we need to handle the parameter passing.
|
||||
|
||||
# Actually, looking at sync_manager implementation I just wrote:
|
||||
# def _perform_sync(self):
|
||||
# server_config.load()
|
||||
# return sync_all_playlists(local_dir=server_config.local_path, mode=SyncMode(server_config.sync_mode))
|
||||
|
||||
# It ignores arguments. This is a limitation.
|
||||
# I should update SyncManager to accept kwargs for sync_all_playlists.
|
||||
|
||||
# Let's update SyncManager first.
|
||||
pass
|
||||
|
||||
@app.post("/api/sync")
|
||||
async def api_sync(payload: SyncRequest):
|
||||
server_config.load()
|
||||
@@ -459,7 +531,20 @@ async def api_sync(payload: SyncRequest):
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
local_dir = payload.local_path or server_config.local_path
|
||||
results = sync_all_playlists(local_dir=local_dir, mode=sync_mode, test_folder=TEST_PLAYLIST_DIR)
|
||||
|
||||
# Use sync_manager to execute sync, ensuring state is updated
|
||||
try:
|
||||
results = sync_manager.run_sync(
|
||||
trigger_source="api",
|
||||
wait=True,
|
||||
# We need to pass these to _perform_sync
|
||||
sync_kwargs={"local_dir": local_dir, "mode": sync_mode}
|
||||
)
|
||||
except Exception as e:
|
||||
if str(e) == "Sync already in progress":
|
||||
raise HTTPException(status_code=409, detail="Sync already in progress")
|
||||
raise e
|
||||
|
||||
merged_count = sum(len(item.merged_paths) for item in results)
|
||||
conflict_count = sum(len(item.conflicts) for item in results)
|
||||
deleted_count = sum(1 for item in results if item.action == "deleted")
|
||||
|
||||
Reference in New Issue
Block a user