diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs new file mode 100644 index 000000000..4b29c0b46 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -0,0 +1,118 @@ +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo +{ + [TestFixture] + public class UpdateMediaInfoServiceFixture : CoreTest + { + private void GivenFileExists() + { + Mocker.GetMock() + .Setup(v => v.FileExists(It.IsAny())) + .Returns(true); + } + + private void GivenSuccessfulScan() + { + Mocker.GetMock() + .Setup(v => v.GetMediaInfo(It.IsAny())) + .Returns(new MediaInfoModel()); + } + + private void GivenFailedScan(String path) + { + Mocker.GetMock() + .Setup(v => v.GetMediaInfo(path)) + .Returns((MediaInfoModel)null); + } + + [Test] + public void should_get_for_existing_episodefile_on_after_series_scan() + { + var episodeFiles = Builder.CreateListOfSize(3) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .TheFirst(1) + .With(v => v.MediaInfo = new MediaInfoModel()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void should_ignore_missing_files() + { + var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenSuccessfulScan(); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Never()); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Never()); + } + + [Test] + public void should_continue_after_failure() + { + var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(v => v.Path = @"C:\series\media.mkv".AsOsAgnostic()) + .TheFirst(1) + .With(v => v.Path = @"C:\series\media2.mkv".AsOsAgnostic()) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + GivenFailedScan(@"C:\series\media2.mkv".AsOsAgnostic()); + + Subject.Handle(new SeriesScannedEvent(new Tv.Series { Id = 1 })); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(@"C:\series\media.mkv".AsOsAgnostic()), Times.Exactly(1)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(1)); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9d137cfde..bf8c6b567 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -190,6 +190,7 @@ + diff --git a/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs b/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs new file mode 100644 index 000000000..f8763332b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/056_add_mediainfo_to_episodefile.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(56)] + public class add_mediainfo_to_episodefile : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles").AddColumn("MediaInfo").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 12dccb7b0..242ee02aa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.MediaFiles { @@ -15,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles public string SceneName { get; set; } public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } public LazyList Episodes { get; set; } public override string ToString() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index e0e6c7263..5494717e8 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -73,6 +73,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.Path = localEpisode.Path.CleanFilePath(); episodeFile.Size = _diskProvider.GetFileSize(localEpisode.Path); episodeFile.Quality = localEpisode.Quality; + episodeFile.MediaInfo = localEpisode.MediaInfo; episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 3836c1ef0..93382b430 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.MediaFiles.EpisodeImport @@ -23,19 +24,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IParsingService _parsingService; private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; + private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, IParsingService parsingService, IMediaFileService mediaFileService, IDiskProvider diskProvider, - + IVideoFileInfoReader videoFileInfoReader, Logger logger) { _specifications = specifications; _parsingService = parsingService; _mediaFileService = mediaFileService; _diskProvider = diskProvider; + _videoFileInfoReader = videoFileInfoReader; _logger = logger; } @@ -69,6 +72,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport parsedEpisode.Size = _diskProvider.GetFileSize(file); _logger.Debug("Size: {0}", parsedEpisode.Size); + parsedEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + decision = GetDecision(parsedEpisode); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 0a7fe8a70..57f9976cb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles { List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesWithoutMediaInfo(); } @@ -30,5 +31,10 @@ namespace NzbDrone.Core.MediaFiles .AndWhere(c => c.SeasonNumber == seasonNumber) .ToList(); } + + public List GetFilesWithoutMediaInfo() + { + return Query.Where(c => c.MediaInfo == null).ToList(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 3bc146fad..f45753929 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.MediaFiles void Delete(EpisodeFile episodeFile, bool forUpgrade = false); List GetFilesBySeries(int seriesId); List GetFilesBySeason(int seriesId, int seasonNumber); + List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, int seriesId); EpisodeFile Get(int id); List Get(IEnumerable ids); @@ -62,6 +63,11 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); } + public List GetFilesWithoutMediaInfo() + { + return _mediaFileRepository.GetFilesWithoutMediaInfo(); + } + public List FilterExistingFiles(List files, int seriesId) { var seriesFiles = GetFilesBySeries(seriesId).Select(f => f.Path).ToList(); diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index c18ec0870..964c62d4d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,8 +1,9 @@ using System; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles.MediaInfo { - public class MediaInfoModel + public class MediaInfoModel : IEmbeddedDocument { public string VideoCodec { get; set; } public int VideoBitrate { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs new file mode 100644 index 000000000..cf3502dea --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -0,0 +1,63 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.MediaInfo +{ + public class UpdateMediaInfoService : IHandle + { + private readonly IDiskProvider _diskProvider; + private readonly IMediaFileService _mediaFileService; + private readonly IVideoFileInfoReader _videoFileInfoReader; + private readonly Logger _logger; + + public UpdateMediaInfoService(IDiskProvider diskProvider, + IMediaFileService mediaFileService, + IVideoFileInfoReader videoFileInfoReader, + Logger logger) + { + _diskProvider = diskProvider; + _mediaFileService = mediaFileService; + _videoFileInfoReader = videoFileInfoReader; + _logger = logger; + } + + private void UpdateMediaInfo(List mediaFiles) + { + foreach (var mediaFile in mediaFiles) + { + var path = mediaFile.Path; + + if (!_diskProvider.FileExists(path)) + { + _logger.Debug("Can't update MediaInfo because '{0}' does not exist", path); + continue; + } + + mediaFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(path); + + if (mediaFile.MediaInfo != null) + { + _mediaFileService.Update(mediaFile); + _logger.Debug("Updated MediaInfo for '{0}'", path); + } + } + } + + public void Handle(SeriesScannedEvent message) + { + var mediaFiles = _mediaFileService.GetFilesBySeries(message.Series.Id) + .Where(c => c.MediaInfo == null) + .ToList(); + + UpdateMediaInfo(mediaFiles); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 6ea6013bd..3873058a0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -212,6 +212,7 @@ + @@ -482,6 +483,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 7a65d7dfb..7b9920f1a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -216,6 +216,8 @@ namespace NzbDrone.Core.Organizer tokenValues.Add("{Episode Title}", GetEpisodeTitle(episodeTitles)); tokenValues.Add("{Quality Title}", GetQualityTitle(episodeFile.Quality)); + AddMediaInfoTokens(episodeFile, tokenValues); + var filename = ReplaceTokens(pattern, tokenValues).Trim(); filename = FilenameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString() ); @@ -333,6 +335,91 @@ namespace NzbDrone.Core.Organizer return result.Trim(); } + private void AddMediaInfoTokens(EpisodeFile episodeFile, Dictionary tokenValues) + { + if (episodeFile.MediaInfo == null) + return; + + var mediaInfoFull = string.Empty; + + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("x264")) + mediaInfoFull += "x264"; + else if (Path.GetFileNameWithoutExtension(episodeFile.Path).Contains("h264")) + mediaInfoFull += "h264"; + else + mediaInfoFull += "h264"; + break; + + default: + mediaInfoFull += episodeFile.MediaInfo.VideoCodec; + break; + } + + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + mediaInfoFull += ".AC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + mediaInfoFull += ".MP3"; + else + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + + case "DTS": + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + + default: + mediaInfoFull += "." + episodeFile.MediaInfo.AudioFormat; + break; + } + + tokenValues.Add("{MediaInfo Short}", mediaInfoFull); + + var audioLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!string.IsNullOrEmpty(audioLanguagesToken) && audioLanguagesToken != "EN") + mediaInfoFull += string.Format("[{0}]", audioLanguagesToken); + + var subtitleLanguagesToken = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!string.IsNullOrEmpty(subtitleLanguagesToken)) + mediaInfoFull += string.Format(".[{0}]", subtitleLanguagesToken); + + tokenValues.Add("{MediaInfo Full}", mediaInfoFull); + } + + private string GetLanguagesToken(string mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + private string ReplaceTokens(string pattern, Dictionary tokenValues) { return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenValues)); diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 447055328..eefe76e51 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Collections.Generic; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; namespace NzbDrone.Core.Parser.Model { @@ -14,6 +15,7 @@ namespace NzbDrone.Core.Parser.Model public Series Series { get; set; } public List Episodes { get; set; } public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } public Boolean ExistingFile { get; set; } public int SeasonNumber diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 61ede3492..d39a53064 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -27,16 +27,19 @@ namespace NzbDrone.Core.Parser private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; private readonly ISceneMappingService _sceneMappingService; + private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, ISceneMappingService sceneMappingService, + IDiskProvider diskProvider, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; _sceneMappingService = sceneMappingService; + _diskProvider = diskProvider; _logger = logger; }