Updated MediaInfo schema and revised logic that Formats it. Also added logic to log events to Sentry.

This commit is contained in:
Taloth Saldono 2017-07-30 21:30:34 +02:00
parent 94f2473fbb
commit 27dca830cc
15 changed files with 338 additions and 80 deletions

View File

@ -78,6 +78,16 @@ namespace NzbDrone.Common.Extensions
return !string.IsNullOrWhiteSpace(text);
}
public static bool StartsWithIgnoreCase(this string text, string startsWith)
{
return text.StartsWith(startsWith, StringComparison.InvariantCultureIgnoreCase);
}
public static bool EqualsIgnoreCase(this string text, string equals)
{
return text.Equals(equals, StringComparison.InvariantCultureIgnoreCase);
}
public static bool ContainsIgnoreCase(this string text, string contains)
{
return text.IndexOf(contains, StringComparison.InvariantCultureIgnoreCase) > -1;
@ -118,4 +128,4 @@ namespace NzbDrone.Common.Extensions
return Encoding.ASCII.GetString(new [] { byteResult });
}
}
}
}

View File

@ -1,4 +1,5 @@
using NLog;
using NLog.Fluent;
namespace NzbDrone.Common.Instrumentation.Extensions
{

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NLog.Fluent;
namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class SentryLoggerExtensions
{
public static readonly Logger SentryLogger = LogManager.GetLogger("Sentry");
public static LogBuilder SentryFingerprint(this LogBuilder logBuilder, params string[] fingerprint)
{
return logBuilder.Property("Sentry", fingerprint);
}
public static LogBuilder WriteSentryDebug(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Debug, fingerprint);
}
public static LogBuilder WriteSentryInfo(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Info, fingerprint);
}
public static LogBuilder WriteSentryWarn(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Warn, fingerprint);
}
public static LogBuilder WriteSentryError(this LogBuilder logBuilder, params string[] fingerprint)
{
return LogSentryMessage(logBuilder, LogLevel.Error, fingerprint);
}
private static LogBuilder LogSentryMessage(LogBuilder logBuilder, LogLevel level, string[] fingerprint)
{
SentryLogger.Log(level)
.CopyLogEvent(logBuilder.LogEventInfo)
.SentryFingerprint(fingerprint)
.Write();
return logBuilder.Property("Sentry", null);
}
private static LogBuilder CopyLogEvent(this LogBuilder logBuilder, LogEventInfo logEvent)
{
return logBuilder.LoggerName(logEvent.LoggerName)
.TimeStamp(logEvent.TimeStamp)
.Message(logEvent.Message, logEvent.Parameters)
.Properties((Dictionary<object, object>)logEvent.Properties)
.Exception(logEvent.Exception);
}
}
}

View File

@ -109,9 +109,13 @@ namespace NzbDrone.Common.Instrumentation
Layout = "${message}"
};
var loggingRule = new LoggingRule("*", updateClient ? LogLevel.Trace : LogLevel.Error, target);
var loggingRule = new LoggingRule("*", updateClient ? LogLevel.Trace : LogLevel.Warn, target);
LogManager.Configuration.AddTarget("sentryTarget", target);
LogManager.Configuration.LoggingRules.Add(loggingRule);
// Events logged to Sentry go only to Sentry.
var loggingRuleSentry = new LoggingRule("Sentry", LogLevel.Debug, target) { Final = true };
LogManager.Configuration.LoggingRules.Insert(0, loggingRuleSentry);
}
private static void RegisterDebugger()

View File

@ -71,6 +71,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private static List<string> GetFingerPrint(LogEventInfo logEvent)
{
if (logEvent.Properties.ContainsKey("Sentry"))
{
return ((string[])logEvent.Properties["Sentry"]).ToList();
}
var fingerPrint = new List<string>
{
logEvent.Level.Ordinal.ToString(),
@ -94,13 +99,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
return fingerPrint;
}
private bool IsSentryMessage(LogEventInfo logEvent)
{
if (logEvent.Properties.ContainsKey("Sentry"))
{
return logEvent.Properties["Sentry"] != null;
}
if (logEvent.Level >= LogLevel.Error && logEvent.Exception != null)
{
return true;
}
return false;
}
protected override void Write(LogEventInfo logEvent)
{
if (_unauthorized)
{
return;
}
try
{
// don't report non-critical events without exceptions
if (logEvent.Exception == null || _unauthorized)
if (!IsSentryMessage(logEvent))
{
return;
}
@ -112,6 +137,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
}
var extras = logEvent.Properties.ToDictionary(x => x.Key.ToString(), x => x.Value.ToString());
extras.Remove("Sentry");
_client.Logger = logEvent.LoggerName;
var sentryMessage = new SentryMessage(logEvent.Message, logEvent.Parameters);
@ -134,11 +160,16 @@ namespace NzbDrone.Common.Instrumentation.Sentry
sentryEvent.Fingerprint.Add(logEvent.Exception.GetType().FullName);
}
if (logEvent.Properties.ContainsKey("Sentry"))
{
sentryEvent.Fingerprint.Clear();
Array.ForEach((string[])logEvent.Properties["Sentry"], sentryEvent.Fingerprint.Add);
}
var osName = Environment.GetEnvironmentVariable("OS_NAME");
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
sentryEvent.Tags.Add("os_name", osName);
sentryEvent.Tags.Add("os_version", $"{osName} {osVersion}");
sentryEvent.Tags.Add("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");

View File

@ -177,7 +177,8 @@
<Compile Include="Http\UserAgentBuilder.cs" />
<Compile Include="Instrumentation\CleanseLogMessage.cs" />
<Compile Include="Instrumentation\CleansingJsonVisitor.cs" />
<Compile Include="Instrumentation\Extensions\LoggerProgressExtensions.cs" />
<Compile Include="Instrumentation\Extensions\LoggerExtensions.cs" />
<Compile Include="Instrumentation\Extensions\SentryLoggerExtensions.cs" />
<Compile Include="Instrumentation\GlobalExceptionHandlers.cs" />
<Compile Include="Instrumentation\LogEventExtensions.cs" />
<Compile Include="Instrumentation\NzbDroneFileTarget.cs" />

View File

@ -41,11 +41,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "Other Audio Format"
AudioFormat = "Other Audio Format",
AudioCodecID = "Other Audio Codec"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(mediaInfoModel.AudioFormat);
ExceptionVerification.ExpectedErrors(1);
ExceptionVerification.ExpectedWarns(1);
}
}
}

View File

@ -26,15 +26,15 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
}
[Test]
public void should_return_VideoCodec_by_default()
public void should_return_VideoFormat_by_default()
{
var mediaInfoModel = new MediaInfoModel
{
VideoCodec = "VideoCodec"
VideoFormat = "VideoCodec"
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, null).Should().Be(mediaInfoModel.VideoCodec);
ExceptionVerification.ExpectedErrors(1);
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, null).Should().Be(mediaInfoModel.VideoFormat);
ExceptionVerification.ExpectedWarns(1);
}
}
}

View File

@ -60,7 +60,33 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
.All()
.With(v => v.RelativePath = "media.mkv")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = 3 })
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = UpdateMediaInfoService.CURRENT_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesBySeries(1))
.Returns(episodeFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new SeriesScannedEvent(_series));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<EpisodeFile>()), Times.Exactly(2));
}
[Test]
public void should_skip_not_yet_date_media_info()
{
var episodeFiles = Builder<EpisodeFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.mkv")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = UpdateMediaInfoService.MINIMUM_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()

View File

@ -3,8 +3,10 @@ using System.Globalization;
using System.IO;
using System.Linq;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
@ -41,6 +43,74 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.AudioCodecID == null)
{
return FormatAudioCodecLegacy(mediaInfo, sceneName);
}
var audioFormat = mediaInfo.AudioFormat;
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
var audioProfile = mediaInfo.AudioProfile ?? string.Empty;
var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty;
if (audioFormat.IsNullOrWhiteSpace())
{
return string.Empty;
}
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
return "AC3";
}
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
return "EAC3";
}
if (audioFormat.EqualsIgnoreCase("AAC"))
{
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
{
return "HE-AAC";
}
return "AAC";
}
if (audioFormat.EqualsIgnoreCase("DTS"))
{
return "DTS";
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
return "FLAC";
}
if (audioFormat.EqualsIgnoreCase("MPEG Audio"))
{
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
{
return "MP3";
}
if (mediaInfo.AudioProfile == "Layer 2")
{
return "MP2";
}
}
Logger.Debug()
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioCodecLibrary), sceneName)
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, audioFormat, audioCodecID)
.Write();
return audioFormat;
}
public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var audioFormat = mediaInfo.AudioFormat;
@ -49,51 +119,120 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return audioFormat;
}
if (audioFormat == "AC-3")
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
return "AC3";
}
if (audioFormat == "E-AC-3")
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
return "EAC3";
}
if (audioFormat == "AAC")
if (audioFormat.EqualsIgnoreCase("AAC"))
{
return "AAC";
}
if (audioFormat == "MPEG Audio")
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
{
return mediaInfo.AudioProfile == "Layer 3" ? "MP3" : audioFormat;
return "MP3";
}
if (audioFormat == "DTS")
if (audioFormat.EqualsIgnoreCase("DTS"))
{
return "DTS";
}
if (audioFormat.Equals("FLAC", StringComparison.OrdinalIgnoreCase))
if (audioFormat.EqualsIgnoreCase("TrueHD"))
{
return "TrueHD";
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
return "FLAC";
}
if (audioFormat.Equals("Vorbis", StringComparison.OrdinalIgnoreCase))
if (audioFormat.EqualsIgnoreCase("Vorbis"))
{
return "Vorbis";
}
if (audioFormat.Equals("Opus", StringComparison.OrdinalIgnoreCase))
if (audioFormat.EqualsIgnoreCase("Opus"))
{
return "Opus";
}
Logger.Error(new UnknownCodecException(audioFormat, sceneName), "Unknown audio format: {0} in '{1}'. Please notify Sonarr developers.", audioFormat, sceneName);
return audioFormat;
}
public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.VideoFormat == null)
{
return FormatVideoCodecLegacy(mediaInfo, sceneName);
}
var videoFormat = mediaInfo.VideoFormat;
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
var videoProfile = mediaInfo.VideoProfile ?? string.Empty;
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
if (videoFormat.IsNullOrWhiteSpace())
{
return videoFormat;
}
if (videoFormat == "AVC")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
{
return "x264";
}
return GetSceneNameMatch(sceneName, "AVC", "h264");
}
if (videoFormat == "HEVC")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
{
return "x265";
}
return GetSceneNameMatch(sceneName, "HEVC", "h265");
}
if (videoFormat == "MPEG-2 Video")
{
return "MPEG2";
}
if (videoFormat == "MPEG-4 Visual")
{
if (videoCodecID.ContainsIgnoreCase("XVID"))
{
return "XviD";
}
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
videoCodecID.ContainsIgnoreCase("DIVX") ||
videoCodecID.ContainsIgnoreCase("DX50"))
{
return "DivX";
}
}
Logger.Debug()
.Message("Unknown video format: '{0}' in '{1}'.", string.Join(", ", videoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName)
.WriteSentryWarn("UnknownVideoFormat", mediaInfo.ContainerFormat, videoFormat, videoCodecID)
.Write();
return videoFormat;
}
public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var videoCodec = mediaInfo.VideoCodec;
@ -104,16 +243,12 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (videoCodec == "AVC")
{
return sceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(sceneName).Contains("h264")
? "h264"
: "x264";
return GetSceneNameMatch(sceneName, "AVC", "h264", "x264");
}
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
{
return sceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(sceneName).ContainsIgnoreCase("h265")
? "h265"
: "x265";
return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265");
}
if (videoCodec == "MPEG-2 Video")
@ -123,28 +258,41 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (videoCodec == "MPEG-4 Visual")
{
return sceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(sceneName).ContainsIgnoreCase("DivX")
? "DivX"
: "XviD";
return GetSceneNameMatch(sceneName, "DivX", "XviD");
}
if (videoCodec.StartsWith("XviD", StringComparison.OrdinalIgnoreCase))
if (videoCodec.StartsWithIgnoreCase("XviD"))
{
return "XviD";
}
if (videoCodec.StartsWith("DivX", StringComparison.OrdinalIgnoreCase))
if (videoCodec.StartsWithIgnoreCase("DivX"))
{
return "DivX";
}
if (videoCodec.Equals("VC-1", StringComparison.OrdinalIgnoreCase))
if (videoCodec.EqualsIgnoreCase("VC-1"))
{
return "VC1";
}
Logger.Error(new UnknownCodecException(videoCodec, sceneName), "Unknown video codec: {0} in '{1}'. Please notify Sonarr developers.", videoCodec, sceneName);
return videoCodec;
}
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
{
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Path.GetFileNameWithoutExtension(sceneName) : string.Empty;
foreach (var token in tokens)
{
if (sceneName.ContainsIgnoreCase(token))
{
return token;
}
}
// Last token is the default.
return tokens.Last();
}
}
}

View File

@ -9,12 +9,20 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public class MediaInfoModel : IEmbeddedDocument
{
public string ContainerFormat { get; set; }
// Deprecated according to MediaInfo
public string VideoCodec { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public string VideoCodecLibrary { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioCodecLibrary { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public class UnknownCodecException : Exception
{
public string Codec { get; set; }
public string SceneName { get; set; }
public UnknownCodecException(string codec, string sceneName)
: base($"Unknown codec {codec}")
{
Codec = codec;
SceneName = sceneName;
}
}
}

View File

@ -18,7 +18,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
private readonly IConfigService _configService;
private readonly Logger _logger;
private const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 3;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 3;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 4;
public UpdateMediaInfoService(IDiskProvider diskProvider,
IMediaFileService mediaFileService,
@ -65,7 +66,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
}
var allMediaFiles = _mediaFileService.GetFilesBySeries(message.Series.Id);
var filteredMediaFiles = allMediaFiles.Where(c => c.MediaInfo == null || c.MediaInfo.SchemaRevision < CURRENT_MEDIA_INFO_SCHEMA_REVISION).ToList();
var filteredMediaFiles = allMediaFiles.Where(c => c.MediaInfo == null || c.MediaInfo.SchemaRevision < MINIMUM_MEDIA_INFO_SCHEMA_REVISION).ToList();
UpdateMediaInfo(message.Series, filteredMediaFiles);
}

View File

@ -104,54 +104,44 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate");
int aBindex = aBitRate.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase);
if (aBindex > 0)
{
aBitRate = aBitRate.Remove(aBindex);
}
string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(aBitRate, out audioBitRate);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount);
string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)");
int aCindex = audioChannelsStr.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase);
if (aCindex > 0)
{
audioChannelsStr = audioChannelsStr.Remove(aCindex);
}
string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions");
string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List");
string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile");
int aPindex = audioProfile.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase);
if (aPindex > 0)
{
audioProfile = audioProfile.Remove(aPindex);
}
string videoProfile = mediaInfo.Get(StreamKind.Video, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(audioChannelsStr, out audioChannels);
var mediaInfoModel = new MediaInfoModel
{
VideoCodec = mediaInfo.Get(StreamKind.Video, 0, "Codec/String"),
ContainerFormat = mediaInfo.Get(StreamKind.General, 0, "Format"),
VideoFormat = mediaInfo.Get(StreamKind.Video, 0, "Format"),
VideoCodecID = mediaInfo.Get(StreamKind.Video, 0, "CodecID"),
VideoProfile = videoProfile,
VideoCodecLibrary = mediaInfo.Get(StreamKind.Video, 0, "Encoded_Library"),
VideoBitrate = videoBitRate,
VideoBitDepth = videoBitDepth,
Height = height,
Width = width,
AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"),
AudioCodecID = mediaInfo.Get(StreamKind.Audio, 0, "CodecID"),
AudioProfile = audioProfile,
AudioCodecLibrary = mediaInfo.Get(StreamKind.Audio, 0, "Encoded_Library"),
AudioBitrate = audioBitRate,
RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime),
AudioStreamCount = streamCount,
AudioChannels = audioChannels,
AudioChannelPositions = audioChannelPositions,
AudioChannelPositionsText = audioChannelPositionsText,
AudioProfile = audioProfile.Trim(),
VideoFps = videoFrameRate,
AudioLanguages = audioLanguages,
Subtitles = subtitles,

View File

@ -807,7 +807,6 @@
<Compile Include="MediaFiles\MediaInfo\MediaInfoFormatter.cs" />
<Compile Include="MediaFiles\MediaInfo\MediaInfoLib.cs" />
<Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" />
<Compile Include="MediaFiles\MediaInfo\UnknownCodecException.cs" />
<Compile Include="MediaFiles\MediaInfo\UpdateMediaInfoService.cs" />
<Compile Include="MediaFiles\MediaInfo\VideoFileInfoReader.cs" />
<Compile Include="MediaFiles\RecycleBinProvider.cs" />