diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-06-07 22:37:34 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-06-07 23:06:14 +0200 |
| commit | 09723bd123b767b7d99157faa86df0fc305c87ea (patch) | |
| tree | 447c08df31b82a3b79e08610475dd1b958664864 | |
| parent | c242533f4ecdbf1fb04c172751007aab89a8645e (diff) | |
Aggregate alternate versions via GetAllVersions
| -rw-r--r-- | MediaBrowser.Controller/Entities/BaseItem.cs | 31 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Entities/Video.cs | 18 | ||||
| -rw-r--r-- | tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs | 7 |
3 files changed, 42 insertions, 14 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 84ba560900..0a3fdbc1ab 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -87,9 +87,7 @@ namespace MediaBrowser.Controller.Entities Model.Entities.ExtraType.Short }; - // Separators the naming layer treats as version delimiters (Emby.Naming VideoFlagDelimiters), - // used when stripping the shared prefix from an alternate version's media source name. - private static readonly char[] VersionSeparators = [' ', '-', '_', '.']; + private static readonly char[] VersionDelimiters = ['-', '_', '.']; private string _sortName; @@ -1235,13 +1233,13 @@ namespace MediaBrowser.Controller.Entities // Prefer the suffix that differs from the other versions: strip the prefix shared by // all sibling files. This works regardless of folder layout, so it also labels episode // versions that share a season folder (e.g. "Greyscale" instead of the full - // "Show - S01E02 - Title - Greyscale"). The prefix is already retreated to a separator + // "Show - S01E02 - Title - Greyscale"). The prefix is already retreated to a delimiter // boundary (see GetCommonVersionPrefix). if (!string.IsNullOrEmpty(commonPrefix) && displayName.Length > commonPrefix.Length && displayName.StartsWith(commonPrefix, StringComparison.OrdinalIgnoreCase)) { - var name = displayName.AsSpan(commonPrefix.Length).TrimStart(VersionSeparators); + var name = displayName.AsSpan(commonPrefix.Length).TrimStart([' ', .. VersionDelimiters]); if (!name.IsWhiteSpace()) { terms.Add(name.ToString()); @@ -1255,7 +1253,7 @@ namespace MediaBrowser.Controller.Entities var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath); if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase)) { - var name = displayName.AsSpan(containingFolderName.Length).TrimStart(VersionSeparators); + var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', .. VersionDelimiters]); if (!name.IsWhiteSpace()) { terms.Add(name.ToString()); @@ -1341,11 +1339,14 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Computes the case-insensitive longest common prefix of the supplied version file names, - /// retreated to the last separator boundary. Retreating keeps the differing suffix intact: + /// retreated to the last delimiter boundary. Retreating keeps the differing suffix intact: /// it avoids slicing through a word every version shares (e.g. "Grey" in "Greyscale" and /// "Greyish") while still trimming the common part when every version is suffixed (e.g. - /// "- Greyscale" / "- Colorized"). The separators mirror the version delimiters recognised by - /// the naming layer (Emby.Naming VideoFlagDelimiters). + /// "- Greyscale" / "- Colorized"). It prefers a structural delimiter ('-', '_', '.') so a + /// token shared by the descriptors but separated only by spaces (e.g. a common "2160p ") is + /// kept in the label, falling back to a space only when no structural delimiter is shared. The + /// separators mirror the version delimiters recognised by the naming layer (Emby.Naming + /// VideoFlagDelimiters). /// </summary> /// <param name="fileNames">The version file names without extension; must contain at least one entry.</param> /// <returns>The shared prefix retreated to a separator boundary, or an empty string when none is shared.</returns> @@ -1379,12 +1380,22 @@ namespace MediaBrowser.Controller.Entities if (!prefixIsWholeName) { + // Retreat to the last structural delimiter ('-', '_', '.'). var cut = prefix.Length; - while (cut > 0 && Array.IndexOf(VersionSeparators, prefix[cut - 1]) < 0) + while (cut > 0 && Array.IndexOf(VersionDelimiters, prefix[cut - 1]) < 0) { cut--; } + if (cut == 0) + { + cut = prefix.Length; + while (cut > 0 && prefix[cut - 1] != ' ') + { + cut--; + } + } + prefix = prefix[..cut]; } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 168ef7d817..f48de0869a 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -409,16 +409,26 @@ namespace MediaBrowser.Controller.Entities } /// <summary> + /// Gets this video together with all of its alternate versions (local and linked and, when this + /// is itself an alternate, the primary and the primary's other versions), deduplicated. + /// </summary> + /// <returns>This video and every alternate version of it.</returns> + public IReadOnlyList<Video> GetAllVersions() + { + return GetAllItemsForMediaSources() + .Select(i => i.Item) + .OfType<Video>() + .ToList(); + } + + /// <summary> /// Gets the alternate version of this video that matches the supplied item id. /// </summary> /// <param name="itemId">The version item id (the playback media source id).</param> /// <returns>The matching version, or <c>null</c> when the id is not a version of this video.</returns> public Video GetAlternateVersion(Guid itemId) { - return GetAllItemsForMediaSources() - .Select(i => i.Item) - .OfType<Video>() - .FirstOrDefault(i => i.Id.Equals(itemId)); + return GetAllVersions().FirstOrDefault(i => i.Id.Equals(itemId)); } public override string CreatePresentationUniqueKey() diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index 2ee95f076b..664821815b 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -82,6 +82,13 @@ public class BaseItemTests [InlineData("Movie (2020).UHD", "Movie (2020).1080p", "UHD", "1080p")] // Resolution variants that share leading digits must retreat to the separator, not yield "p"/"i". [InlineData("Movie - 1080p", "Movie - 1080i", "1080p", "1080i")] + // A token shared by the descriptors but separated only by spaces (the resolution) must stay in the + // label: retreat to the '-' delimiter, not the interior space, so the resolution is kept. + [InlineData( + "movie (2020) - 2160p Extended", + "movie (2020) - 2160p Original", + "2160p Extended", + "2160p Original")] // Bracketed version labels: the opening bracket is kept in the label. [InlineData( "Blade Runner (1982) [Final Cut] [1080p HEVC AAC]", |
