diff --git a/Jellyfin/backend/Services/GameThumbService.cs b/Jellyfin/backend/Services/GameThumbService.cs index aa18ba2..af13005 100644 --- a/Jellyfin/backend/Services/GameThumbService.cs +++ b/Jellyfin/backend/Services/GameThumbService.cs @@ -149,9 +149,9 @@ private static string BuildUrl(string platform, ThumbKind kind, string name) => /// minus the extension. This is the filename rather than the display title, which for a game /// in its own folder is the folder name and would not match. /// - private static string LibretroThumbName(string romFileName) + private static string LibretroThumbName(string name) { - var chars = (Path.GetFileNameWithoutExtension(romFileName) ?? string.Empty).ToCharArray(); + var chars = name.ToCharArray(); for (var i = 0; i < chars.Length; i++) { if (Array.IndexOf(ReservedChars, chars[i]) >= 0) diff --git a/Jellyfin/backend/Services/GamesService.cs b/Jellyfin/backend/Services/GamesService.cs index 35e1734..520a3b2 100644 --- a/Jellyfin/backend/Services/GamesService.cs +++ b/Jellyfin/backend/Services/GamesService.cs @@ -515,7 +515,13 @@ public IReadOnlyList GetGames(string libraryId, string? systemId) var systemDir = FindSystemDir(library, romPath); var systemName = systemDir == null ? string.Empty : Path.GetFileName(systemDir); var core = ResolveSystemCore(systemName, new List { romPath }); - return (core, Path.GetFileName(romPath)); + + // Resolve the exact No-Intro name from Rdb if available so region codes and tags (like (USA)) are included in the art URL. + var title = ResolveTitle(systemDir, romPath); + var rdb = _rdb?.TryLookup(core, romPath, title); + var name = rdb?.Name ?? Path.GetFileNameWithoutExtension(romPath); + + return (core, name); } /// diff --git a/Jellyfin/backend/Services/LaunchBoxService.cs b/Jellyfin/backend/Services/LaunchBoxService.cs index 7d58042..6ee12aa 100644 Binary files a/Jellyfin/backend/Services/LaunchBoxService.cs and b/Jellyfin/backend/Services/LaunchBoxService.cs differ diff --git a/Jellyfin/backend/Services/RdbService.cs b/Jellyfin/backend/Services/RdbService.cs index 31edf1c..6a1f16a 100644 --- a/Jellyfin/backend/Services/RdbService.cs +++ b/Jellyfin/backend/Services/RdbService.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; +using System.Globalization; using System.Net.Http; +using System.Text; using Microsoft.Extensions.Logging; namespace Moonfin.Server.Services; @@ -152,6 +154,40 @@ public RdbService(IHttpClientFactory httpClientFactory, ILogger logg } } + // Fallback when the exact match misses: take the closest name where one + // side is a prefix of the other, to absorb region or edition suffixes. + // Rdb keeps region parentheticals, so it allows a wider gap than LaunchBox. + if (fileName.Length >= 5) + { + RdbRecord? bestRecord = null; + var minDiff = int.MaxValue; + var ambiguous = false; + foreach (var kv in index.ByName) + { + if (kv.Key.StartsWith(fileName, StringComparison.Ordinal) || + fileName.StartsWith(kv.Key, StringComparison.Ordinal)) + { + var diff = Math.Abs(kv.Key.Length - fileName.Length); + if (diff < minDiff) + { + minDiff = diff; + bestRecord = kv.Value; + ambiguous = false; + } + else if (diff == minDiff && !ReferenceEquals(kv.Value, bestRecord)) + { + // Two different games are equally close, so a common prefix + // can't be resolved to one. Skip rather than pick wrong art. + ambiguous = true; + } + } + } + if (bestRecord != null && !ambiguous && minDiff <= 45) + { + return bestRecord; + } + } + return null; } @@ -350,9 +386,16 @@ private static uint[] BuildCrcTable() private static string NormalizeName(string value) { + var normalizedString = value.Normalize(NormalizationForm.FormD); var sb = new System.Text.StringBuilder(value.Length); - foreach (var ch in value) + foreach (var ch in normalizedString) { + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(ch); + if (unicodeCategory == UnicodeCategory.NonSpacingMark) + { + continue; + } + if (char.IsLetterOrDigit(ch)) { sb.Append(char.ToLowerInvariant(ch));