mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-10 01:38:49 +01:00
Fix version names
This commit is contained in:
@@ -87,6 +87,10 @@ 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 string _sortName;
|
||||
|
||||
private string _forcedSortName;
|
||||
@@ -1099,8 +1103,9 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
}
|
||||
|
||||
var list = GetAllItemsForMediaSources();
|
||||
var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType)).ToList();
|
||||
var list = GetAllItemsForMediaSources().ToList();
|
||||
var commonPrefix = GetCommonNamePrefix(list);
|
||||
var result = list.Select(i => GetVersionInfo(enablePathSubstitution, i.Item, i.MediaSourceType, commonPrefix)).ToList();
|
||||
|
||||
if (IsActiveRecording())
|
||||
{
|
||||
@@ -1128,7 +1133,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return Enumerable.Empty<(BaseItem, MediaSourceType)>();
|
||||
}
|
||||
|
||||
private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type)
|
||||
private MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, BaseItem item, MediaSourceType type, string commonPrefix = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
@@ -1141,7 +1146,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
Protocol = protocol ?? MediaProtocol.File,
|
||||
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
|
||||
MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
|
||||
Name = GetMediaSourceName(item),
|
||||
Name = GetMediaSourceName(item, commonPrefix),
|
||||
Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath,
|
||||
RunTimeTicks = item.RunTimeTicks,
|
||||
Container = item.Container,
|
||||
@@ -1220,7 +1225,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return info;
|
||||
}
|
||||
|
||||
internal string GetMediaSourceName(BaseItem item)
|
||||
internal string GetMediaSourceName(BaseItem item, string commonPrefix = null)
|
||||
{
|
||||
var terms = new List<string>();
|
||||
|
||||
@@ -1228,12 +1233,31 @@ namespace MediaBrowser.Controller.Entities
|
||||
if (item.IsFileProtocol && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
var displayName = System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
if (HasLocalAlternateVersions)
|
||||
|
||||
// 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
|
||||
// boundary (see GetCommonVersionPrefix).
|
||||
if (!string.IsNullOrEmpty(commonPrefix)
|
||||
&& displayName.Length > commonPrefix.Length
|
||||
&& displayName.StartsWith(commonPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var name = displayName.AsSpan(commonPrefix.Length).TrimStart(VersionSeparators);
|
||||
if (!name.IsWhiteSpace())
|
||||
{
|
||||
terms.Add(name.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to the containing folder name (the common layout for movie versions, and
|
||||
// the path taken when no common prefix could be derived).
|
||||
if (terms.Count == 0 && HasLocalAlternateVersions)
|
||||
{
|
||||
var containingFolderName = System.IO.Path.GetFileName(ContainingFolderPath);
|
||||
if (displayName.Length > containingFolderName.Length && displayName.StartsWith(containingFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var name = displayName.AsSpan(containingFolderName.Length).TrimStart([' ', '-']);
|
||||
var name = displayName.AsSpan(containingFolderName.Length).TrimStart(VersionSeparators);
|
||||
if (!name.IsWhiteSpace())
|
||||
{
|
||||
terms.Add(name.ToString());
|
||||
@@ -1290,6 +1314,85 @@ namespace MediaBrowser.Controller.Entities
|
||||
return string.Join('/', terms);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives the prefix shared by the supplied media source items' file names, used to strip the
|
||||
/// common part and surface a short version label per source. Returns null when there are fewer
|
||||
/// than two file-based sources, since there is nothing to differentiate.
|
||||
/// </summary>
|
||||
/// <param name="items">The media source items.</param>
|
||||
/// <returns>The shared prefix, or null when no useful prefix exists.</returns>
|
||||
private static string GetCommonNamePrefix(IReadOnlyList<(BaseItem Item, MediaSourceType MediaSourceType)> items)
|
||||
{
|
||||
var fileNames = new List<string>();
|
||||
foreach (var (item, _) in items)
|
||||
{
|
||||
if (item.IsFileProtocol && !string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
fileNames.Add(System.IO.Path.GetFileNameWithoutExtension(item.Path));
|
||||
}
|
||||
}
|
||||
|
||||
if (fileNames.Count < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var prefix = GetCommonVersionPrefix(fileNames);
|
||||
return string.IsNullOrEmpty(prefix) ? null : prefix;
|
||||
}
|
||||
|
||||
/// <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:
|
||||
/// 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).
|
||||
/// </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>
|
||||
internal static string GetCommonVersionPrefix(IReadOnlyList<string> fileNames)
|
||||
{
|
||||
var prefix = fileNames[0];
|
||||
for (var i = 1; i < fileNames.Count && prefix.Length > 0; i++)
|
||||
{
|
||||
var name = fileNames[i];
|
||||
var length = Math.Min(prefix.Length, name.Length);
|
||||
var common = 0;
|
||||
while (common < length && char.ToUpperInvariant(prefix[common]) == char.ToUpperInvariant(name[common]))
|
||||
{
|
||||
common++;
|
||||
}
|
||||
|
||||
prefix = prefix[..common];
|
||||
}
|
||||
|
||||
// If the common prefix is itself a whole file name then one version is unlabelled (the
|
||||
// base name); the boundary already sits at the end of that name, so don't retreat into it.
|
||||
var prefixIsWholeName = false;
|
||||
for (var i = 0; i < fileNames.Count; i++)
|
||||
{
|
||||
if (fileNames[i].Length == prefix.Length)
|
||||
{
|
||||
prefixIsWholeName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefixIsWholeName)
|
||||
{
|
||||
var cut = prefix.Length;
|
||||
while (cut > 0 && Array.IndexOf(VersionSeparators, prefix[cut - 1]) < 0)
|
||||
{
|
||||
cut--;
|
||||
}
|
||||
|
||||
prefix = prefix[..cut];
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public Task RefreshMetadata(CancellationToken cancellationToken)
|
||||
{
|
||||
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user