diff --git a/unwind/db.py b/unwind/db.py index f93db6e..bb3e6f0 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -9,6 +9,7 @@ from databases import Database from . import config from .models import ( Movie, + Progress, Rating, User, asplain, @@ -125,34 +126,32 @@ async def apply_db_patches(db: Database): await db.execute("vacuum") -async def get_import_progress() -> float: - """Return the current import progress percentage. - - The progress will be a value from 0 to 100. If the value is 100, the last - import was completed successfully and no import is currently running. - """ - query = "SELECT state FROM progress WHERE id='import_imdb'" - state = await shared_connection().fetch_val(query) - return float(state or 100) +async def get_import_progress() -> Optional[Progress]: + """Return the latest import progress.""" + return await get(Progress, type="import-imdb-movies", order_by="started DESC") -async def set_import_progress(progress: float): +async def set_import_progress(progress: float) -> Progress: """Set the current import progress percentage.""" progress = min(max(0.0, progress), 100.0) # clamp to 0 <= progress <= 100 - values = {"id": "import_imdb"} + current = await get_import_progress() + is_update = current and current.stopped is None + + if not current or not is_update: + current = Progress(type="import-imdb-movies") + + current.state = f"{progress:.2f}" if progress >= 100: - query = "DELETE FROM progress WHERE id=:id" - else: - query = """ - INSERT INTO progress(id, state, started) - VALUES (:id, :state, :started) - ON CONFLICT DO UPDATE SET state=excluded.state - """ - values.update({"state": f"{progress:.2f}", "started": utcnow().isoformat()}) + current.stopped = utcnow().isoformat() - await shared_connection().execute(query=query, values=values) + if is_update: + await update(current) + else: + await add(current) + + return current def shared_connection() -> Database: @@ -183,7 +182,9 @@ async def add(item): ModelType = TypeVar("ModelType") -async def get(model: Type[ModelType], **kwds) -> Optional[ModelType]: +async def get( + model: Type[ModelType], *, order_by: str = None, **kwds +) -> Optional[ModelType]: values = {k: v for k, v in kwds.items() if v is not None} if not values: return @@ -191,6 +192,8 @@ async def get(model: Type[ModelType], **kwds) -> Optional[ModelType]: fields_ = ", ".join(f.name for f in fields(model)) cond = " AND ".join(f"{k}=:{k}" for k in values) query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" + if order_by: + query += f" ORDER BY {order_by}" row = await shared_connection().fetch_one(query=query, values=values) return fromplain(model, row) if row else None diff --git a/unwind/models.py b/unwind/models.py index b3aae26..125ef1f 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -140,6 +140,17 @@ def utcnow(): return datetime.now().replace(tzinfo=timezone.utc) +@dataclass +class Progress: + _table: ClassVar[str] = "progress" + + id: ULID = field(default_factory=ULID) + type: str = None + state: str = None + started: datetime = field(default_factory=utcnow) + stopped: Optional[str] = None + + @dataclass class Movie: _table: ClassVar[str] = "movies" diff --git a/unwind/sql/20210720-223416.sql b/unwind/sql/20210720-223416.sql new file mode 100644 index 0000000..95e1b78 --- /dev/null +++ b/unwind/sql/20210720-223416.sql @@ -0,0 +1,24 @@ +-- add IMDb vote count + +CREATE TABLE _migrate_progress ( + id TEXT PRIMARY KEY NOT NULL, + type TEXT NOT NULL, + state TEXT NOT NULL, + started TEXT NOT NULL, + stopped TEXT +);; + +INSERT INTO _migrate_progress +SELECT + id, + 'import-imdb-movies' AS type, + state, + started, + NULL AS stopped +FROM progress +WHERE true;; + +DROP TABLE progress;; + +ALTER TABLE _migrate_progress +RENAME TO progress;; diff --git a/unwind/web.py b/unwind/web.py index 367dc68..edc9924 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -23,7 +23,7 @@ from starlette.routing import Mount, Route from . import config, db, imdb, imdb_import from .db import close_connection_pool, find_ratings, open_connection_pool from .middleware.responsetime import ResponseTimeMiddleware -from .models import Group, Movie, User, asplain +from .models import Group, Movie, Progress, User, asplain from .types import ULID from .utils import b64encode, phc_compare, phc_scrypt @@ -198,15 +198,38 @@ async def add_movie(request): import_lock = asyncio.Lock() +@route("/movies/_reload_imdb", methods=["GET"]) +@requires(["authenticated", "admin"]) +async def progress_for_load_imdb_movies(request): + progress = await db.get_import_progress() + if not progress: + return JSONResponse({"status": "No import exists."}, status_code=404) + + p = asplain(progress) + percent = float(p["state"]) + + return JSONResponse( + { + "status": "Import is running." if percent < 100 else "Import finished.", + "progress": percent, + "started": p["started"], + "stopped": p["stopped"], + } + ) + + @route("/movies/_reload_imdb", methods=["POST"]) @requires(["authenticated", "admin"]) async def load_imdb_movies(request): async with import_lock: progress = await db.get_import_progress() - if progress < 100: - return JSONResponse( - {"status": "Import running.", "progress": progress}, status_code=409 - ) + if progress: + percent = float(progress.state) + if percent < 100: + return JSONResponse( + {"status": "Import is running.", "progress": percent}, + status_code=409, + ) await db.set_import_progress(0)