You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

1632 lines
62 KiB

#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Probing
{
public class ProbeResultNormalizer
{
// When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
private const int MaxSubtitleDescriptionExtractionLength = 100;
private const string ArtistReplaceValue = " | ";
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
private static readonly Regex _performerPattern = new(@"(?<name>.*) \((?<instrument>.*)\)");
private readonly ILogger _logger;
private readonly ILocalizationManager _localization;
private string[] _splitWhiteList;
public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization)
{
_logger = logger;
_localization = localization;
}
private IReadOnlyList<string> SplitWhitelist => _splitWhiteList ??= new string[]
{
"AC/DC",
"A/T/O/S",
"As/Hi Soundworks",
"Au/Ra",
"Bremer/McCoy",
"b/bqスタヂオ",
"DOV/S",
"DJ'TEKINA//SOMETHING",
"IX/ON",
"J-CORE SLi//CER",
"M(a/u)SH",
"Kaoru/Brilliance",
"signum/ii",
"Richiter(LORB/DUGEM DI BARAT)",
"이달의 소녀 1/3",
"R!N / Gemie",
"LOONA 1/3",
"LOONA / yyxy",
"LOONA / ODD EYE CIRCLE",
"K/DA",
"22/7",
"諭吉佳作/men",
"//dARTH nULL",
"Phantom/Ghost",
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
{
var info = new MediaInfo
{
Path = path,
Protocol = protocol,
VideoType = videoType
};
FFProbeHelpers.NormalizeFFProbeResult(data);
SetSize(data, info);
var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>();
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
.Where(i => i is not null)
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
.ToList();
info.MediaAttachments = internalStreams.Select(GetMediaAttachment)
.Where(i => i is not null)
.ToList();
if (data.Format is not null)
{
info.Container = NormalizeFormat(data.Format.FormatName);
if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value))
{
info.Bitrate = value;
}
}
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var tagStreamType = isAudio ? CodecType.Audio : CodecType.Video;
var tagStream = data.Streams?.FirstOrDefault(i => i.CodecType == tagStreamType);
if (tagStream?.Tags is not null)
{
foreach (var (key, value) in tagStream.Tags)
{
tags[key] = value;
}
}
if (data.Format?.Tags is not null)
{
foreach (var (key, value) in data.Format.Tags)
{
tags[key] = value;
}
}
FetchGenres(info, tags);
info.Name = tags.GetFirstNotNullNorWhiteSpaceValue("title", "title-eng");
info.ForcedSortName = tags.GetFirstNotNullNorWhiteSpaceValue("sort_name", "title-sort", "titlesort");
info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc");
info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort");
info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number");
info.ShowName = tags.GetValueOrDefault("show_name");
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
// Several different forms of retail/premiere date
info.PremiereDate =
FFProbeHelpers.GetDictionaryDateTime(tags, "originaldate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date_released") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "date") ??
FFProbeHelpers.GetDictionaryDateTime(tags, "creation_time");
// Set common metadata for music (audio) and music videos (video)
info.Album = tags.GetValueOrDefault("album");
if (tags.TryGetValue("artists", out var artists) && !string.IsNullOrWhiteSpace(artists))
{
info.Artists = SplitDistinctArtists(artists, new[] { '/', ';' }, false).ToArray();
}
else
{
var artist = tags.GetFirstNotNullNorWhiteSpaceValue("artist");
info.Artists = artist is null
? Array.Empty<string>()
: SplitDistinctArtists(artist, _nameDelimiters, true).ToArray();
}
// Guess ProductionYear from PremiereDate if missing
if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue)
{
info.ProductionYear = info.PremiereDate.Value.Year;
}
// Set mediaType-specific metadata
if (isAudio)
{
SetAudioRuntimeTicks(data, info);
// tags are normally located under data.format, but we've seen some cases with ogg where they're part of the info stream
// so let's create a combined list of both
SetAudioInfoFromTags(info, tags);
}
else
{
FetchStudios(info, tags, "copyright");
var iTunExtc = tags.GetFirstNotNullNorWhiteSpaceValue("iTunEXTC");
if (iTunExtc is not null)
{
var parts = iTunExtc.Split('|', StringSplitOptions.RemoveEmptyEntries);
// Example
// mpaa|G|100|For crude humor
if (parts.Length > 1)
{
info.OfficialRating = parts[1];
if (parts.Length > 3)
{
info.OfficialRatingDescription = parts[3];
}
}
}
var iTunXml = tags.GetFirstNotNullNorWhiteSpaceValue("iTunMOVI");