mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:07:03 +01:00
New package. Walks every thread on 4chan's /wsg/, downloads each video it has
not seen, and reviews them one at a time: mpv embedded in a Tk window via --wid
with Keep/Pass/Undo directly beneath the picture.
Dedupe is structural rather than a convention. Every post carrying a file also
carries md5 -- base64 MD5 of the whole file -- so it is the sqlite primary key.
An already-seen video costs zero bytes to skip and a byte-identical repost in
another thread collapses onto the existing row. Verified against a live file:
the API's Nz9OEKdMuMZEdYE6eLTKmA== matched base64(md5(bytes)) exactly.
Undo is unbounded and survives quitting: the verdict trail lives in the index,
not in memory, so yesterday's decisions can still be taken back.
No downloaded video is ever deleted. A pass moves the file to trash/ and leaves
it there; files.apply_move refuses to overwrite an existing destination, so the
guarantee holds by construction rather than per caller.
Three bugs only running it could find, each caught from evidence rather than
guesswork:
- the video stayed black: a <Configure> handler set mpv's geometry, fighting
the automatic sizing --wid already does
- playback never advanced: refresh() ran every tick and re-sent loadfile ~7x
a second, restarting the video continuously
- it froze on one video around the 26th while filenames kept advancing: mpv
pushes an event stream down the same socket and nothing drained it, so the
buffer filled and mpv stopped reading commands. Isolated to command #35
without a drain thread; all 60 honoured with one.
A five-lens review then found nine more, all reproduced before fixing: a failed
mpv start stranded the non-daemon worker forever; undo could clobber a file in
incoming/; a NUL-bearing md5 poisoned the index permanently; an oversized int
killed the worker with no event; gzip bombs on both hosts were accepted; the
board endpoints were fetched unconditionally forever; and Session.missing could
corrupt the undo trail.
mpv runs as a subprocess over its JSON IPC socket rather than via python-mpv,
which resolves libmpv at import time and would fail on CI while passing here.
Third-party surface is requests alone, already declared.
277 package tests, 100% branch coverage on all 22 modules, no suppressions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
158 lines
4.7 KiB
Python
158 lines
4.7 KiB
Python
"""Bookkeeping for the ``threads`` table.
|
|
|
|
Split out of :mod:`python_pkg.wsg_grabber.store` to stay under this repo's
|
|
500-line-per-file cap. The seam is the natural one: this module owns which
|
|
threads have been visited and what their last ``Last-Modified`` was, which is
|
|
what lets an unchanged thread cost a 304 rather than a re-parse.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
import sqlite3
|
|
|
|
from python_pkg.wsg_grabber.models import ThreadRef
|
|
|
|
|
|
def _now() -> str:
|
|
"""Return the current UTC time as an ISO-8601 string.
|
|
|
|
Returns:
|
|
str: Timestamp suitable for the index's text date columns.
|
|
"""
|
|
return datetime.now(tz=timezone.utc).isoformat()
|
|
|
|
|
|
def known_threads(conn: sqlite3.Connection) -> dict[int, int]:
|
|
"""Return each known thread's last-modified stamp.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
|
|
Returns:
|
|
dict[int, int]: Thread number mapped to its stored API timestamp.
|
|
"""
|
|
return {
|
|
int(row["thread_no"]): int(row["api_last_mod"])
|
|
for row in conn.execute("SELECT thread_no, api_last_mod FROM threads")
|
|
}
|
|
|
|
|
|
def http_last_modified(conn: sqlite3.Connection, thread_no: int) -> str | None:
|
|
"""Return the stored ``Last-Modified`` header for a thread.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
thread_no: Thread identifier.
|
|
|
|
Returns:
|
|
str | None: Header value to send back as ``If-Modified-Since``.
|
|
"""
|
|
row = conn.execute(
|
|
"SELECT http_last_mod FROM threads WHERE thread_no = ?",
|
|
(thread_no,),
|
|
).fetchone()
|
|
if row is None or row["http_last_mod"] is None:
|
|
return None
|
|
return str(row["http_last_mod"])
|
|
|
|
|
|
def record_thread(
|
|
conn: sqlite3.Connection,
|
|
ref: ThreadRef,
|
|
header: str | None,
|
|
status: str = "live",
|
|
) -> None:
|
|
"""Insert or update a thread's bookkeeping row.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
ref: Thread number and its API timestamp.
|
|
header: ``Last-Modified`` header from the thread fetch, if any.
|
|
status: ``live`` or ``gone``.
|
|
"""
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO threads (thread_no, api_last_mod, http_last_mod,
|
|
last_checked, status)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(thread_no) DO UPDATE SET
|
|
api_last_mod = excluded.api_last_mod,
|
|
http_last_mod = COALESCE(excluded.http_last_mod, threads.http_last_mod),
|
|
last_checked = excluded.last_checked,
|
|
status = excluded.status
|
|
""",
|
|
(ref.thread_no, ref.api_last_modified, header, _now(), status),
|
|
)
|
|
|
|
|
|
def mark_thread_gone(conn: sqlite3.Connection, thread_no: int) -> None:
|
|
"""Flag a thread as no longer fetchable.
|
|
|
|
This inserts rather than merely updating: a thread can 404 the very first
|
|
time it is seen, and a plain UPDATE would touch no rows, leaving nothing to
|
|
record that it is dead. It would then be re-fetched on every future run.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
thread_no: Thread identifier.
|
|
"""
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO threads (thread_no, api_last_mod, http_last_mod,
|
|
last_checked, status)
|
|
VALUES (?, 0, NULL, ?, 'gone')
|
|
ON CONFLICT(thread_no) DO UPDATE SET
|
|
status = 'gone',
|
|
last_checked = excluded.last_checked
|
|
""",
|
|
(thread_no, _now()),
|
|
)
|
|
|
|
|
|
def cached_last_modified(conn: sqlite3.Connection, url: str) -> str | None:
|
|
"""Return the stored ``Last-Modified`` for a board-level endpoint.
|
|
|
|
``threads.json`` and ``archive.json`` are not tied to any one thread, so
|
|
their stamps live here rather than in the threads table. Without this the
|
|
reviewer re-fetches both unconditionally on every idle cycle, forever.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
url: Absolute API URL.
|
|
|
|
Returns:
|
|
str | None: Header value to send back as ``If-Modified-Since``.
|
|
"""
|
|
row = conn.execute(
|
|
"SELECT last_modified FROM http_cache WHERE url = ?",
|
|
(url,),
|
|
).fetchone()
|
|
return None if row is None else str(row["last_modified"])
|
|
|
|
|
|
def record_last_modified(
|
|
conn: sqlite3.Connection,
|
|
url: str,
|
|
stamp: str | None,
|
|
) -> None:
|
|
"""Store the ``Last-Modified`` a board-level endpoint returned.
|
|
|
|
Args:
|
|
conn: Open connection.
|
|
url: Absolute API URL.
|
|
stamp: Header value; ignored when the server did not send one.
|
|
"""
|
|
if stamp is None:
|
|
return
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO http_cache (url, last_modified) VALUES (?, ?)
|
|
ON CONFLICT(url) DO UPDATE SET last_modified = excluded.last_modified
|
|
""",
|
|
(url, stamp),
|
|
)
|