Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
225 lines
8.9 KiB
C#
225 lines
8.9 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");
|
|
}
|
|
|
|
[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));
|
|
}
|
|
}
|