Zwei Dinge, die zusammengehören. **Den Bilderdialog gab es nur an der Kachel.** Wer auf der Seite eines Films stand und sah, dass das Plakat falsch ist, stand genau dort, wo er nichts dagegen tun konnte — der Weg führte zurück in die Liste und über das Dreipunktmenü. Jetzt steht „Bearbeiten" in der Knopfreihe der Detailseite, bei Filmen wie bei Serien, und nur im Kopf: dieselbe Reihe steht auch über jeder Fassung und jeder Folge. **Und es waren vierundzwanzig, weil dort vierundzwanzig stand.** Bei „365 Tage" hat TMDB neunundsechzig Plakate. Dazu ein zweiter, leiserer Filter: gefragt wurden nur drei Sprachen. Fürs automatische Holen ist das richtig — dort wird eines genommen, und es soll passen. Wer von Hand wählt, will sehen, was es gibt; sortiert wird ohnehin nach Sprache, also steht das Passende weiterhin oben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
341 lines
13 KiB
C#
341 lines
13 KiB
C#
using System.Net;
|
|
using Flaro.Core.Metadata;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Flaro.Metadata.Tests;
|
|
|
|
public class TmdbClientTests
|
|
{
|
|
private static TmdbClient Client(Attrappe attrappe, ITmdbSettingsSource? einstellungen = null) =>
|
|
new(new HttpClient(attrappe) { BaseAddress = new Uri(TmdbClient.BaseAddress) },
|
|
einstellungen ?? FesteEinstellungen.MitSchluessel(),
|
|
NullLogger<TmdbClient>.Instance);
|
|
|
|
private const string FilmTreffer = """
|
|
{"results":[{"id":603,"title":"Matrix","release_date":"1999-03-30",
|
|
"overview":"Neo zweifelt an der Wirklichkeit.",
|
|
"poster_path":"/abc.jpg","backdrop_path":"/def.jpg"}]}
|
|
""";
|
|
|
|
[Test]
|
|
public async Task Ohne_Schluessel_wird_nichts_gefragt()
|
|
{
|
|
var attrappe = Attrappe.MitJson(FilmTreffer);
|
|
var client = Client(attrappe, FesteEinstellungen.Ohne);
|
|
|
|
Assert.That(await client.IsConfiguredAsync(), Is.False);
|
|
Assert.That(await client.FindMovieAsync("Matrix", 1999), Is.Null);
|
|
Assert.That(attrappe.Aufrufe, Is.Empty, "Ohne Schlüssel darf keine Anfrage hinausgehen");
|
|
}
|
|
|
|
/// <summary>Die Bilderliste für die Auswahl von Hand.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Zwei Grenzen, die beide störten.</b> Die Anfrage holte nur drei
|
|
/// Sprachen, und gezählt wurde bei vierundzwanzig je Art zu Ende — bei
|
|
/// „365 Tage" hat TMDB neunundsechzig Plakate, und die fehlenden
|
|
/// fünfundvierzig sah niemand. Wer von Hand wählt, soll sehen, was es
|
|
/// gibt.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Die_Bilderliste_ist_nicht_bei_vierundzwanzig_zu_Ende()
|
|
{
|
|
var plakate = string.Join(",", Enumerable.Range(0, 69)
|
|
.Select(i => $$"""{"file_path":"/p{{i}}.jpg","iso_639_1":"en","vote_average":1.0}"""));
|
|
var attrappe = Attrappe.MitJson($$"""{"posters":[{{plakate}}],"backdrops":[],"logos":[]}""");
|
|
|
|
var bilder = await Client(attrappe).BilderAsync(603, serie: false);
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(bilder.Count(b => b.Kind == "poster"), Is.EqualTo(69));
|
|
Assert.That(attrappe.Aufrufe[0], Does.Not.Contain("include_image_language"),
|
|
"Die Auswahl von Hand soll alle Sprachen sehen");
|
|
});
|
|
}
|
|
|
|
/// <summary>Die passende Sprache steht trotzdem oben.</summary>
|
|
[Test]
|
|
public async Task Die_eigene_Sprache_steht_vorn()
|
|
{
|
|
var attrappe = Attrappe.MitJson("""
|
|
{"posters":[
|
|
{"file_path":"/en.jpg","iso_639_1":"en","vote_average":9.0},
|
|
{"file_path":"/de.jpg","iso_639_1":"de","vote_average":1.0},
|
|
{"file_path":"/ja.jpg","iso_639_1":"ja","vote_average":8.0}],
|
|
"backdrops":[],"logos":[]}
|
|
""");
|
|
|
|
var bilder = await Client(attrappe, FesteEinstellungen.MitSchluessel("de-DE"))
|
|
.BilderAsync(603, serie: false);
|
|
|
|
Assert.That(bilder[0].Url, Does.Contain("/de.jpg"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ein_Film_wird_gelesen()
|
|
{
|
|
var film = await Client(Attrappe.MitJson(FilmTreffer)).FindMovieAsync("Matrix", 1999);
|
|
|
|
Assert.That(film, Is.Not.Null);
|
|
Assert.That(film!.ProviderId, Is.EqualTo(603));
|
|
Assert.That(film.Title, Is.EqualTo("Matrix"));
|
|
Assert.That(film.Year, Is.EqualTo(1999));
|
|
Assert.That(film.Overview, Does.StartWith("Neo zweifelt"));
|
|
Assert.That(film.Poster!.Url, Is.EqualTo("https://image.tmdb.org/t/p/w500/abc.jpg"));
|
|
Assert.That(film.Backdrop!.Url, Is.EqualTo("https://image.tmdb.org/t/p/w1280/def.jpg"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Titel_Jahr_und_Sprache_landen_in_der_Anfrage()
|
|
{
|
|
var attrappe = Attrappe.MitJson(FilmTreffer);
|
|
await Client(attrappe, FesteEinstellungen.MitSchluessel("de-DE")).FindMovieAsync("Der Untergang", 2004);
|
|
|
|
// Die erste Anfrage ist die Suche; danach folgt der Nachschlag der
|
|
// IMDb-Kennung für die NFO-Datei.
|
|
var aufruf = attrappe.Aufrufe[0];
|
|
Assert.That(aufruf, Does.Contain("query=Der%20Untergang"));
|
|
Assert.That(aufruf, Does.Contain("year=2004"));
|
|
Assert.That(aufruf, Does.Contain("language=de-DE"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ohne_Jahr_wird_keines_mitgeschickt()
|
|
{
|
|
var attrappe = Attrappe.MitJson(FilmTreffer);
|
|
await Client(attrappe).FindMovieAsync("Matrix", null);
|
|
Assert.That(attrappe.Aufrufe[0], Does.Not.Contain("year="));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_leere_Trefferliste_ergibt_nichts()
|
|
{
|
|
Assert.That(await Client(Attrappe.MitJson("""{"results":[]}""")).FindMovieAsync("Gibtesnicht", null), Is.Null);
|
|
}
|
|
|
|
/// <summary>Ein falscher Schlüssel muss auffallen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>„Nichts gefunden" und „dein Schlüssel ist falsch" sehen in der
|
|
/// Mediathek gleich aus.</b> Ohne diese Unterscheidung sucht jemand
|
|
/// stundenlang nach dem Grund, warum keine Bilder erscheinen.
|
|
/// </remarks>
|
|
[TestCase(HttpStatusCode.Unauthorized)]
|
|
[TestCase(HttpStatusCode.Forbidden)]
|
|
public void Ein_zurueckgewiesener_Schluessel_wirft(HttpStatusCode code)
|
|
{
|
|
var client = Client(Attrappe.MitStatus(code));
|
|
var fehler = Assert.ThrowsAsync<MetadataCredentialsException>(() => client.FindMovieAsync("Matrix", null));
|
|
Assert.That(fehler!.Message, Does.Contain("Schlüssel"));
|
|
}
|
|
|
|
/// <summary>Alles andere ist kein Grund, den Scan abzubrechen.</summary>
|
|
[TestCase(HttpStatusCode.NotFound)]
|
|
[TestCase(HttpStatusCode.InternalServerError)]
|
|
[TestCase(HttpStatusCode.TooManyRequests)]
|
|
[TestCase(HttpStatusCode.ServiceUnavailable)]
|
|
public async Task Andere_Fehler_ergeben_still_nichts(HttpStatusCode code)
|
|
{
|
|
Assert.That(await Client(Attrappe.MitStatus(code)).FindMovieAsync("Matrix", null), Is.Null);
|
|
}
|
|
|
|
/// <summary>Ein Anmeldeportal im Hotel-WLAN antwortet mit HTML und Status 200.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Darauf mit einer Ausnahme zu reagieren hiesse, den ganzen Scan an einem
|
|
/// fremden Netz scheitern zu lassen.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Unsinn_statt_JSON_wirft_nicht()
|
|
{
|
|
var attrappe = Attrappe.MitJson("<html><body>Bitte anmelden</body></html>");
|
|
Assert.That(await Client(attrappe).FindMovieAsync("Matrix", null), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Kein_Netz_ergibt_nichts_statt_eines_Absturzes()
|
|
{
|
|
var attrappe = Attrappe.DieWirft(new HttpRequestException("Name or service not known"));
|
|
Assert.That(await Client(attrappe).FindMovieAsync("Matrix", null), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_Serie_wird_gelesen()
|
|
{
|
|
var json = """
|
|
{"results":[{"id":70523,"name":"Dark","first_air_date":"2017-12-01",
|
|
"overview":"Vier Familien.","poster_path":"/p.jpg","backdrop_path":null}]}
|
|
""";
|
|
var serie = await Client(Attrappe.MitJson(json)).FindSeriesAsync("Dark", null);
|
|
|
|
Assert.That(serie!.ProviderId, Is.EqualTo(70523));
|
|
Assert.That(serie.Title, Is.EqualTo("Dark"));
|
|
Assert.That(serie.Year, Is.EqualTo(2017));
|
|
Assert.That(serie.Poster!.Url, Does.EndWith("/p.jpg"));
|
|
Assert.That(serie.Backdrop, Is.Null, "Ein fehlendes Bild ist null, keine kaputte Adresse");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_Folge_wird_gelesen()
|
|
{
|
|
var attrappe = Attrappe.MitJson("""
|
|
{"name":"Geheimnisse","overview":"Ein Junge verschwindet.","still_path":"/s.jpg"}
|
|
""");
|
|
var folge = await Client(attrappe).FindEpisodeAsync(70523, 1, 1);
|
|
|
|
Assert.That(folge!.Title, Is.EqualTo("Geheimnisse"));
|
|
Assert.That(folge.Still!.Url, Is.EqualTo("https://image.tmdb.org/t/p/w300/s.jpg"));
|
|
Assert.That(attrappe.Aufrufe.Single(), Does.Contain("tv/70523/season/1/episode/1"));
|
|
}
|
|
|
|
/// <summary>Eine Folge, die TMDB nicht kennt, ist kein Fehler.</summary>
|
|
[Test]
|
|
public async Task Eine_unbekannte_Folge_ergibt_nichts()
|
|
{
|
|
Assert.That(await Client(Attrappe.MitStatus(HttpStatusCode.NotFound)).FindEpisodeAsync(1, 99, 99), Is.Null);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Leere_Felder_werden_zu_null_statt_zu_Leerstrings()
|
|
{
|
|
var json = """{"results":[{"id":1,"title":"Ohne","release_date":"","overview":"","poster_path":""}]}""";
|
|
var film = await Client(Attrappe.MitJson(json)).FindMovieAsync("Ohne", null);
|
|
|
|
Assert.That(film!.Overview, Is.Null);
|
|
Assert.That(film.Year, Is.Null);
|
|
Assert.That(film.Poster, Is.Null);
|
|
}
|
|
}
|
|
|
|
/// <summary>Welcher Treffer gewinnt, wenn mehrere in Frage kommen.</summary>
|
|
public class TrefferwahlTests
|
|
{
|
|
private static TmdbClient Client(Attrappe attrappe) =>
|
|
new(new HttpClient(attrappe) { BaseAddress = new Uri(TmdbClient.BaseAddress) },
|
|
FesteEinstellungen.MitSchluessel(), NullLogger<TmdbClient>.Instance);
|
|
|
|
/// <summary>Am echten Dienst gemessen, nicht vermutet.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Die Suche nach „Dark" lieferte „Dark Shadows" von 1966.</b> TMDB
|
|
/// sortiert nach Beliebtheit, und ohne Jahresangabe führt das in die Irre.
|
|
/// Ein Treffer mit genau diesem Namen ist gemeint, auch wenn er weiter
|
|
/// unten steht.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ein_genau_passender_Name_sticht_den_beliebteren()
|
|
{
|
|
var attrappe = Attrappe.MitJson("""
|
|
{"results":[
|
|
{"id":1,"name":"Dark Shadows","first_air_date":"1966-06-27"},
|
|
{"id":2,"name":"Dark","first_air_date":"2017-12-01"}
|
|
]}
|
|
""");
|
|
|
|
var serie = await Client(attrappe).FindSeriesAsync("Dark", null);
|
|
Assert.That(serie!.ProviderId, Is.EqualTo(2));
|
|
Assert.That(serie.Year, Is.EqualTo(2017));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Gross_und_Kleinschreibung_spielt_dabei_keine_Rolle()
|
|
{
|
|
var attrappe = Attrappe.MitJson("""
|
|
{"results":[{"id":1,"title":"Irgendwas"},{"id":2,"title":"DER UNTERGANG"}]}
|
|
""");
|
|
|
|
var film = await Client(attrappe).FindMovieAsync("Der Untergang", null);
|
|
Assert.That(film!.ProviderId, Is.EqualTo(2));
|
|
}
|
|
|
|
/// <summary>Ohne genaue Entsprechung bleibt es beim beliebtesten.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Eine eigene Ähnlichkeitsbewertung zu bauen hiesse, TMDBs Rangfolge
|
|
/// nachzubauen — mit weniger Daten.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task Ohne_genaue_Entsprechung_gewinnt_der_erste()
|
|
{
|
|
var attrappe = Attrappe.MitJson("""
|
|
{"results":[{"id":1,"title":"Der Untergang des Hauses Usher"},{"id":2,"title":"Untergang"}]}
|
|
""");
|
|
|
|
var film = await Client(attrappe).FindMovieAsync("Der Untergang", null);
|
|
Assert.That(film!.ProviderId, Is.EqualTo(1));
|
|
}
|
|
}
|
|
|
|
/// <summary>Was versucht wird, wenn der Titel so nicht gefunden wird.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Die Fälle stammen aus dem echten Bestand und sind an TMDB nachgemessen.</b>
|
|
/// „highschool musical 3" liefert dort null Treffer, „high school musical 3"
|
|
/// zwei. „Harry Potter im Orden des Phoenix" null, „Harry Potter" zwanzig.
|
|
/// Und „dr dolittle" dreizehn — der fehlende Punkt war nie das Problem.
|
|
/// </remarks>
|
|
public class SuchvariantenTests
|
|
{
|
|
[Test]
|
|
public void Der_Titel_selbst_kommt_zuerst()
|
|
{
|
|
Assert.That(Suchvarianten.Fuer("Dr. Dolittle").First(), Is.EqualTo("Dr. Dolittle"));
|
|
}
|
|
|
|
[Test]
|
|
public void Ein_zusammengeschriebenes_Wort_wird_getrennt()
|
|
{
|
|
Assert.That(Suchvarianten.Fuer("highschool musical 3"),
|
|
Does.Contain("high school musical 3"));
|
|
}
|
|
|
|
/// <summary>Von der Mitte nach aussen — der richtige Schnitt kommt früh.</summary>
|
|
[Test]
|
|
public void Der_richtige_Schnitt_kommt_in_den_ersten_Versuchen()
|
|
{
|
|
var alle = Suchvarianten.Fuer("highschool musical 3").ToList();
|
|
Assert.That(alle.IndexOf("high school musical 3"), Is.LessThanOrEqualTo(3));
|
|
}
|
|
|
|
[Test]
|
|
public void Hintere_Worte_fallen_weg()
|
|
{
|
|
Assert.That(Suchvarianten.Fuer("Harry Potter im Orden des Phoenix"),
|
|
Does.Contain("Harry Potter"));
|
|
}
|
|
|
|
/// <summary>Unter zwei Worten hört es auf — „Harry" allein taugt nicht.</summary>
|
|
[Test]
|
|
public void Ein_einzelnes_Wort_wird_nicht_gesucht()
|
|
{
|
|
Assert.That(Suchvarianten.Fuer("Harry Potter im Orden des Phoenix"),
|
|
Does.Not.Contain("Harry"));
|
|
}
|
|
|
|
[Test]
|
|
public void Es_wird_nicht_endlos_probiert()
|
|
{
|
|
Assert.That(Suchvarianten.Fuer("Ein ziemlich langer Titel mit vielen Worten darin"),
|
|
Has.Exactly(10).Items);
|
|
}
|
|
|
|
/// <summary>Phoenix findet Phönix: die Akzente fallen weg.</summary>
|
|
[Test]
|
|
public void Aehnlichkeit_sieht_ueber_Akzente_hinweg()
|
|
{
|
|
var richtig = Suchvarianten.Aehnlichkeit(
|
|
"Harry Potter im Orden des Phoenix", "Harry Potter und der Orden des Phönix");
|
|
var falsch = Suchvarianten.Aehnlichkeit(
|
|
"Harry Potter im Orden des Phoenix", "Harry Potter: Fireplace");
|
|
|
|
Assert.That(richtig, Is.GreaterThan(falsch));
|
|
}
|
|
|
|
[Test]
|
|
public void Nichts_Gemeinsames_ergibt_null()
|
|
{
|
|
Assert.That(Suchvarianten.Aehnlichkeit("Dune", "Alles steht Kopf"), Is.Zero);
|
|
}
|
|
}
|