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.
1632 lines
62 KiB
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");
|
|