Merge: promote dev to prod
This commit is contained in:
commit
6140be18fc
5 changed files with 52 additions and 19 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.34.0
|
||||
0.34.1
|
||||
|
|
@ -16,6 +16,8 @@ history + onDeck, last-write-wins) build on this and land in later ships.
|
|||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import sysconfig
|
||||
|
|
@ -67,13 +69,12 @@ def import_owner_watch_state(db: Session, link: PlexLink) -> dict:
|
|||
item_id_by_rk: dict[str, int] = {
|
||||
rk: iid for rk, iid in db.query(PlexItem.rating_key, PlexItem.id)
|
||||
}
|
||||
# This user's existing states, by item — so we upsert rather than duplicate.
|
||||
existing: dict[int, PlexState] = {
|
||||
st.item_id: st
|
||||
for st in db.query(PlexState).filter_by(user_id=link.user_id)
|
||||
}
|
||||
stats = {"watched": 0, "in_progress": 0, "scanned": 0}
|
||||
wanted = _enabled_section_keys(db)
|
||||
# Accumulate one row per item (keyed by item_id → inherently de-duped) then bulk-UPSERT. An upsert
|
||||
# is idempotent AND concurrency-safe: a repeated/overlapping enable (the toggle firing twice while
|
||||
# the multi-second import runs) can't raise a duplicate-key error — the conflict just re-updates.
|
||||
rows_by_item: dict[int, dict] = {}
|
||||
|
||||
try:
|
||||
with PlexClient(db) as plex:
|
||||
|
|
@ -98,18 +99,35 @@ def import_owner_watch_state(db: Session, link: PlexLink) -> dict:
|
|||
if res is None:
|
||||
continue
|
||||
status, pos, watched_at, prog_at = res
|
||||
st = existing.get(item_id)
|
||||
if st is None:
|
||||
st = PlexState(user_id=link.user_id, item_id=item_id)
|
||||
db.add(st)
|
||||
existing[item_id] = st
|
||||
st.status = status
|
||||
st.position_seconds = pos
|
||||
st.watched_at = watched_at
|
||||
st.progress_updated_at = prog_at
|
||||
# This state now mirrors Plex — mark it so Phase B's push doesn't bounce it back.
|
||||
st.synced_to_plex = True
|
||||
rows_by_item[item_id] = {
|
||||
"user_id": link.user_id,
|
||||
"item_id": item_id,
|
||||
"status": status,
|
||||
"position_seconds": pos,
|
||||
"watched_at": watched_at,
|
||||
"progress_updated_at": prog_at,
|
||||
# Mirrors Plex now — so Phase B's push doesn't bounce it straight back.
|
||||
"synced_to_plex": True,
|
||||
}
|
||||
stats["watched" if status == "watched" else "in_progress"] += 1
|
||||
|
||||
# Bulk upsert in chunks. Plex wins on the intersection; Siftlode-only states (item_ids not
|
||||
# in rows_by_item) are never referenced, so they're preserved untouched (union).
|
||||
rows = list(rows_by_item.values())
|
||||
for i in range(0, len(rows), 1000):
|
||||
stmt = pg_insert(PlexState).values(rows[i : i + 1000])
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
index_elements=[PlexState.user_id, PlexState.item_id],
|
||||
set_={
|
||||
"status": stmt.excluded.status,
|
||||
"position_seconds": stmt.excluded.position_seconds,
|
||||
"watched_at": stmt.excluded.watched_at,
|
||||
"progress_updated_at": stmt.excluded.progress_updated_at,
|
||||
"synced_to_plex": stmt.excluded.synced_to_plex,
|
||||
"updated_at": func.now(),
|
||||
},
|
||||
)
|
||||
db.execute(stmt)
|
||||
link.initial_import_done = True
|
||||
link.last_watch_sync_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -469,7 +469,11 @@ function PlexWatchSync() {
|
|||
<Section title={t("settings.plexSync.title")}>
|
||||
<p className="text-xs text-muted leading-relaxed mb-2">{t("settings.plexSync.intro")}</p>
|
||||
<SettingRow label={t("settings.plexSync.toggle")} hint={t("settings.plexSync.toggleHint")}>
|
||||
<Switch checked={enabled} onChange={(v) => toggle.mutate(v)} />
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={toggle.isPending || reimport.isPending}
|
||||
onChange={(v) => !toggle.isPending && toggle.mutate(v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{enabled && (
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ export function Switch({
|
|||
checked,
|
||||
onChange,
|
||||
label,
|
||||
disabled,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -21,8 +23,9 @@ export function Switch({
|
|||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={label}
|
||||
disabled={disabled}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`w-9 h-5 rounded-full transition relative shrink-0 ${checked ? "bg-accent" : "bg-border"}`}
|
||||
className={`w-9 h-5 rounded-full transition relative shrink-0 disabled:opacity-50 disabled:cursor-not-allowed ${checked ? "bg-accent" : "bg-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all ${
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ export interface ReleaseEntry {
|
|||
}
|
||||
|
||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||
{
|
||||
version: "0.34.1",
|
||||
date: "2026-07-09",
|
||||
summary: "Fix: turning on Plex watch sync could show a server error.",
|
||||
fixes: [
|
||||
"Enabling Plex watch sync could briefly show a \"Server error\" if the switch was toggled again while the first import was still running. The import is now safe to run more than once, and the switch is disabled while it works. (If you saw the error, your Plex history still imported correctly.)",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.34.0",
|
||||
date: "2026-07-09",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue