fix(media): verify read the library the way the servers write it #103
No files matched your search
+19
-6
@@ -9,8 +9,13 @@ its destination, and NO row may still name its source — a source row is a scan
|
|||||||
that has not caught up, and it is the one signature of a library pointing at
|
that has not caught up, and it is the one signature of a library pointing at
|
||||||
files that no longer exist.
|
files that no longer exist.
|
||||||
|
|
||||||
Two doors, both read-only, both without a lock while the server runs
|
Two doors, both read-only, both without a lock while the server runs (there is
|
||||||
(`mode=ro&immutable=1`; there is no `sqlite3` binary on these hosts):
|
no `sqlite3` binary on these hosts). Open `mode=ro` PLAIN: both servers keep
|
||||||
|
their libraries in WAL mode, and `immutable=1` — which claims the file never
|
||||||
|
changes — makes sqlite skip the write-ahead log entirely, so every row the
|
||||||
|
server has committed but not yet checkpointed reads as MISSING, and a read
|
||||||
|
beside a running scanner can even fail as "database disk image is malformed".
|
||||||
|
`immutable=1` is only the fallback, for a database whose `-shm` cannot be read.
|
||||||
|
|
||||||
jellyfin `BaseItems.Path`. The container binds the share at `/data`, so the
|
jellyfin `BaseItems.Path`. The container binds the share at `/data`, so the
|
||||||
database holds `/data/<library>/…` where the journal holds
|
database holds `/data/<library>/…` where the journal holds
|
||||||
@@ -92,10 +97,18 @@ def read_moves(path):
|
|||||||
def server_paths(db, kind):
|
def server_paths(db, kind):
|
||||||
"""Every path the server holds, or a refusal that names what is missing."""
|
"""Every path the server holds, or a refusal that names what is missing."""
|
||||||
spec = KINDS[kind]
|
spec = KINDS[kind]
|
||||||
try:
|
con = None
|
||||||
con = sqlite3.connect(f"file:{db}?mode=ro&immutable=1", uri=True)
|
err = None
|
||||||
except sqlite3.Error as e:
|
# Plain ro reads the WAL the server commits through; immutable is the last
|
||||||
die(f"cannot open {db}: {e}")
|
# resort, because it cannot see a row the server has not checkpointed.
|
||||||
|
for opt in ("mode=ro", "mode=ro&immutable=1"):
|
||||||
|
try:
|
||||||
|
con = sqlite3.connect(f"file:{db}?{opt}", uri=True)
|
||||||
|
break
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
err = e
|
||||||
|
if con is None:
|
||||||
|
die(f"cannot open {db}: {err}")
|
||||||
with con:
|
with con:
|
||||||
have = {r[1] for r in con.execute(f"pragma table_info({spec['table']})")}
|
have = {r[1] for r in con.execute(f"pragma table_info({spec['table']})")}
|
||||||
if not spec["columns"] <= have:
|
if not spec["columns"] <= have:
|
||||||
|
|||||||
@@ -210,3 +210,38 @@ setup() {
|
|||||||
[ "${lines[3]}" = "missing from the DB: 0" ]
|
[ "${lines[3]}" = "missing from the DB: 0" ]
|
||||||
[ "${lines[4]}" = "not indexed (extension ignored): 1" ]
|
[ "${lines[4]}" = "not indexed (extension ignored): 1" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# wal_case <db> — a Jellyfin database whose NEW row lives only in the
|
||||||
|
# write-ahead log: the server has committed the rename but not checkpointed it,
|
||||||
|
# which is the normal state of a library a scan is writing. The holder
|
||||||
|
# connection keeps the WAL from folding into the main file, and the verifier
|
||||||
|
# runs as a real subprocess so it faces the same files Plex and Jellyfin leave.
|
||||||
|
wal_case() {
|
||||||
|
python3 - "$JFDB" "$JOURNAL" "$SHARE" "$VERIFY" <<'PY'
|
||||||
|
import sqlite3, subprocess, sys
|
||||||
|
db, journal, share, verify = sys.argv[1:5]
|
||||||
|
w = sqlite3.connect(db)
|
||||||
|
w.execute("pragma journal_mode=wal")
|
||||||
|
w.execute("pragma wal_autocheckpoint=0")
|
||||||
|
w.execute("insert into BaseItems (Path, Type) values ('/data/Movies/Old.mkv', 'x')")
|
||||||
|
w.commit()
|
||||||
|
w.execute("pragma wal_checkpoint(truncate)")
|
||||||
|
holder = sqlite3.connect(db) # last-connection close would checkpoint
|
||||||
|
w.execute("update BaseItems set Path='/data/Movies/Old/Old.mkv'")
|
||||||
|
w.commit()
|
||||||
|
r = subprocess.run([sys.executable, verify, "--journal", journal,
|
||||||
|
"--db-kind", "jellyfin", "--db", db,
|
||||||
|
"--share", share, "--container", "/data"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
print(r.stdout, end="")
|
||||||
|
sys.exit(r.returncode)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "a row the server committed but has not checkpointed is proof, not a miss" {
|
||||||
|
move_row "$SHARE/Movies/Old.mkv" "$SHARE/Movies/Old/Old.mkv"
|
||||||
|
run wal_case
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "${lines[1]}" = "landed: 1" ]
|
||||||
|
[ "${lines[3]}" = "missing from the DB: 0" ]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user