39c2885fd6
External VobSub subtitle pairs (.idx and .sub) were only probed via the bare .sub file. In cases where multiple languages are present, this results in missing language metadata. Fix by detecting the matching .idx file during media info resolution to run ffprobe on that file and skip processing the .sub entirely. ffprobe will automatically find the matching (same directory, case-sensitive base) .sub file and process both. Added regression tests covering idx/sub pairing, unpaired files, cross-directory pairs, and language-flagged filenames. Fixes #17745
141 lines
6.5 KiB
C#
141 lines
6.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Emby.Naming.Common;
|
|
using Jellyfin.Extensions;
|
|
using MediaBrowser.Model.Dlna;
|
|
using MediaBrowser.Model.Globalization;
|
|
|
|
namespace Emby.Naming.ExternalFiles
|
|
{
|
|
/// <summary>
|
|
/// External media file parser class.
|
|
/// </summary>
|
|
public class ExternalPathParser
|
|
{
|
|
private readonly NamingOptions _namingOptions;
|
|
private readonly DlnaProfileType _type;
|
|
private readonly ILocalizationManager _localizationManager;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ExternalPathParser"/> class.
|
|
/// </summary>
|
|
/// <param name="localizationManager">The localization manager.</param>
|
|
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
|
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
|
|
public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type)
|
|
{
|
|
_localizationManager = localizationManager;
|
|
_namingOptions = namingOptions;
|
|
_type = type;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse filename and extract information.
|
|
/// </summary>
|
|
/// <param name="path">Path to file.</param>
|
|
/// <param name="extraString">Part of the filename only containing the extra information.</param>
|
|
/// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns>
|
|
public ExternalPathParserResult? ParseFile(string path, string? extraString)
|
|
{
|
|
if (path.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var extension = Path.GetExtension(path.AsSpan());
|
|
|
|
// .idx carries VobSub per-track language metadata. Recognize it here rather
|
|
// than adding it to NamingOptions.SubtitleFileExtensions, which also gates
|
|
// subtitle uploads/saves.
|
|
var isVobSubIndex = _type == DlnaProfileType.Subtitle && extension.Equals(".idx", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!isVobSubIndex
|
|
&& !(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
|
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
|
&& !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var pathInfo = new ExternalPathParserResult(path);
|
|
|
|
if (string.IsNullOrEmpty(extraString))
|
|
{
|
|
return pathInfo;
|
|
}
|
|
|
|
foreach (var separator in _namingOptions.MediaFlagDelimiters)
|
|
{
|
|
var languageString = extraString;
|
|
var titleString = string.Empty;
|
|
const int SeparatorLength = 1;
|
|
|
|
while (languageString.Length > 0)
|
|
{
|
|
int lastSeparator = languageString.LastIndexOf(separator);
|
|
|
|
if (lastSeparator == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
string currentSlice = languageString[lastSeparator..];
|
|
string currentSliceWithoutSeparator = currentSlice[SeparatorLength..];
|
|
|
|
if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
pathInfo.IsDefault = true;
|
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
languageString = languageString[..lastSeparator];
|
|
continue;
|
|
}
|
|
|
|
if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
pathInfo.IsForced = true;
|
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
languageString = languageString[..lastSeparator];
|
|
continue;
|
|
}
|
|
|
|
// Try to translate to three character code
|
|
var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
|
|
|
|
if (culture is not null && pathInfo.Language is null)
|
|
{
|
|
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
|
? culture.Name
|
|
: culture.ThreeLetterISOLanguageName;
|
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else if (culture is not null && pathInfo.Language == "hin")
|
|
{
|
|
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
|
|
pathInfo.IsHearingImpaired = true;
|
|
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
|
? culture.Name
|
|
: culture.ThreeLetterISOLanguageName;
|
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
pathInfo.IsHearingImpaired = true;
|
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else
|
|
{
|
|
titleString = currentSlice + titleString;
|
|
}
|
|
|
|
languageString = languageString[..lastSeparator];
|
|
}
|
|
|
|
pathInfo.Title = titleString.Length >= SeparatorLength ? titleString[SeparatorLength..] : null;
|
|
}
|
|
|
|
return pathInfo;
|
|
}
|
|
}
|
|
}
|