storage: JSON files -> SQLite (users/history/searches/events)

- one-time import from the legacy JSON (left in place as snapshot);
  ghost rows (int vs str ep_id) deduped on import, unrepresentable after
- history POST is now an upsert: the (user, slug_id, ep_id) PK is the
  merge key, so the unguarded read-modify-write (lost updates) and the
  corruption-swallow (JSONDecodeError -> [] -> fresh overwrite) are gone
- searches: NOCASE PK upsert + cap 8
- events table + position/search logging (watch-time trajectory),
  90-day retention pruned at boot
- account delete cascades all four tables
- 22/22 tests green; old JSON files untouched
This commit is contained in:
jozamudi committed 2026-08-24 16:02:27 +00:00
1 parent 2c0ee8eae3
commit 202bfa41c3
5 files changed
+222 -105

No files matched your search

+17 -6
View File
@@ -96,7 +96,7 @@ def test_no_self_signup(base=BASE):
jar = login("admin", "adminpw12345", base)
assert req("POST", "/signup", {"username": "zed", "password": "whatever123"},
jar=jar, base=base) == 405
assert "zed" not in json.load(open(_users_file()))
assert "zed" not in _users()
def test_history_requires_ep_id(base=BASE):
@@ -172,8 +172,8 @@ def test_self_delete(base=BASE):
# give dave a searches file too: account delete must remove it
assert req("POST", "/api/searches", {"q": "dave-search"}, jar=jar, base=base) == 200
assert req("POST", "/account/delete", jar=jar, base=base) == 200
assert "dave" not in json.load(open(_users_file()))
assert not os.path.exists(os.path.join(os.environ["ANIWEB_DATA"], "searches", "dave.json"))
assert "dave" not in _users()
assert _search_count("dave") == 0 # account delete cascades the rows
# old session cookie is dead immediately (middleware re-checks existence)
assert req("GET", "/api/me", jar=jar, base=base) == 401
jar2 = CJ()
@@ -230,6 +230,17 @@ def _json(method, path, jar=None, base=BASE):
return json.loads(resp.read().decode())
def _users_file():
import os
return os.path.join(os.environ["ANIWEB_DATA"], "users.json")
def _users():
import sqlite3
c = sqlite3.connect(os.path.join(os.environ["ANIWEB_DATA"], "aniweb.db"))
names = [r[0] for r in c.execute("SELECT name FROM users")]
c.close()
return names
def _search_count(user):
import sqlite3
c = sqlite3.connect(os.path.join(os.environ["ANIWEB_DATA"], "aniweb.db"))
n = c.execute("SELECT COUNT(*) FROM searches WHERE user=?", (user,)).fetchone()[0]
c.close()
return n