summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 14:58:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 14:58:58 +0200
commit71b7a310ce938f072fe20f27eeeadd40685f1ad1 (patch)
tree75a7d93d58fda0e742dd9b1c94538b532c34464a /packages/meshbay-node/src/meshbay_node/daemon.py
parentb7733e812fadd6007976d262bd1d793572a36ba7 (diff)
downloadmeshbay-71b7a310ce938f072fe20f27eeeadd40685f1ad1.tar.gz
fix(node): correct TMDB movie matching, per-file overrides, rematch
A batch of wrong poster-grid matches found live on a real library (2026-08-29): a two-volume film's second part matched the first; a numbered sequel matched a same-year making-of documentary; several entries of one franchise matched a single early entry whose localized TMDB title is the franchise name; one matched nothing. One mechanism: _tmdb_search returned the first candidate query whose title-similarity ratio merely cleared 0.6, before alternative_title / the Roman-numeral variant was ever tried. Matching: - title_parse: fold guessit's volume/part number back into display_title so the parts of a multi-part film stay distinct in the query, the card and the override. - _tmdb_search: keep a strong PASS 1 fast path (ratio >= 0.85, one request), otherwise score every candidate query and pick the best. A year-exact rescue lifts a sub-0.6 top hit to the confidence floor only when TMDB's own year-filtered result lands exactly on the filename's year. No local re-ranking of any single result list; no tmdb.py change. Fix match / rematch: - _admin_exec_tmdb_override: a movie override touches its own file only (guessit gives a whole franchise one display_title); a show override still fans out. Corrected files are marked in media_cache.tmdb_override. - media_cache: tmdb_override table; clear_file_tmdb / clear_tmdb_matches drop auto-resolved matches while sparing manual corrections. - ops.rematch_video + `meshbay-node video rematch` (loopback endpoint + CLI verb): re-resolve a group's video matches after a matcher fix. file_tmdb is keyed by content hash and otherwise only pruned on deletion, so nothing dislodged a cached match before. - a rename now drops the stale auto match too (daemon _reenrich_renamed_video_entries). UI: - VideoDetailModal shows the source filename and resolved TMDB id; an unmatched poster gets a badge (3 new video.* i18n keys x 10 locales). So a wrong match can actually be identified before hitting Fix match. docs/mediacenter.md 10.1 records this and the V8-V13 follow-up backlog (show-branch ladder, year-aware _best_match, wider sequel_variants, the 0.6-0.85 extra calls, movie grid merge, per-card rematch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py38
1 files changed, 33 insertions, 5 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 779ad91..72813b6 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -448,6 +448,7 @@ class NodeDaemon:
# enricher (Pillow, not ffmpeg/mutagen). No credential, no
# third-party client to construct: EXIF is read locally.
self._photo_enricher = PhotoEnricher(self._media_cache)
+ self._state["media_cache"] = self._media_cache
log.info("Media cache opened: %s", media_cache_db)
# 5. Denylist
@@ -1218,6 +1219,14 @@ class NodeDaemon:
if old is None or (old.name == entry.name and old.path == entry.path):
continue
self._enriched_attempted.discard((indexer.group_id, entry.id))
+ # The rename re-derives the title (the whole point of this
+ # method), which can change the correct TMDB match — but
+ # file_tmdb is keyed by content hash, unchanged by a rename, so
+ # nothing else would ever dislodge the old name's match. A
+ # manual "Fix match" correction is kept (clear_file_tmdb skips
+ # anything in media_cache.tmdb_override).
+ if self._media_cache is not None:
+ await self._media_cache.clear_file_tmdb(entry.id)
await self._enrich_new_video_entries(indexer, updates)
async def _enrich_new_audio_entries(self, indexer: DirectoryIndexer, entries: list) -> None:
@@ -1576,21 +1585,22 @@ def main() -> None:
parser.add_argument("command", nargs="?",
choices=["init", "reset", "status", "ui", "gek-init",
"gek", "operator", "member", "group", "file",
- "denylist", "reload", "restart-daemon",
+ "video", "denylist", "reload", "restart-daemon",
"calibrate-argon2"],
help="init: provision config + keystore | reset: erase all "
"node state | status: node state and keys "
"| ui: print the admin UI URL | operator pair: pair a "
"browser with this node | member list|invite|revoke|unpin "
"| group list|add|remove | gek init|rotate | file list|rm "
- "| denylist show|clear | reload: re-read node.toml "
+ "| video rematch: re-resolve TMDB matches for a group's "
+ "videos | denylist show|clear | reload: re-read node.toml "
"(systemctl --user reload) | restart-daemon: restart "
"the systemd unit (systemctl --user restart) "
"| calibrate-argon2: benchmark")
parser.add_argument("subcommand", nargs="?",
help="'pair' for operator; list|invite|revoke|unpin for "
"member; list|add|remove for group; init|rotate for gek; "
- "list|rm for file; show|clear for denylist")
+ "list|rm for file; rematch for video; show|clear for denylist")
parser.add_argument("target", nargs="?",
help="username for member invite|revoke|unpin; group name "
"for group add; file id for file rm; identifier for "
@@ -1615,8 +1625,8 @@ def main() -> None:
# Query commands print a report; library logging would interleave with it.
quiet = args.command in ("status", "ui", "gek-init", "gek", "operator",
- "member", "group", "file", "denylist", "reload",
- "restart-daemon", "reset")
+ "member", "group", "file", "video", "denylist",
+ "reload", "restart-daemon", "reset")
logging.basicConfig(
level=logging.ERROR if quiet else getattr(logging, args.log_level),
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
@@ -2107,6 +2117,24 @@ def main() -> None:
print("usage: meshbay-node file list|rm <id> [--group NAME] [--yes]")
sys.exit(1)
+ if args.command == "video":
+ cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
+ if (args.subcommand or "") != "rematch":
+ print("usage: meshbay-node video rematch [--group NAME] [--yes]")
+ sys.exit(1)
+ group_id = _resolve_group(cfg, args.group)
+ if not args.yes:
+ print("Re-resolve every automatic TMDB match for this group's videos?")
+ print("Manual 'Fix match' corrections are kept. Re-resolution is lazy —")
+ print("each poster re-queries TMDB the next time it is opened.")
+ if input("proceed? [y/N] ").strip().lower() not in ("y", "yes"):
+ print("cancelled")
+ return
+ out = _daemon_api(cfg, f"/api/groups/{group_id}/video/rematch", method="POST")
+ print(f"cleared {out.get('removed', 0)} automatic match(es) "
+ f"across {out.get('videos', 0)} video file(s)")
+ return
+
if args.command == "group":
if args.subcommand in (None, "list"):
# Milestone 14.2.