summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js23
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js108
13 files changed, 132 insertions, 21 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 d3bf11a..c4ee388 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -185,6 +185,8 @@ export default {
'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.',
+ 'video.versions': '{n} Versionen',
+ 'video.rematch_one': 'Neu zuordnen',
// 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 5c60f9e..16ae3ab 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -183,6 +183,8 @@ export default {
'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.',
+ 'video.versions': '{n} versions',
+ 'video.rematch_one': 'Re-match',
// 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 1e3aba2..88b222d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -183,6 +183,8 @@ export default {
'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.',
+ 'video.versions': '{n} versiones',
+ 'video.rematch_one': 'Volver a asociar',
// 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 c07b57c..b08cf28 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -184,6 +184,8 @@ export default {
'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é.',
+ 'video.versions': '{n} versions',
+ 'video.rematch_one': 'Relancer la recherche',
// 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 4b68951..f1e8467 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -184,6 +184,8 @@ export default {
'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.',
+ 'video.versions': '{n} versioni',
+ 'video.rematch_one': 'Riassocia',
// 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 471933d..95cc040 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -181,6 +181,8 @@ export default {
'video.search_apply_hint_movie': 'このファイルにのみ適用されます。',
'video.source_file': 'ファイル: {name}',
'video.no_match': '確実なTMDB一致なし — ファイル名を表示しています。',
+ 'video.versions': '{n} 個のバージョン',
+ 'video.rematch_one': '再マッチ',
// 音楽
'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 a24cb42..076aa15 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -185,6 +185,8 @@ export default {
'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.',
+ 'video.versions': '{n} versies',
+ 'video.rematch_one': 'Opnieuw koppelen',
// 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 58dc7b5..00ef59e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -192,6 +192,8 @@ export default {
'video.search_apply_hint_movie': 'Dotyczy tylko tego pliku.',
'video.source_file': 'Plik: {name}',
'video.no_match': 'Brak pewnego dopasowania TMDB — pokazano nazwę pliku.',
+ 'video.versions': 'Wersje: {n}',
+ 'video.rematch_one': 'Dopasuj ponownie',
// 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 00d9051..7d9173e 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
@@ -185,6 +185,8 @@ export default {
'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.',
+ 'video.versions': '{n} versões',
+ 'video.rematch_one': 'Combinar de novo',
// 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 e5590d8..74d729e 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
@@ -178,6 +178,8 @@ export default {
'video.search_apply_hint_movie': '仅适用于此文件。',
'video.source_file': '文件:{name}',
'video.no_match': '没有可靠的 TMDB 匹配 — 显示文件名。',
+ 'video.versions': '{n} 个版本',
+ 'video.rematch_one': '重新匹配',
// 音乐
'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 a7e8e3d..8e714f3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -2929,6 +2929,8 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
margin-bottom: 4px;
}
.video-card-unmatched { outline: 1px dashed var(--warn, #d98324); outline-offset: -1px; }
+.video-version-list { margin-top: 12px; display: flex; flex-direction: column; gap: 4px; }
+.video-admin-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.video-card-flag {
display: inline-block;
margin-left: 4px;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index d6a28d9..5a6e36b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -73,7 +73,7 @@ function _aborted() {
// list is what lets a response two steps later be tied back to the right
// one.
const ADMIN_OP_TYPES = new Set([
- 'tmdb_override', 'tmdb_config', 'tmdb_enabled', 'video_root', 'audio_root',
+ 'tmdb_override', 'tmdb_rematch', 'tmdb_config', 'tmdb_enabled', 'video_root', 'audio_root',
'photo_roots',
'musicbrainz_enabled', 'file_delete', 'dir_delete',
'member_upload', 'apps_enabled', 'set_scan_settings', 'member_revoke',
@@ -916,6 +916,22 @@ class MeshBayTransport {
}
/**
+ * Drop one file's cached TMDB match so it re-resolves with the node's
+ * current matcher (§10.1/V13) — the one-click alternative to the full
+ * search-and-pick flow. Signed for the same reason as overrideTmdbMatch.
+ */
+ async rematchTmdbMatch(fileId, signFn) {
+ const msg = await this._sendAndWait({
+ type: 'tmdb_rematch', v: '0.7', file_id: fileId,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ if (msg.type === 'admin_challenge') {
+ return this._authorizeAdminOp(msg, 'tmdb_rematch', `file_id=${fileId}`, signFn);
+ }
+ return msg;
+ }
+
+ /**
* Set/clear a custom TMDB API token, and/or set the language TMDB is
* queried in (e.g. "fr-FR") — one for the whole node, since both are one
* operator's shared credential/cache, not a per-group concern (see
@@ -2032,6 +2048,11 @@ class MeshBayTransport {
fileId: msg.file_id || '', tmdbId: msg.tmdb_id || '', mediaType: msg.media_type || '',
});
}
+ // Same shape: the operator dropped one file's match to have it
+ // re-resolved (§10.1/V13). No tmdbId — the node re-derives it.
+ if (msg.type === 'tmdb_rematch_ack' && this._onTmdbOverride) {
+ this._onTmdbOverride({ fileId: msg.file_id || '', tmdbId: '', mediaType: '' });
+ }
// Same shape: the operator changed which folder is the Videos app's
// entry point for this group.
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 f73def0..6d13e27 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
@@ -478,12 +478,26 @@ function TmdbSearchOverlay({
}
function VideoDetailModal({
- title, meta, repEntry, show, transportRef, gekRef, onClose, onPlay, isNodeAdmin,
+ title, meta, repEntry, show, files, transportRef, gekRef, onClose, onPlay, isNodeAdmin,
}) {
const confident = Boolean(meta && meta.confidence && meta.tmdb_id);
const [searching, setSearching] = useState(false);
+ const [rematching, setRematching] = useState(false);
const mediaType = show ? 'tv' : 'movie';
+ // §10.1/V13: drop this file's cached match on the node and let it
+ // re-resolve with the current matcher — the one-click alternative to the
+ // full search-and-pick flow above.
+ const doRematch = useCallback(async () => {
+ if (rematching) return;
+ setRematching(true);
+ try {
+ await transportRef.current.rematchTmdbMatch(repEntry.id, buildSignFn(transportRef));
+ bumpMediaMetaGeneration();
+ } catch { /* leave the current match in place */ }
+ setRematching(false);
+ }, [rematching, repEntry, transportRef]);
+
// Reset whenever a different file/show is opened in this same modal
// instance — repEntry/show change identity, selectedSeason must not
// silently keep pointing at whatever the previous show's season 4 was.
@@ -535,20 +549,40 @@ function VideoDetailModal({
`}
`}
${isNodeAdmin && html`
- <button class="admin-btn video-fix-match" onClick=${() => setSearching(true)}>
- ${t('video.fix_match')}
- </button>
+ <div class="video-admin-actions">
+ <button class="admin-btn video-fix-match" onClick=${() => setSearching(true)}>
+ ${t('video.fix_match')}
+ </button>
+ <button class="admin-btn" onClick=${doRematch} disabled=${rematching}>
+ ${rematching ? html`<span class="spinner"></span>` : t('video.rematch_one')}
+ </button>
+ </div>
`}
${showMultiSeason && html`
<${SeasonTabs} seasons=${show.seasons} selected=${selectedSeason}
onSelect=${setSelectedSeason} />
`}
- ${!show && html`
+ ${!show && (!files || files.length <= 1) && html`
<button class="admin-btn" onClick=${() => onPlay(repEntry)}>
<${Icon} name="play" /> ${t('group.play')}
${repEntry.duration ? ` (${formatDuration(repEntry.duration)})` : ''}
</button>
`}
+ ${!show && files && files.length > 1 && html`
+ <div class="video-version-list">
+ ${[...files]
+ .sort((a, b) => (b.height || 0) - (a.height || 0) || (b.size || 0) - (a.size || 0))
+ .map((f) => html`
+ <button class="video-episode-row" key=${f.id} onClick=${() => onPlay(f)}>
+ <${Icon} name="play" />
+ <span class="video-episode-label">
+ ${[formatResolution(f.width, f.height), formatDuration(f.duration),
+ formatSize(f.size)].filter(Boolean).join(' · ')}
+ </span>
+ </button>
+ `)}
+ </div>
+ `}
${show && html`
<div class="video-season-list">
${(showMultiSeason ? show.seasons.filter((s) => s.season === selectedSeason) : show.seasons)
@@ -643,28 +677,62 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
});
}, [shows, metaByGroup]);
- const openDetail = (title, repEntry, show) => setDetail({ title, repEntry, show });
+ // The movie counterpart of mergedShows (§10.1/V12): two files that TMDB
+ // resolves to the same id — the usual case being one film present at two
+ // resolutions, or the same rip in two folders — collapse to one card
+ // whose detail modal lists the versions. Same keying discipline as
+ // mergedShows: the first constituent's id is stable and unique, so an
+ // unmerged movie keeps the exact key its card already had and a merge
+ // updates props instead of remounting (which would throw away an
+ // already-resolved poster).
+ const mergedMovies = useMemo(() => {
+ const byTmdbId = new Map();
+ const standalone = [];
+ for (const e of movies) {
+ const meta = metaByGroup[`movie:${e.id}`];
+ const tmdbId = meta && meta.confidence && meta.tmdb_id;
+ if (tmdbId) {
+ if (!byTmdbId.has(tmdbId)) byTmdbId.set(tmdbId, []);
+ byTmdbId.get(tmdbId).push(e);
+ } else {
+ standalone.push([e]);
+ }
+ }
+ return [...byTmdbId.values(), ...standalone].map((files) => ({
+ key: `movie:${files[0].id}`,
+ title: files[0].display_title || files[0].name,
+ files,
+ }));
+ }, [movies, metaByGroup]);
+
+ const openDetail = (title, repEntry, show, files) => setDetail({ title, repEntry, show, files });
const detailTRef = detail && detail.repEntry._tRef ? detail.repEntry._tRef : transportRef;
const detailGRef = detail && detail.repEntry._gRef ? detail.repEntry._gRef : gekRef;
const detailMeta = useMediaMeta(detailTRef, detail ? detail.repEntry.id : null, !!detail);
return html`
<div class="video-grid">
- ${movies.map((e) => html`
- <${LazyTile} key=${e.id}>
- <${PosterCard} title=${e.display_title || e.name}
- subtitle=${formatDuration(e.duration)} repEntry=${e}
- groupKey=${`movie:${e.id}`}
+ ${mergedMovies.map((m) => {
+ const repEntry = m.files.find((e) => e.thumb_hash) || m.files[0];
+ const subtitle = m.files.length > 1
+ ? t('video.versions', { n: m.files.length })
+ : formatDuration(repEntry.duration);
+ // With TMDB off and only one file there is nothing the detail modal
+ // would add for a movie (no overview, no season list) — straight to
+ // the player. More than one file always needs the version picker.
+ const straightToPlayer = !tmdbEnabled && m.files.length === 1;
+ return html`
+ <${LazyTile} key=${m.key}>
+ <${PosterCard} title=${m.title}
+ subtitle=${subtitle} repEntry=${repEntry}
+ groupKey=${`movie:${m.files[0].id}`}
+ onMetaResolved=${handleMetaResolved}
transportRef=${transportRef} gekRef=${gekRef}
- onOpen=${() => (tmdbEnabled
- // With TMDB off there is nothing the detail modal would show
- // for a movie (no overview, no season list to pick from,
- // unlike a show) — so it would just be an extra click in
- // front of a Play button. Straight to the player instead.
- ? openDetail(e.display_title || e.name, e, null)
- : onPreview(e))} />
+ onOpen=${() => (straightToPlayer
+ ? onPreview(repEntry)
+ : openDetail(m.title, repEntry, null, m.files))} />
</${LazyTile}>
- `)}
+ `; })}
${mergedShows.map((s) => {
// Prefer an episode that actually has a thumbnail over blindly
// episodes[0]: if that specific file's enrichment hasn't produced
@@ -696,7 +764,7 @@ function PosterGrid({ movies, shows, transportRef, gekRef, onPreview, tmdbEnable
</div>
${detail && html`
<${VideoDetailModal} title=${detail.title} meta=${detailMeta}
- repEntry=${detail.repEntry} show=${detail.show}
+ repEntry=${detail.repEntry} show=${detail.show} files=${detail.files}
transportRef=${detailTRef} gekRef=${detailGRef} isNodeAdmin=${isNodeAdmin}
onClose=${() => setDetail(null)}
onPlay=${(entry) => { setDetail(null); onPreview(entry); }} />