diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-29 14:58:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-29 14:58:58 +0200 |
| commit | 71b7a310ce938f072fe20f27eeeadd40685f1ad1 (patch) | |
| tree | 75a7d93d58fda0e742dd9b1c94538b532c34464a /packages/meshbay-hub/src/meshbay_hub | |
| parent | b7733e812fadd6007976d262bd1d793572a36ba7 (diff) | |
| download | meshbay-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-hub/src/meshbay_hub')
12 files changed, 69 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 525f70c..d3bf11a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -182,6 +182,9 @@ export default { 'video.search_button': 'Suchen', 'video.search_no_results': 'Keine Treffer gefunden.', 'video.search_apply_hint': 'Gilt für alle Dateien, die derzeit unter diesem Titel gruppiert sind.', + 'video.search_apply_hint_movie': 'Gilt nur für diese Datei.', + 'video.source_file': 'Datei: {name}', + 'video.no_match': 'Keine sichere TMDB-Übereinstimmung — Dateiname wird angezeigt.', // Musik 'music.mode_grid': 'Alben', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a35eaf5..5c60f9e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -180,6 +180,9 @@ export default { 'video.search_button': 'Search', 'video.search_no_results': 'No matches found.', 'video.search_apply_hint': 'Applies to every file currently grouped under this title.', + 'video.search_apply_hint_movie': 'Applies to this file only.', + 'video.source_file': 'File: {name}', + 'video.no_match': 'No confident TMDB match — showing the filename.', // Music 'music.mode_grid': 'Albums', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 7400fc3..1e3aba2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -180,6 +180,9 @@ export default { 'video.search_button': 'Buscar', 'video.search_no_results': 'No se encontraron coincidencias.', 'video.search_apply_hint': 'Se aplica a todos los archivos agrupados actualmente bajo este título.', + 'video.search_apply_hint_movie': 'Se aplica solo a este archivo.', + 'video.source_file': 'Archivo: {name}', + 'video.no_match': 'Sin coincidencia fiable en TMDB — se muestra el nombre del archivo.', // Música 'music.mode_grid': 'Álbumes', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 98b155a..c07b57c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -181,6 +181,9 @@ export default { 'video.search_button': 'Rechercher', 'video.search_no_results': 'Aucune correspondance trouvée.', 'video.search_apply_hint': 'S’applique à tous les fichiers actuellement regroupés sous ce titre.', + 'video.search_apply_hint_movie': 'Ne s’applique qu’à ce fichier.', + 'video.source_file': 'Fichier : {name}', + 'video.no_match': 'Aucune correspondance TMDB fiable — nom de fichier affiché.', // Musique 'music.mode_grid': 'Albums', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 9843e7c..4b68951 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -181,6 +181,9 @@ export default { 'video.search_button': 'Cerca', 'video.search_no_results': 'Nessuna corrispondenza trovata.', 'video.search_apply_hint': 'Si applica a tutti i file attualmente raggruppati sotto questo titolo.', + 'video.search_apply_hint_movie': 'Si applica solo a questo file.', + 'video.source_file': 'File: {name}', + 'video.no_match': 'Nessuna corrispondenza TMDB affidabile — mostrato il nome del file.', // Musica 'music.mode_grid': 'Album', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 41635d4..471933d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -178,6 +178,9 @@ export default { 'video.search_button': '検索', 'video.search_no_results': '一致する結果が見つかりません。', 'video.search_apply_hint': '現在このタイトルでグループ化されているすべてのファイルに適用されます。', + 'video.search_apply_hint_movie': 'このファイルにのみ適用されます。', + 'video.source_file': 'ファイル: {name}', + 'video.no_match': '確実なTMDB一致なし — ファイル名を表示しています。', // 音楽 'music.mode_grid': 'アルバム', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 38f9845..a24cb42 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -182,6 +182,9 @@ export default { 'video.search_button': 'Zoeken', 'video.search_no_results': 'Geen overeenkomsten gevonden.', 'video.search_apply_hint': 'Geldt voor alle bestanden die momenteel onder deze titel zijn gegroepeerd.', + 'video.search_apply_hint_movie': 'Alleen van toepassing op dit bestand.', + 'video.source_file': 'Bestand: {name}', + 'video.no_match': 'Geen betrouwbare TMDB-match — bestandsnaam wordt getoond.', // Muziek 'music.mode_grid': 'Albums', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index bfad68b..58dc7b5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -189,6 +189,9 @@ export default { 'video.search_button': 'Szukaj', 'video.search_no_results': 'Nie znaleziono dopasowań.', 'video.search_apply_hint': 'Dotyczy wszystkich plików obecnie zgrupowanych pod tym tytułem.', + 'video.search_apply_hint_movie': 'Dotyczy tylko tego pliku.', + 'video.source_file': 'Plik: {name}', + 'video.no_match': 'Brak pewnego dopasowania TMDB — pokazano nazwę pliku.', // Muzyka 'music.mode_grid': 'Albumy', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index a6ddadd..00d9051 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -182,6 +182,9 @@ export default { 'video.search_button': 'Pesquisar', 'video.search_no_results': 'Nenhuma correspondência encontrada.', 'video.search_apply_hint': 'Aplica-se a todos os arquivos atualmente agrupados sob este título.', + 'video.search_apply_hint_movie': 'Aplica-se somente a este arquivo.', + 'video.source_file': 'Arquivo: {name}', + 'video.no_match': 'Sem correspondência confiável no TMDB — exibindo o nome do arquivo.', // Música 'music.mode_grid': 'Álbuns', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 5fb2a0b..e5590d8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -175,6 +175,9 @@ export default { 'video.search_button': '搜索', 'video.search_no_results': '未找到匹配项。', 'video.search_apply_hint': '将应用于当前归类在此标题下的所有文件。', + 'video.search_apply_hint_movie': '仅适用于此文件。', + 'video.source_file': '文件:{name}', + 'video.no_match': '没有可靠的 TMDB 匹配 — 显示文件名。', // 音乐 'music.mode_grid': '专辑', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index b315512..a7e8e3d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -2916,6 +2916,30 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } color: var(--text-secondary); margin-top: 6px; } +.video-detail-source { + font-size: 0.78em; + color: var(--text-dim); + margin-top: 6px; + word-break: break-all; +} +.video-detail-tmdbref { white-space: nowrap; } +.video-detail-nomatch { + font-size: 0.85em; + color: var(--warn, #d98324); + margin-bottom: 4px; +} +.video-card-unmatched { outline: 1px dashed var(--warn, #d98324); outline-offset: -1px; } +.video-card-flag { + display: inline-block; + margin-left: 4px; + padding: 0 5px; + border-radius: 8px; + font-size: 0.8em; + font-weight: 700; + color: var(--warn, #d98324); + border: 1px solid var(--warn, #d98324); + cursor: help; +} .video-season-list { margin-top: 14px; } .video-season { margin-bottom: 14px; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js index df930fd..f73def0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js @@ -325,7 +325,7 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g const ready = metaReady && imageReady; return html` - <div class="video-card" onClick=${onOpen}> + <div class="video-card ${ready && !confident ? 'video-card-unmatched' : ''}" onClick=${onOpen}> ${!ready && html` <div class="video-poster video-poster-loading"><span class="spinner"></span></div> `} @@ -338,7 +338,10 @@ function PosterCard({ title, subtitle, repEntry, transportRef, gekRef, onOpen, g </div> ${ready && html` <div class="video-card-info"> - <div class="video-card-title">${(confident && meta.title) || title}</div> + <div class="video-card-title"> + ${(confident && meta.title) || title} + ${!confident && html`<span class="video-card-flag" title=${t('video.no_match')}>?</span>`} + </div> <div class="video-card-sub"> ${confident && meta.release_date ? yearOf(meta.release_date) : ''} ${confident && meta.first_air_date ? yearOf(meta.first_air_date) : ''} @@ -443,7 +446,9 @@ function TmdbSearchOverlay({ ${searching ? html`<span class="spinner"></span>` : t('video.search_button')} </button> </form> - <p class="video-search-hint">${t('video.search_apply_hint')}</p> + <p class="video-search-hint"> + ${mediaType === 'tv' ? t('video.search_apply_hint') : t('video.search_apply_hint_movie')} + </p> ${error && html`<p class="video-search-error">${error}</p>`} ${results && results.length === 0 && !searching && html` <p class="page-message">${t('video.search_no_results')}</p> @@ -508,6 +513,13 @@ function VideoDetailModal({ <${Icon} name="close" /></button> </div> <div class="video-detail-body"> + ${meta !== null && !confident && html` + <p class="video-detail-nomatch">${t('video.no_match')}</p> + `} + <p class="video-detail-source"> + ${t('video.source_file', { name: `${repEntry.path}/${repEntry.name}` })} + ${confident && html`<span class="video-detail-tmdbref"> · TMDB #${meta.tmdb_id}</span>`} + </p> ${confident && html` <p class="video-detail-overview">${(seasonConfident && seasonMeta.overview) || meta.overview}</p> <p class="video-detail-facts"> |