Hanni und Nanni ist nicht Hanni und Nanni 3
Deine drei Fälle haben eines gemeinsam, und es ist nicht das, was ich zuerst vermutet hatte: keiner der Dateinamen nennt ein Jahr. Der Riegel von gestern greift deshalb nicht. Gemessen, woran es wirklich liegt. Bei TMDB heisst der Film „Hanni & Nanni", auf der Platte „Hanni und Nanni". Das Kaufmanns-Und fällt im Vergleichs- schlüssel als Sonderzeichen heraus, das Wort „und" bleibt stehen — die Namensgleichheit griff also nie. Gewählt wurde stattdessen der erstbeste Treffer, und das war „Hanni & Nanni 3". Der richtige stand auf Platz zwei, und alle vier Treffer hatten dieselbe Beliebtheit: die Reihenfolge war Zufall. Bindewörter zwischen zwei Namen zählen jetzt nicht mehr mit — gestrichen, nicht übersetzt, sonst bliebe „Batman and Robin" aussen vor. Am Wortanfang bleiben sie stehen: „Und dann kam Polly" ist kein „dann kam Polly". Und die Suche vergleicht über den Schlüssel statt Zeichen für Zeichen. Das verhindert aber nur Neues. Was schon zusammengelegt ist, bleibt es — deshalb gibt es jetzt „Eigener Film": eine Datei löst sich aus ihrem Titel und wird ein eigener Eintrag, mit dem Titel aus dem Dateinamen und ohne Kennung. Eine zu raten hiesse, denselben Fehler zu wiederholen; so steht sie unter „Ohne Zuordnung" und ist in zwei Klicks richtig. Der Super-Mario-Fall liegt anders: beide Titel treffen heute richtig. Der steckt noch aus einem früheren Lauf fest und braucht das Trennen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c3124ba27c
commit
58e06d0539
@@ -41,9 +41,12 @@ public sealed record OhneZuordnung(
|
||||
public sealed record Mehrfachbericht(
|
||||
int Gesamt, IReadOnlyList<Mehrfachtitel> Titel);
|
||||
|
||||
/// <param name="Dateien">Die Namen der Dateien, die an diesem einen Titel hängen.</param>
|
||||
/// <param name="Dateien">Die Dateien, die an diesem einen Titel hängen.</param>
|
||||
public sealed record Mehrfachtitel(
|
||||
Guid Id, string Art, string Titel, int? Jahr, IReadOnlyList<string> Dateien);
|
||||
Guid Id, string Art, string Titel, int? Jahr, IReadOnlyList<Fassungsdatei> Dateien);
|
||||
|
||||
/// <summary>Eine Datei an einem Titel — mit Kennung, damit sie sich herauslösen lässt.</summary>
|
||||
public sealed record Fassungsdatei(Guid Id, string Name);
|
||||
|
||||
public sealed record UnzugeordneteDatei(Guid Id, string Pfad, string Grund);
|
||||
|
||||
|
||||
@@ -299,15 +299,15 @@ public static class ManagementEndpoints
|
||||
.Select(m => new
|
||||
{
|
||||
m.Id, m.Title, Jahr = m.ParsedYear ?? m.Year,
|
||||
Pfade = db.MediaFiles.Where(f => f.MovieId == m.Id)
|
||||
.Select(f => f.Path).ToList(),
|
||||
Dateien = db.MediaFiles.Where(f => f.MovieId == m.Id)
|
||||
.Select(f => new { f.Id, f.Path }).ToList(),
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var liste = mehrfach
|
||||
.Select(m => new Mehrfachtitel(
|
||||
m.Id, "movie", m.Title, m.Jahr,
|
||||
m.Pfade.Select(p => Dateiname(p) ?? p).ToList()))
|
||||
m.Dateien.Select(f => new Fassungsdatei(f.Id, Dateiname(f.Path) ?? f.Path)).ToList()))
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(new Mehrfachbericht(liste.Count, liste));
|
||||
|
||||
@@ -151,6 +151,63 @@ public static class MatchEndpoints
|
||||
return betroffen > 0 ? Results.NoContent() : Results.NotFound();
|
||||
}).RequireAuthorization(FlaroPolicies.Administrator);
|
||||
|
||||
// **Eine Datei aus ihrem Titel herauslösen.**
|
||||
//
|
||||
// Zwei Dateien hängen an einem Eintrag, gehören aber zu zwei Filmen:
|
||||
// „Hanni und Nanni" und „Hanni und Nanni 3" liefen bei der
|
||||
// Metadatenquelle auf denselben Treffer, und der zweite Film war
|
||||
// damit nicht mehr zu sehen. Die Riegel dagegen greifen erst beim
|
||||
// nächsten Lauf — was schon zusammengelegt ist, bleibt es.
|
||||
//
|
||||
// Der neue Eintrag bekommt den Titel aus dem Dateinamen und keine
|
||||
// Kennung: er steht danach unter „Ohne Zuordnung" und lässt sich dort
|
||||
// in zwei Klicks richtigstellen. Eine Kennung zu raten hiesse, denselben
|
||||
// Fehler noch einmal zu machen.
|
||||
app.MapPost("/api/v1/files/{id:guid}/split", async (
|
||||
Guid id, FlaroContext db, TimeProvider clock) =>
|
||||
{
|
||||
var datei = await db.MediaFiles.FirstOrDefaultAsync(f => f.Id == id);
|
||||
if (datei is null) return Results.NotFound();
|
||||
|
||||
if (datei.MovieId is not { } alt)
|
||||
{
|
||||
return Results.Problem(
|
||||
"Nur eine Filmdatei lässt sich herauslösen. Bei Serien entscheidet "
|
||||
+ "die Folgennummer, zu welcher Folge eine Datei gehört.",
|
||||
statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
var geschwister = await db.MediaFiles.CountAsync(f => f.MovieId == alt);
|
||||
if (geschwister < 2)
|
||||
{
|
||||
return Results.Problem(
|
||||
"An diesem Titel hängt nur diese eine Datei — da gibt es nichts zu trennen.",
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
var name = FileNames.ParseMovie(datei.Path);
|
||||
var bisher = await db.Movies.AsNoTracking().FirstAsync(m => m.Id == alt);
|
||||
|
||||
var neu = new Movie
|
||||
{
|
||||
LibraryId = datei.LibraryId,
|
||||
ParsedTitle = name.Title,
|
||||
ParsedYear = name.Year,
|
||||
Title = name.Title,
|
||||
SortTitleValue = SortTitle.For(name.Title),
|
||||
Year = name.Year,
|
||||
// **Sofort sichtbar.** Wer etwas herauslöst, will es sehen —
|
||||
// nicht erst freigeben müssen.
|
||||
PublishAt = bisher.PublishAt ?? clock.GetUtcNow(),
|
||||
};
|
||||
db.Movies.Add(neu);
|
||||
|
||||
datei.MovieId = neu.Id;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Results.Ok(new MatchState(null, null, false, neu.Title, neu.Year, Id: neu.Id));
|
||||
}).RequireAuthorization(FlaroPolicies.Administrator);
|
||||
|
||||
// **Woher dieser Titel kommt.**
|
||||
//
|
||||
// Steht im Dialog über der Suche. Wenn eine Zuordnung völlig daneben
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Flaro.Core.Library;
|
||||
@@ -61,7 +62,41 @@ public static class Titelschluessel
|
||||
}
|
||||
}
|
||||
|
||||
return bau.ToString().TrimEnd();
|
||||
return OhneBindewort(bau.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
/// <summary>Die Wörter, die nur zwei Namen verbinden.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Weil „&" verschwindet und „und" stehenbleibt.</b> TMDB führt
|
||||
/// den Film als „Hanni & Nanni", auf der Platte heisst er „Hanni und
|
||||
/// Nanni" — das Kaufmanns-Und fällt als Sonderzeichen heraus, das Wort
|
||||
/// nicht, und die beiden Schlüssel gingen auseinander. Die Namensgleichheit
|
||||
/// griff deshalb nicht, und gewählt wurde stattdessen der erstbeste
|
||||
/// Treffer: „Hanni & Nanni 3". Gemessen am echten Dienst — der richtige
|
||||
/// Film stand auf Platz zwei.
|
||||
///
|
||||
/// <b>Gestrichen statt umgeschrieben.</b> „&" nach „und" zu übersetzen
|
||||
/// hiesse, sich für eine Sprache zu entscheiden; „Batman and Robin" bliebe
|
||||
/// dann aussen vor. Ohne das Bindewort passt beides.
|
||||
/// </remarks>
|
||||
private static readonly string[] Bindewoerter = ["und", "and", "et", "e", "y"];
|
||||
|
||||
private static string OhneBindewort(string schluessel)
|
||||
{
|
||||
if (schluessel.Length == 0) return schluessel;
|
||||
|
||||
var worte = schluessel.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (worte.Length < 3) return schluessel; // „Ich und" ist kein Bindewort-Fall
|
||||
|
||||
// **Nur zwischen zwei Wörtern, nie am Rand.** „Und dann kam Polly"
|
||||
// fängt mit einem Bindewort an, und das gehört zum Titel.
|
||||
var behalten = worte
|
||||
.Where((w, i) => i == 0 || i == worte.Length - 1
|
||||
|| !Bindewoerter.Contains(w, StringComparer.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
return string.Join(' ', behalten);
|
||||
}
|
||||
|
||||
/// <summary>Ob zwei Fundstücke derselbe Titel sind.</summary>
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Text.Json.Serialization;
|
||||
using Flaro.Core.Metadata;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Flaro.Core.Library;
|
||||
|
||||
namespace Flaro.Metadata;
|
||||
|
||||
/// <summary>Fragt TMDB nach Titeln, Beschreibungen und Bildern.</summary>
|
||||
@@ -289,10 +291,19 @@ public sealed class TmdbClient(
|
||||
// Steht ein Treffer mit genau diesem Namen in der Liste, ist er
|
||||
// gemeint.
|
||||
//
|
||||
// Nur bei Gleichheit, nicht bei Ähnlichkeit: eine eigene Bewertung zu
|
||||
// bauen hiesse, TMDBs Rangfolge nachzubauen — mit weniger Daten.
|
||||
var genau = treffer.FirstOrDefault(e =>
|
||||
string.Equals(titelVon(e)?.Trim(), titel.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
// **Verglichen wird über den Titelschlüssel, nicht Zeichen für
|
||||
// Zeichen.** Auf der Platte heisst der Film „Hanni und Nanni", bei
|
||||
// TMDB „Hanni & Nanni" — zeichengenau passt das nie. Gewählt wurde
|
||||
// deshalb der erstbeste Treffer, und das war „Hanni & Nanni 3".
|
||||
// Gemessen: der richtige Film stand auf Platz zwei, und alle vier
|
||||
// Treffer hatten dieselbe Beliebtheit. Die Reihenfolge war also
|
||||
// Zufall, und auf Zufall soll sich hier nichts verlassen.
|
||||
//
|
||||
// Weiterhin nur bei Gleichheit des Schlüssels, nicht bei Ähnlichkeit:
|
||||
// eine eigene Bewertung zu bauen hiesse, TMDBs Rangfolge nachzubauen
|
||||
// — mit weniger Daten.
|
||||
var gesucht = Titelschluessel.Von(titel);
|
||||
var genau = treffer.FirstOrDefault(e => Titelschluessel.Von(titelVon(e)) == gesucht);
|
||||
|
||||
return genau ?? treffer[0];
|
||||
}
|
||||
|
||||
@@ -512,6 +512,76 @@ public class MetadataTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Eine Datei lässt sich aus ihrem Titel herauslösen.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Weil die Riegel nur Neues verhindern.</b> Was schon zusammengelegt
|
||||
/// ist, bleibt es — „Hanni und Nanni" und „Hanni und Nanni 3" hängen dann
|
||||
/// an einem Eintrag, und einer der beiden Filme ist nicht mehr zu sehen.
|
||||
/// Der neue Eintrag bekommt den Titel aus dem Dateinamen und keine
|
||||
/// Kennung; eine zu raten hiesse, denselben Fehler zu wiederholen.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public async Task Eine_Datei_laesst_sich_aus_ihrem_Titel_herausloesen()
|
||||
{
|
||||
Datei("Hanni und Nanni/Hanni und Nanni.mp4");
|
||||
Datei("Hanni und Nanni/Hanni und Nanni 3.mp4");
|
||||
|
||||
// Die Quelle liefert für beides denselben Film — der Fall aus dem Betrieb.
|
||||
var anbieter = new VorgegebenerAnbieter
|
||||
{
|
||||
Film = new MovieMetadata(225313, "Hanni & Nanni 3", 2013, null, null, null),
|
||||
};
|
||||
|
||||
using var server = MitAnbieter(anbieter);
|
||||
var (client, bibliothek) = await Einrichten(server, "Movies");
|
||||
await Scannen(client, bibliothek);
|
||||
|
||||
var mehrfach = await client.GetFromJsonAsync<Mehrfachbericht>(
|
||||
$"/api/v1/libraries/{bibliothek}/versions");
|
||||
|
||||
Assert.That(mehrfach!.Gesamt, Is.EqualTo(1), "zwei Dateien an einem Titel");
|
||||
var dateien = mehrfach.Titel[0].Dateien;
|
||||
Assert.That(dateien, Has.Count.EqualTo(2));
|
||||
|
||||
var raus = dateien.First(f => f.Name.Contains(" 3"));
|
||||
var antwort = await client.PostAsync($"/api/v1/files/{raus.Id}/split", null);
|
||||
antwort.EnsureSuccessStatusCode();
|
||||
var neu = (await antwort.Content.ReadFromJsonAsync<MatchState>())!;
|
||||
|
||||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(filme!.Items, Has.Count.EqualTo(2), "jetzt zwei Filme");
|
||||
Assert.That(neu.Title, Is.EqualTo("Hanni und Nanni 3"), "der Titel kommt aus dem Dateinamen");
|
||||
Assert.That(neu.TmdbId, Is.Null, "und ohne Kennung — geraten wird nicht");
|
||||
Assert.That(filme.Items.Select(m => m.Files.Count), Is.All.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Eine einzelne Datei lässt sich nicht herauslösen.</summary>
|
||||
[Test]
|
||||
public async Task Eine_einzelne_Datei_laesst_sich_nicht_herausloesen()
|
||||
{
|
||||
Datei("Matrix (1999)/Matrix (1999).mkv");
|
||||
|
||||
var anbieter = new VorgegebenerAnbieter
|
||||
{
|
||||
Film = new MovieMetadata(603, "Matrix", 1999, null, null, null),
|
||||
};
|
||||
|
||||
using var server = MitAnbieter(anbieter);
|
||||
var (client, bibliothek) = await Einrichten(server, "Movies");
|
||||
await Scannen(client, bibliothek);
|
||||
|
||||
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
||||
var datei = filme!.Items[0].Files.Single();
|
||||
|
||||
var antwort = await client.PostAsync($"/api/v1/files/{datei.Id}/split", null);
|
||||
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
|
||||
}
|
||||
|
||||
/// <summary>Eine falsche Zuordnung darf ihre eigene Korrektur nicht verhindern.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
|
||||
@@ -84,4 +84,58 @@ public class FassungTests
|
||||
Assert.That(FileNames.ParseMovie("The Matrix (1999) - Mobil.mp4").Title, Is.EqualTo("The Matrix"));
|
||||
Assert.That(FileNames.ParseMovie("The Matrix (1999) - 2160p.mkv").Title, Is.EqualTo("The Matrix"));
|
||||
}
|
||||
|
||||
/* ---------- Bindewörter ---------- */
|
||||
|
||||
/// <summary>„Hanni und Nanni" und „Hanni & Nanni" sind derselbe Titel.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <b>Der Fall, an dem ein ganzer Film verschwand.</b> Auf der Platte
|
||||
/// heisst er „Hanni und Nanni", bei TMDB „Hanni & Nanni" — das
|
||||
/// Kaufmanns-Und fiel als Sonderzeichen heraus, das Wort „und" blieb
|
||||
/// stehen. Die Namensgleichheit griff deshalb nie, und gewählt wurde der
|
||||
/// erstbeste Treffer: „Hanni & Nanni 3". Beide Dateien landeten auf
|
||||
/// derselben Kennung, und Teil 1 war nicht mehr zu sehen.
|
||||
/// </remarks>
|
||||
[TestCase("Hanni und Nanni", "Hanni & Nanni")]
|
||||
[TestCase("Batman and Robin", "Batman & Robin")]
|
||||
[TestCase("Tom und Jerry", "Tom & Jerry")]
|
||||
public void Bindewoerter_machen_keinen_Unterschied(string eines, string anderes)
|
||||
{
|
||||
Assert.That(Titelschluessel.Von(eines), Is.EqualTo(Titelschluessel.Von(anderes)));
|
||||
}
|
||||
|
||||
/// <summary>Eine Folgennummer bleibt ein Unterschied.</summary>
|
||||
[Test]
|
||||
public void Eine_Nummer_bleibt_ein_Unterschied()
|
||||
{
|
||||
Assert.That(Titelschluessel.Von("Hanni und Nanni"),
|
||||
Is.Not.EqualTo(Titelschluessel.Von("Hanni & Nanni 3")));
|
||||
}
|
||||
|
||||
/// <summary>Am Anfang gehört das Wort zum Titel.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// „Und dann kam Polly" fängt mit einem Bindewort an — es wegzulassen
|
||||
/// machte daraus „dann kam Polly", und das ist ein anderer Film.
|
||||
/// </remarks>
|
||||
[TestCase("Und dann kam Polly")]
|
||||
[TestCase("And Then There Were None")]
|
||||
public void Am_Anfang_bleibt_das_Wort_stehen(string titel)
|
||||
{
|
||||
Assert.That(Titelschluessel.Von(titel), Does.StartWith(
|
||||
titel.Split(' ')[0].ToLowerInvariant()));
|
||||
}
|
||||
|
||||
/// <summary>Zwei Wörter bleiben zwei Wörter.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// Bei „Ich und" bliebe sonst nur „Ich" übrig — und das passt dann auf
|
||||
/// jeden Film, der so anfängt.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void Aus_zwei_Woertern_wird_nichts_gestrichen()
|
||||
{
|
||||
Assert.That(Titelschluessel.Von("Krieg und"), Is.EqualTo("krieg und"));
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -815,10 +815,11 @@ export type OhneZuordnung = {
|
||||
export const ohneZuordnung = (bibliothek: string, anzahl = 300) =>
|
||||
anfrage<OhneZuordnung[]>(`/libraries/${bibliothek}/unmatched?take=${anzahl}`);
|
||||
|
||||
export type Fassungsdatei = { id: string; name: string };
|
||||
export type Mehrfachtitel = {
|
||||
id: string; art: 'movie' | 'series';
|
||||
titel: string; jahr: number | null;
|
||||
dateien: string[];
|
||||
dateien: Fassungsdatei[];
|
||||
};
|
||||
export type Mehrfachbericht = { gesamt: number; titel: Mehrfachtitel[] };
|
||||
|
||||
@@ -826,6 +827,15 @@ export type Mehrfachbericht = { gesamt: number; titel: Mehrfachtitel[] };
|
||||
export const mehrfachfassungen = (bibliothek: string, anzahl = 300) =>
|
||||
anfrage<Mehrfachbericht>(`/libraries/${bibliothek}/versions?take=${anzahl}`);
|
||||
|
||||
/** Löst eine Datei aus ihrem Titel heraus — sie wird ein eigener Film.
|
||||
*
|
||||
* **Für falsch Zusammengelegtes.** Zwei Dateien an einem Eintrag sind meist
|
||||
* zwei Fassungen; sind es zwei Filme, war einer davon nicht mehr zu sehen.
|
||||
* Der neue Eintrag bekommt den Titel aus dem Dateinamen und keine Kennung —
|
||||
* er steht danach unter „Ohne Zuordnung" und lässt sich dort richtigstellen. */
|
||||
export const dateiHerausloesen = (datei: string) =>
|
||||
anfrage<Zuordnungsstand>(`/files/${datei}/split`, { method: 'POST' });
|
||||
|
||||
export type VermissteDatei = {
|
||||
fileId: string; path: string; versionLabel: string | null;
|
||||
missingSince: string; libraryName: string;
|
||||
|
||||
@@ -1609,3 +1609,12 @@ body:has(.auswahlleiste:not(.hidden)) .hinweise { bottom: 104px; }
|
||||
font-size: 12px; color: var(--text-muted); flex: 1 1 auto;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Jede Datei eines mehrfach belegten Titels einzeln, mit einem Weg heraus. */
|
||||
.fassungstitel { padding: 4px 0; }
|
||||
.fassungsdatei {
|
||||
display: flex; align-items: center; gap: var(--s-snug);
|
||||
padding: 3px var(--s-snug) 3px 24px;
|
||||
}
|
||||
.fassungsdatei .oz-dazu { flex: 1 1 auto; }
|
||||
.btn.klein { padding: 4px 10px; font-size: 12px; flex: 0 0 auto; }
|
||||
|
||||
+68
-10
@@ -10,10 +10,11 @@ import {
|
||||
servernameSetzen, systemauskunft,
|
||||
laufendeWiedergaben, serverzustand, bildAdresse,
|
||||
speicherorte, bildspeicherLeeren, unzugeordnet, ohneZuordnung, mehrfachfassungen,
|
||||
dateiHerausloesen, type Mehrfachtitel,
|
||||
sprachverteilung, abweichendeAuffrischen,
|
||||
type Bibliothek, type Wiedergabe, ApiFehler,
|
||||
} from '../api';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../util';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung, hinweisZeigen } from '../util';
|
||||
import { dialog, nachfragen } from '../dialog';
|
||||
import { icon, icons } from '../icons';
|
||||
import { kontextmenue, menueAnZiel, type Menuepunkt } from '../menue';
|
||||
@@ -473,16 +474,12 @@ function ohneZuordnungBereich(): HTMLElement {
|
||||
}
|
||||
|
||||
if (mehrfach.gesamt > 0) {
|
||||
liste.appendChild(gruppe(
|
||||
liste.appendChild(fassungsgruppe(
|
||||
`${b.name} — ${anzahl(mehrfach.gesamt, 'Titel')} mit mehreren Dateien`,
|
||||
'Meist zwei Fassungen desselben Films. Stehen hier aber zwei '
|
||||
+ 'verschiedene Filme, lief einer von beiden auf den falschen Treffer — '
|
||||
+ 'dann hilft „Zuordnung korrigieren".',
|
||||
mehrfach.titel.map(t => ({
|
||||
titel: t.titel + (t.jahr ? ` (${t.jahr})` : ''),
|
||||
dazu: t.dateien.join(' · '),
|
||||
tun: () => bearbeiten(t.art === 'series' ? 'series' : 'movies', t.id, () => void laden()),
|
||||
}))));
|
||||
'Meist zwei Fassungen desselben Films — dann ist alles in Ordnung. '
|
||||
+ 'Stehen hier zwei verschiedene Filme, war einer davon nicht mehr zu '
|
||||
+ 'sehen: „Eigener Film" löst ihn wieder heraus.',
|
||||
mehrfach.titel));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -491,6 +488,67 @@ function ohneZuordnungBereich(): HTMLElement {
|
||||
return `${n} ${n === 1 ? wort : (wort === 'Datei' ? 'Dateien' : wort)}`;
|
||||
}
|
||||
|
||||
/** Dieselbe Gruppe, aber jede Datei einzeln mit einem Weg heraus. */
|
||||
function fassungsgruppe(
|
||||
ueberschrift: string, hinweis: string, titel: Mehrfachtitel[],
|
||||
): HTMLElement {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'einstellung';
|
||||
|
||||
const kopf = document.createElement('div');
|
||||
kopf.className = 'zeile';
|
||||
kopf.innerHTML = `<strong>${esc(ueberschrift)}</strong>`
|
||||
+ `<span class="winzig">${esc(hinweis)}</span>`;
|
||||
d.appendChild(kopf);
|
||||
|
||||
const innen = document.createElement('div');
|
||||
innen.className = 'ohnezuordnung';
|
||||
|
||||
titel.forEach(t => {
|
||||
const zeile = document.createElement('div');
|
||||
zeile.className = 'fassungstitel';
|
||||
|
||||
const name = document.createElement('button');
|
||||
name.className = 'ohnezuordnung-zeile';
|
||||
name.innerHTML = `<span class="oz-titel">${esc(t.titel + (t.jahr ? ` (${t.jahr})` : ''))}</span>`;
|
||||
name.onclick = () => bearbeiten(
|
||||
t.art === 'series' ? 'series' : 'movies', t.id, () => void laden());
|
||||
zeile.appendChild(name);
|
||||
|
||||
t.dateien.forEach(f => {
|
||||
const d2 = document.createElement('div');
|
||||
d2.className = 'fassungsdatei';
|
||||
|
||||
const n = document.createElement('span');
|
||||
n.className = 'oz-dazu';
|
||||
n.textContent = f.name;
|
||||
|
||||
// **Nur bei mehr als einer Datei sinnvoll** — und mehr als eine ist
|
||||
// hier die Bedingung, überhaupt in der Liste zu stehen.
|
||||
const raus = knopf('Eigener Film', 'btn klein');
|
||||
raus.onclick = async () => {
|
||||
raus.disabled = true;
|
||||
try {
|
||||
const neu = await dateiHerausloesen(f.id);
|
||||
hinweisZeigen(`„${neu.title}" ist jetzt ein eigener Film — ohne Zuordnung.`);
|
||||
await laden();
|
||||
} catch (fehler) {
|
||||
hinweisZeigen(meldung(fehler), 'fehler');
|
||||
raus.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
d2.append(n, raus);
|
||||
zeile.appendChild(d2);
|
||||
});
|
||||
|
||||
innen.appendChild(zeile);
|
||||
});
|
||||
|
||||
d.appendChild(innen);
|
||||
return d;
|
||||
}
|
||||
|
||||
function gruppe(
|
||||
ueberschrift: string, hinweis: string,
|
||||
zeilen: { titel: string; dazu: string; tun: () => void }[],
|
||||
|
||||
Reference in New Issue
Block a user