Files
jellyfin-ha-src/Emby.Naming/Video/Format3DParser.cs
T
TowyTowy 5be844e1b7 Fix 3D format detection when the tag is the last token of the path
Format3DParser drops the last character of the final path token: when
IndexOfAny finds no more delimiters, the slice is taken with
'index = path.Length - 1', so e.g. "hsbs" is compared as "hsb" and
never matches any rule.

File paths are unaffected because the extension is always the final
token, but directory based media have no extension. For DVD/BluRay
folder rips (BaseVideoResolver parses the folder path via
Set3DFormat), a trailing 3D tag such as
"Gravity (2013) 3d hsbs/BDMV" is silently ignored and Video3DFormat
is never set.

This is a regression from 42a2cc174 which replaced the string.Split
based FlagParser with span slicing; the Split implementation kept the
final token intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:11:29 +02:00

87 lines
2.9 KiB
C#

using System;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parse 3D format related flags.
/// </summary>
public static class Format3DParser
{
// Static default result to save on allocation costs.
private static readonly Format3DResult _defaultResult = new(false, null);
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
{
int oldLen = namingOptions.VideoFlagDelimiters.Length;
Span<char> delimiters = stackalloc char[oldLen + 1];
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' ';
return Parse(path, delimiters, namingOptions);
}
private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
{
foreach (var rule in namingOptions.Format3DRules)
{
var result = Parse(path, rule, delimiters);
if (result.Is3D)
{
return result;
}
}
return _defaultResult;
}
private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
{
bool is3D = false;
string? format3D = null;
// If there's no preceding token we just consider it found
var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
while (path.Length > 0)
{
var index = path.IndexOfAny(delimiters);
ReadOnlySpan<char> currentSlice;
if (index == -1)
{
// No delimiter left, the last token is the remainder of the path
currentSlice = path;
path = default;
}
else
{
currentSlice = path[..index];
path = path[(index + 1)..];
}
if (!foundPrefix)
{
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
continue;
}
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
if (is3D)
{
format3D = rule.Token;
break;
}
}
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}