diff --git a/.circleci/Dockerfile b/.circleci/Dockerfile index d8ccf48c12..185dd45135 100644 --- a/.circleci/Dockerfile +++ b/.circleci/Dockerfile @@ -11,3 +11,4 @@ RUN curl -O https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz && tar xvf go*. ENV GOPATH=$HOME/work ENV PATH="${PATH}:/usr/local/go/bin:$GOPATH/bin" RUN go get github.com/aktau/github-release +RUN npm install -g yarn \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index 2adedc5c44..cf59704235 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 defaults: &defaults docker: - - image: gallileo/radarr-cci-primary:5.8.7 + - image: gallileo/radarr-cci-primary:5.8.8 environment: BUILD_VERSION: 0.2.0 @@ -62,6 +62,7 @@ jobs: - _tests - setup - .circleci + - deploy.sh unit_tests: <<: *defaults steps: @@ -105,10 +106,10 @@ jobs: zip -r _packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.windows.zip _packages/Radarr rm -rf _packages/Radarr cp -r _output_mono/ _packages/Radarr - tar -zcvf _packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.linux.tar.gz _packages/Radarr + tar -zcvf _packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.linux.tar.gz -C _packages Radarr rm -rf _packages/Radarr cp -r _output_osx/ _packages/Radarr - tar -zcvf _packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.osx.tar.gz _packages/Radarr + tar -zcvf _packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.osx.tar.gz -C _packages Radarr rm -rf _packages/Radarr cd _output_osx_app/ zip -r ../_packages/Radarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.osx-app.zip * @@ -118,6 +119,9 @@ jobs: - store_artifacts: path: _packages destination: artifacts + - run: + name: "Deploying" + command: chmod +x deploy.sh && ./deploy.sh - persist_to_workspace: root: . # Must be relative path from root @@ -150,16 +154,16 @@ workflows: - unit_tests: requires: - build - - integration_tests: - requires: - - build + #- integration_tests: + # requires: + # - build - publish_artifacts: requires: - build - - request_deploy: - type: approval - requires: - - publish_artifacts - - deploy: - requires: - - request_deploy + #- request_deploy: + # type: approval + # requires: + # - publish_artifacts + #- deploy: + # requires: + # - request_deploy diff --git a/.gitchangelog.rc.release b/.gitchangelog.rc.release index b1ef30292d..56c23fc620 100644 --- a/.gitchangelog.rc.release +++ b/.gitchangelog.rc.release @@ -82,13 +82,13 @@ ignore_regexps = [ ## whenever you are tweaking this variable. ## section_regexps = [ - ('**New features**', [ + ('**New features:**', [ r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), - ('**Fixes**', [ + ('**Fixes:**', [ r'^(?![mM]erge\s*)' ] ), @@ -151,7 +151,7 @@ subject_process = (strip | ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'\4') | ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'\4') | ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'\4') | - ReSub(r'#(\d{3,4})', r'\1') | + ReSub(r'#(\d{3,4})', r'Issue #\1') | SetIfEmpty("No commit message.") | ucfirst | final_dot) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11e612f3de..0f8bfa4116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ ## (unreleased) ### **New features** +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) 64bit mediainfo.dll to 32bit to resolve issue: https://github.com/Radarr/Radarr/issues/3138. [geogolem] +- ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) Refactor MediaInfo tokens (fixes old tokens adds new stuff) ([#3058](https://github.com/Radarr/Radarr/issues/3058)) [Ricardo Amaral] +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) Don't hide custom formats behind advanced settings when editing quality. [Leonardo Galli] +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) Upped rate at which we scan the download client. Should reduce cpu and ram usage as well as decrease pressure on download clients. [Leonardo Galli] +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) Improve model and UI handling for lists. Should finally fix root folder errors. ([#3133](https://github.com/Radarr/Radarr/issues/3133)) [Ricardo Amaral] +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) Don't return unmapped folders on rootfolder API call. Massively improves loading time. ([#3116](https://github.com/Radarr/Radarr/issues/3116)) [Justin Kromlinger] +- ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) Support for Homebrew-installed mono ([#3090](https://github.com/Radarr/Radarr/issues/3090)) [Jeff Byrnes] +- ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) mk3d file format ([#2795](https://github.com/Radarr/Radarr/issues/2795)) [Qstick] +- ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) "Add Paused" option to Deluge and Transmission ([#3038](https://github.com/Radarr/Radarr/issues/3038)) [cookandy] +- ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) All-around small improvements ([#3032](https://github.com/Radarr/Radarr/issues/3032)) [Ricardo Amaral] - ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) Czech Language ([#2948](https://github.com/Radarr/Radarr/issues/2948)) [halali] - ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) Fallback to Bitrate_Nominal for MediaInfo ([#2886](https://github.com/Radarr/Radarr/issues/2886)) [Qstick] - ![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) All new custom formats 9000! (Rescan old files, delete formats, polish UI, etc. See discord for full changes): [Leonardo Galli] @@ -13,6 +23,21 @@ - ![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) "importing an episode" to "importing a movie file" ([#2829](https://github.com/Radarr/Radarr/issues/2829)) [Travis Boss] ### **Fixes** +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Fallback to 'VideoCodec' if 'VideoFormat' is unavailable ([#3142](https://github.com/Radarr/Radarr/issues/3142)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Read video 'BitRate_Nominal' if 'BitRate' is empty ([#3144](https://github.com/Radarr/Radarr/issues/3144)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) UpdateMovieQualityService Tests. [Leonardo Galli] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Ignore "special drives" from System » Disk Space ([#3050](https://github.com/Radarr/Radarr/issues/3050)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Tweak style of movie path template on "add movies" screen ([#3108](https://github.com/Radarr/Radarr/issues/3108)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Unable to update custom formats for releases with bad Source Titles. [Leonardo Galli] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Do not search movie if unmonitored ([#3131](https://github.com/Radarr/Radarr/issues/3131)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Quality badges not being shown on bulk import. ([#3121](https://github.com/Radarr/Radarr/issues/3121)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Trim filename from Kodi movie path before sending library scan request. ([#3097](https://github.com/Radarr/Radarr/issues/3097)) [Lawrence] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Hopefully fixed bulk import not showing files. [Leonardo Galli] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) MPEG-2 remuxes being detected as "Raw-HD" quality. [Leonardo Galli] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Allow directory to be parsed similar to past implementation ([#3057](https://github.com/Radarr/Radarr/issues/3057)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Class names on the 'add movies screen' ([#3047](https://github.com/Radarr/Radarr/issues/3047)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Use proper cursor for text and linked labels ([#3041](https://github.com/Radarr/Radarr/issues/3041)) [Ricardo Amaral] +- ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Donate button requiring two clicks to actually work. [Leonardo Galli] - ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Templates for custom format using wrong modifiers. [Leonardo Galli] - ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) Profiles always failing validation. [Leonardo Galli] - ![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) ImdbIds not being padded with zeroes, which messes up matching. [Leonardo Galli] diff --git a/changelog_release.tpl b/changelog_release.tpl index 6f1e0bc4a1..f4eb2c1ba7 100644 --- a/changelog_release.tpl +++ b/changelog_release.tpl @@ -5,7 +5,7 @@ {{#sections}} {{{label}}} {{#commits}} -- {{{subject}}} [{{{author}}}] +- {{{subject}}} [{{{author}}}] {{/commits}} {{/sections}} diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000000..7ad83a091a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,7 @@ +if [ -z "$CIRCLE_PULL_REQUEST" ]; then + echo "We are building a normal branch, deploying as such..." + curl "http://pr.radarr.video:4466/deploy?url=https%3A%2F%2F${CIRCLE_BUILD_NUM}-77323220-gh.circle-artifacts.com%2F0%2Fartifacts%2FRadarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.linux.tar.gz&b=branch&name=${CIRCLE_BRANCH}" +else + echo "We are building a pr, deploying as such..." + curl "http://pr.radarr.video:4466/deploy?url=https%3A%2F%2F${CIRCLE_BUILD_NUM}-77323220-gh.circle-artifacts.com%2F0%2Fartifacts%2FRadarr.${CIRCLE_BRANCH//\//-}.$BUILD_VERSION.$CIRCLE_BUILD_NUM.linux.tar.gz&b=pr&name=${CIRCLE_PR_NUMBER}" +fi \ No newline at end of file diff --git a/osx/Radarr b/osx/Radarr index 7933f38934..d9c88956a5 100644 --- a/osx/Radarr +++ b/osx/Radarr @@ -9,7 +9,11 @@ APPNAME="Radarr" #set up environment if [[ -x '/opt/local/bin/mono' ]]; then + # Macports and mono-supplied installer path export PATH="/opt/local/bin:$PATH" +elif [[ -x '/usr/local/bin/mono' ]]; then + # Homebrew-supplied path to mono + export PATH="/usr/local/bin:$PATH" fi export DYLD_FALLBACK_LIBRARY_PATH="$DIR" diff --git a/src/Libraries/MediaInfo/MediaInfo.dll b/src/Libraries/MediaInfo/MediaInfo.dll index 24e6cb986a..ca4ce4fb69 100644 Binary files a/src/Libraries/MediaInfo/MediaInfo.dll and b/src/Libraries/MediaInfo/MediaInfo.dll differ diff --git a/src/Libraries/MediaInfo/libmediainfo.0.dylib b/src/Libraries/MediaInfo/libmediainfo.0.dylib index 5e5383ded2..73ff0ba4fb 100644 Binary files a/src/Libraries/MediaInfo/libmediainfo.0.dylib and b/src/Libraries/MediaInfo/libmediainfo.0.dylib differ diff --git a/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs index dee23aa3f0..6e958f1f69 100644 --- a/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs +++ b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.RootFolders; using NzbDrone.Common.Cache; using NzbDrone.Core.Movies; +using NzbDrone.Core.Profiles; namespace NzbDrone.Api.Movies { @@ -34,12 +35,13 @@ public class MovieBulkImportModule : NzbDroneRestModule private readonly IDiskScanService _diskScanService; private readonly ICached _mappedMovies; private readonly IParsingService _parsingService; + private readonly IProfileService _profileService; private readonly IMovieService _movieService; public MovieBulkImportModule(ISearchForNewMovie searchProxy, IRootFolderService rootFolderService, IMakeImportDecision importDecisionMaker, IDiskScanService diskScanService, ICacheManager cacheManager, - IParsingService parsingService, IMovieService movieService) + IParsingService parsingService, IProfileService profileService, IMovieService movieService) : base("/movies/bulkimport") { _searchProxy = searchProxy; @@ -48,6 +50,7 @@ public MovieBulkImportModule(ISearchForNewMovie searchProxy, IRootFolderService _diskScanService = diskScanService; _mappedMovies = cacheManager.GetCache(GetType(), "mappedMoviesCache"); _movieService = movieService; + _profileService = profileService; _parsingService = parsingService; Get["/"] = x => Search(); } @@ -60,6 +63,8 @@ private Response Search() //Todo error handling } + Profile tempProfile = _profileService.All().First(); + RootFolder rootFolder = _rootFolderService.Get(Request.Query.Id); int page = Request.Query.page; @@ -100,6 +105,7 @@ private Response Search() { Title = f.Name.Replace(".", " ").Replace("-", " "), Path = f.Path, + Profile = tempProfile }; } else @@ -111,7 +117,8 @@ private Response Search() Title = parsedTitle.MovieTitle, Year = parsedTitle.Year, ImdbId = parsedTitle.ImdbId, - Path = f.Path + Path = f.Path, + Profile = tempProfile }; } diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index e87e581de4..30bfcc6356 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -52,7 +52,7 @@ private int CreateRootFolder(RootFolderResource rootFolderResource) private List GetRootFolders() { - return _rootFolderService.AllWithUnmappedFolders().ToResource(); + return _rootFolderService.AllWithSpace().ToResource(); } private void DeleteFolder(int id) diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 247274e291..687a7e6de5 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -78,6 +78,21 @@ public static bool IsNotNullOrWhiteSpace(this string text) return !string.IsNullOrWhiteSpace(text); } + public static bool StartsWithIgnoreCase(this string text, string startsWith) + { + return text.StartsWith(startsWith, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool EndsWithIgnoreCase(this string text, string startsWith) + { + return text.EndsWith(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 +133,4 @@ public static string FromOctalString(this string octalValue) return Encoding.ASCII.GetString(new [] { byteResult }); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs new file mode 100644 index 0000000000..3e43fbe0c6 --- /dev/null +++ b/src/NzbDrone.Core.Test/DiskSpace/DiskSpaceServiceFixture.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.DiskSpace; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Movies; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.DiskSpace +{ + [TestFixture] + public class DiskSpaceServiceFixture : CoreTest + { + private string _moviesFolder; + private string _moviesFolder2; + private string _droneFactoryFolder; + + [SetUp] + public void SetUp() + { + _moviesFolder = @"G:\fasdlfsdf\movies".AsOsAgnostic(); + _moviesFolder2 = @"G:\fasdlfsdf\movies2".AsOsAgnostic(); + _droneFactoryFolder = @"G:\dronefactory".AsOsAgnostic(); + + Mocker.GetMock() + .Setup(v => v.GetMounts()) + .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.GetPathRoot(It.IsAny())) + .Returns(@"G:\".AsOsAgnostic()); + + Mocker.GetMock() + .Setup(v => v.GetAvailableSpace(It.IsAny())) + .Returns(0); + + Mocker.GetMock() + .Setup(v => v.GetTotalSize(It.IsAny())) + .Returns(0); + + GivenMovies(); + } + + private void GivenMovies(params Movie[] movies) + { + Mocker.GetMock() + .Setup(v => v.GetAllMovies()) + .Returns(movies.ToList()); + } + + private void GivenExistingFolder(string folder) + { + Mocker.GetMock() + .Setup(v => v.FolderExists(folder)) + .Returns(true); + } + + [Test] + public void should_check_diskspace_for_movies_folders() + { + GivenMovies(new Movie { Path = _moviesFolder }); + + GivenExistingFolder(_moviesFolder); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().NotBeEmpty(); + } + + [Test] + public void should_check_diskspace_for_same_root_folder_only_once() + { + GivenMovies(new Movie { Path = _moviesFolder }, new Movie { Path = _moviesFolder2 }); + + GivenExistingFolder(_moviesFolder); + GivenExistingFolder(_moviesFolder2); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().HaveCount(1); + + Mocker.GetMock() + .Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Once()); + } + + [Test] + [Ignore("Unknown failure")] + public void should_not_check_diskspace_for_missing_movies_folders() + { + GivenMovies(new Movie { Path = _moviesFolder }); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().BeEmpty(); + + Mocker.GetMock() + .Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Never()); + } + + [Test] + public void should_check_diskspace_for_dronefactory_folder() + { + Mocker.GetMock() + .SetupGet(v => v.DownloadedMoviesFolder) + .Returns(_droneFactoryFolder); + + GivenExistingFolder(_droneFactoryFolder); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().NotBeEmpty(); + } + + [Test] + [Ignore("Unknown failure")] + public void should_not_check_diskspace_for_missing_dronefactory_folder() + { + Mocker.GetMock() + .SetupGet(v => v.DownloadedMoviesFolder) + .Returns(_droneFactoryFolder); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().BeEmpty(); + + Mocker.GetMock() + .Verify(v => v.GetAvailableSpace(It.IsAny()), Times.Never()); + } + + [TestCase("/boot")] + [TestCase("/var/lib/rancher")] + [TestCase("/var/lib/rancher/volumes")] + [TestCase("/var/lib/kubelet")] + [TestCase("/var/lib/docker")] + [TestCase("/some/place/docker/aufs")] + [TestCase("/etc/network")] + [TestCase("/snap/filebot/9")] + [TestCase("/snap/core/5145")] + public void should_not_check_diskspace_for_irrelevant_mounts(string path) + { + var mount = new Mock(); + mount.SetupGet(v => v.RootDirectory).Returns(path); + mount.SetupGet(v => v.DriveType).Returns(System.IO.DriveType.Fixed); + + Mocker.GetMock() + .Setup(v => v.GetMounts()) + .Returns(new List { mount.Object }); + + var freeSpace = Subject.GetFreeSpace(); + + freeSpace.Should().BeEmpty(); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs new file mode 100644 index 0000000000..7a80c9af4c --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs @@ -0,0 +1,177 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatAudioChannelsFixture : TestBase + { + [Test] + public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 6, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L C R, Side: L R, LFE" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L R" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 2 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0); + } + + [Test] + public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "2/0/0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions_including_decimal() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/0.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_cleanup_extraneous_text_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "Object Based / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_skip_empty_groups_in_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = " / 2/0/0.0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_first_series_of_numbers_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/2.1 / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_sum_dual_mono_representation_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "1+1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m); + } + + [Test] + public void should_use_AudioChannelPositionText_when_AudioChannelChannelPosition_is_invalid() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 6, + AudioChannelPositions = "15 objects", + AudioChannelPositionsText = "15 objects / Front: L C R, Side: L R, LFE", + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_remove_atmos_objects_from_AudioChannelPostions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "15 objects / 3/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs new file mode 100644 index 0000000000..cfdbb02da2 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatAudioCodecFixture : TestBase + { + private static string sceneName = "My.Series.S01E01-Sonarr"; + + [TestCase("AC-3", "AC3")] + [TestCase("E-AC-3", "EAC3")] + [TestCase("MPEG Audio", "MPEG Audio")] + [TestCase("DTS", "DTS")] + public void should_format_audio_format_legacy(string audioFormat, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = audioFormat + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("MPEG Audio, A_MPEG/L2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")] + [TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget)", "DB Super HDTV", "Vorbis")] + [TestCase("PCM, 1, , ", "DW DVDRip XviD-idTV", "PCM")] // Dubbed most likely + [TestCase("TrueHD, A_TRUEHD, , ", "", "TrueHD")] + [TestCase("WMA, 161, , ", "Droned.wmv", "WMA")] + [TestCase("WMA, 162, Pro, ", "B.N.S04E18.720p.WEB-DL", "WMA")] + [TestCase("Opus, A_OPUS, , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")] + [TestCase("mp3 , 0, , ", "climbing.mp4", "MP3")] + public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat) + { + var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = split[0], + AudioCodecID = split[1], + AudioProfile = split[2], + AudioCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [Test] + public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile() + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = "MPEG Audio", + AudioProfile = "Layer 3" + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be("MP3"); + } + + [Test] + public void should_return_AudioFormat_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = "Other Audio Format", + AudioCodecID = "Other Audio Codec" + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(mediaInfoModel.AudioFormat); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs new file mode 100644 index 0000000000..fa9d6b8a7a --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs @@ -0,0 +1,92 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatVideoCodecFixture : TestBase + { + [TestCase("AVC", null, "x264")] + [TestCase("AVC", "source.title.x264.720p-Sonarr", "x264")] + [TestCase("AVC", "source.title.h264.720p-Sonarr", "h264")] + [TestCase("V_MPEGH/ISO/HEVC", null, "x265")] + [TestCase("V_MPEGH/ISO/HEVC", "source.title.x265.720p-Sonarr", "x265")] + [TestCase("V_MPEGH/ISO/HEVC", "source.title.h265.720p-Sonarr", "h265")] + [TestCase("MPEG-2 Video", null, "MPEG2")] + public void should_format_video_codec_with_source_title_legacy(string videoCodec, string sceneName, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + VideoCodec = videoCodec + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("MPEG Video, 2, Main@High, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")] + [TestCase("MPEG Video, V_MPEG2, Main@High, ", "", "MPEG2")] + [TestCase("MPEG Video, , , ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")] + [TestCase("VC-1, WVC1, Advanced@L4, ", "B.N.S04E18.720p.WEB-DL", "VC1")] + [TestCase("VC-1, V_MS/VFW/FOURCC / WVC1, Advanced@L3, ", "", "VC1")] + [TestCase("VC-1, WMV3, MP@LL, ", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")] + [TestCase("V.MPEG4/ISO/AVC, V.MPEG4/ISO/AVC, , ", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")] + [TestCase("WMV2, WMV2, , ", "Droned.wmv", "WMV")] + [TestCase("xvid, xvid, , ", "", "XviD")] + [TestCase("div3, div3, , ", "spsm.dvdrip.divx.avi'.", "DivX")] + [TestCase("VP6, 4, , ", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")] + [TestCase("VP7, VP70, General, ", "Sweet Seymour.avi", "VP7")] + [TestCase("VP8, V_VP8, , ", "Dick.mkv", "VP8")] + [TestCase("VP9, V_VP9, , ", "Roadkill Ep3x11 - YouTube.webm", "VP9")] + [TestCase("x264, x264, , ", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")] + [TestCase("V_MPEGH/ISO/HEVC, V_MPEGH/ISO/HEVC, , ", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")] + [TestCase("MPEG-4 Visual, 20, Simple@L1, Lavc52.29.0", "Will.And.Grace.S08E14.WS.DVDrip.XviD.I.Love.L.Gay-Obfuscated", "XviD")] + [TestCase("MPEG-4 Visual, 20, Advanced Simple@L5, XviD0046", "", "XviD")] + [TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")] + public void should_format_video_format(string videoFormatPack, string sceneName, string expectedFormat) + { + var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = split[0], + VideoCodecID = split[1], + VideoProfile = split[2], + VideoCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("AVC, AVC, , x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag + [TestCase("HEVC, HEVC, , x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag + [TestCase("AVC, AVC, , ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("HEVC, HEVC, , ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("AVC, AVC, , ", "Some.Video.S01E01", "h264")] // Default value + [TestCase("HEVC, HEVC, , ", "Some.Video.S01E01", "h265")] // Default value + public void should_format_video_format_fallbacks(string videoFormatPack, string sceneName, string expectedFormat) + { + var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = split[0], + VideoCodecID = split[1], + VideoProfile = split[2], + VideoCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [Test] + public void should_return_VideoFormat_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = "VideoCodec" + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, null).Should().Be(mediaInfoModel.VideoFormat); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs index 10f78375f9..c41d09ba43 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -16,15 +16,15 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo [TestFixture] public class UpdateMediaInfoServiceFixture : CoreTest { - private Movie _series; + private Movie _movie; [SetUp] public void Setup() { - _series = new Movie + _movie = new Movie { Id = 1, - Path = @"C:\series".AsOsAgnostic() + Path = @"C:\movie".AsOsAgnostic() }; Mocker.GetMock() @@ -60,7 +60,7 @@ public void should_skip_up_to_date_media_info() .All() .With(v => v.RelativePath = "media.mkv") .TheFirst(1) - .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = 3 }) + .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.CURRENT_MEDIA_INFO_SCHEMA_REVISION }) .BuildList(); Mocker.GetMock() @@ -70,10 +70,36 @@ public void should_skip_up_to_date_media_info() GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void should_skip_not_yet_date_media_info() + { + var episodeFiles = Builder.CreateListOfSize(3) + .All() + .With(v => v.RelativePath = "media.mkv") + .TheFirst(1) + .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION }) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesByMovie(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Handle(new MovieScannedEvent(_movie)); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); @@ -96,10 +122,10 @@ public void should_update_outdated_media_info() GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(3)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(3)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(3)); @@ -119,7 +145,7 @@ public void should_ignore_missing_files() GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() .Verify(v => v.GetMediaInfo("media.mkv"), Times.Never()); @@ -144,12 +170,12 @@ public void should_continue_after_failure() GivenFileExists(); GivenSuccessfulScan(); - GivenFailedScan(Path.Combine(_series.Path, "media2.mkv")); + GivenFailedScan(Path.Combine(_movie.Path, "media2.mkv")); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(1)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(1)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(1)); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index 617a4e41cc..42ad53e72b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -31,10 +31,8 @@ public void get_runtime() var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4"); Subject.GetRunTime(path).Seconds.Should().Be(10); - } - [Test] public void get_info() { @@ -42,21 +40,27 @@ public void get_info() var info = Subject.GetMediaInfo(path); - + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.VideoMultiViewCount.Should().Be(0); + info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); + info.VideoTransferCharacteristics.Should().Be("BT.709"); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().Be(""); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - } [Test] @@ -73,20 +77,27 @@ public void get_info_unicode() var info = Subject.GetMediaInfo(path); + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.VideoMultiViewCount.Should().Be(0); + info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); + info.VideoTransferCharacteristics.Should().Be("BT.709"); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().Be(""); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - } [Test] @@ -100,23 +111,5 @@ public void should_dispose_file_after_scanning_mediainfo() stream.Close(); } - - [Test] - [TestCase("/ Front: L R", 2.0)] - public void should_correctly_read_audio_channels(string ChannelPositions, decimal formattedChannels) - { - var info = new MediaInfoModel() - { - VideoCodec = "AVC", - AudioFormat = "DTS", - AudioLanguages = "English", - Subtitles = "English", - AudioChannels = 2, - AudioChannelPositions = ChannelPositions, - SchemaRevision = 3, - }; - - info.FormattedAudioChannels.Should().Be(formattedChannels); - } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs index 88cb9a64a9..39ab4a6bd3 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpdateMovieFileQualityServiceFixture.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.MediaFiles { @@ -58,6 +59,8 @@ public void should_not_update_if_unable_to_parse() { ExecuteCommand(); + ExceptionVerification.ExpectedWarns(1); + Mocker.GetMock().Verify(s => s.Update(It.IsAny()), Times.Never()); } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index bfd83ac263..695786ddbb 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -168,6 +168,7 @@ + @@ -314,6 +315,9 @@ + + + @@ -580,4 +584,4 @@ --> - \ No newline at end of file + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 38d0e5aa7b..f95e3171f0 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -260,14 +260,14 @@ public void should_format_mediainfo_properly() _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", AudioFormat = "DTS", AudioLanguages = "English/Spanish", Subtitles = "English/Spanish/Italian" }; Subject.BuildFileName(_movie, _movieFile) - .Should().Be("South.Park.X264.DTS[EN+ES].[EN+ES+IT]"); + .Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]"); } [Test] @@ -277,14 +277,52 @@ public void should_exclude_english_in_mediainfo_audio_language() _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", AudioFormat = "DTS", AudioLanguages = "English", Subtitles = "English/Spanish/Italian" }; Subject.BuildFileName(_movie, _movieFile) - .Should().Be("South.Park.X264.DTS.[EN+ES+IT]"); + .Should().Be("South.Park.H264.DTS.[EN+ES+IT]"); + } + + [Test] + public void should_format_mediainfo_3d_properly() + { + _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.3D}.{MediaInfo.Simple}"; + + _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() + { + VideoFormat = "AVC", + VideoMultiViewCount = 2, + AudioFormat = "DTS", + AudioLanguages = "English", + Subtitles = "English/Spanish/Italian" + }; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be("South.Park.3D.h264.DTS"); + } + + [Test] + public void should_format_mediainfo_hdr_properly() + { + _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.HDR}.{MediaInfo.Simple}"; + + _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() + { + VideoFormat = "AVC", + VideoBitDepth = 10, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", + AudioFormat = "DTS", + AudioLanguages = "English", + Subtitles = "English/Spanish/Italian" + }; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be("South.Park.HDR.h264.DTS"); } [Test] diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 7e50e69460..97772a131f 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -236,6 +236,8 @@ public void should_parse_bluray576p_quality(string title) [TestCase("Contract.to.Kill.2016.REMUX.1080p.BluRay.AVC.DTS-HD.MA.5.1-iFT")] [TestCase("27.Dresses.2008.REMUX.1080p.Bluray.AVC.DTS-HR.MA.5.1-LEGi0N")] [TestCase("27.Dresses.2008.BDREMUX.1080p.Bluray.AVC.DTS-HR.MA.5.1-LEGi0N")] + [TestCase("The.Stoning.of.Soraya.M.2008.USA.BluRay.Remux.1080p.MPEG-2.DD.5.1-TDD")] + [TestCase("Wildling.2018.1080p.BluRay.REMUX.MPEG-2.DTS-HD.MA.5.1-EPSiLON")] public void should_parse_remux1080p_quality(string title) { ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R1080P, Modifier.REMUX); @@ -344,7 +346,10 @@ public void should_parse_hardcoded_subs(string postTitle, string sub) private void ParseAndVerifyQuality(string title, Source source, bool proper, Resolution resolution, Modifier modifier = Modifier.NONE) { var result = QualityParser.ParseQuality(title); - result.Resolution.Should().Be(resolution); + if (resolution != Resolution.Unknown) + { + result.Resolution.Should().Be(resolution); + } result.Source.Should().Be(source); if (modifier != Modifier.NONE) { diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index ee5065f813..9a603b7da2 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -22,6 +23,8 @@ public class DiskSpaceService : IDiskSpaceService private readonly IDiskProvider _diskProvider; private readonly Logger _logger; + private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc|snap)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled); + public DiskSpaceService(IMovieService movieService, IConfigService configService, IDiskProvider diskProvider, Logger logger) { _movieService = movieService; @@ -59,7 +62,10 @@ private IEnumerable GetDroneFactoryFreeSpace() private IEnumerable GetFixedDisksFreeSpace() { - return GetDiskSpace(_diskProvider.GetMounts().Where(d => d.DriveType == DriveType.Fixed).Select(d => d.RootDirectory), true); + return GetDiskSpace(_diskProvider.GetMounts() + .Where(d => d.DriveType == DriveType.Fixed) + .Where(d => !_regexSpecialDrive.IsMatch(d.RootDirectory)) + .Select(d => d.RootDirectory), true); } private IEnumerable GetDiskSpace(IEnumerable paths, bool suppressWarnings = false) diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 51f898ea32..6b3c6980c0 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Movies; namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc @@ -32,7 +33,7 @@ public XbmcMetadata(IDetectXbmcNfo detectNfo, _mediaCoverService = mediaCoverService; _diskProvider = diskProvider; _detectNfo = detectNfo; - + } private static readonly Regex MovieImagesRegex = new Regex(@"^(?poster|banner|fanart|clearart|discart|landscape|logo|backdrop|clearlogo)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -157,13 +158,15 @@ public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFil if (movieFile.MediaInfo != null) { + var sceneName = movieFile.GetSceneOrFileName(); + var fileInfo = new XElement("fileinfo"); var streamDetails = new XElement("streamdetails"); var video = new XElement("video"); video.Add(new XElement("aspect", (float)movieFile.MediaInfo.Width / (float)movieFile.MediaInfo.Height)); video.Add(new XElement("bitrate", movieFile.MediaInfo.VideoBitrate)); - video.Add(new XElement("codec", movieFile.MediaInfo.VideoCodec)); + video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName))); video.Add(new XElement("framerate", movieFile.MediaInfo.VideoFps)); video.Add(new XElement("height", movieFile.MediaInfo.Height)); video.Add(new XElement("scantype", movieFile.MediaInfo.ScanType)); @@ -180,7 +183,7 @@ public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFil var audio = new XElement("audio"); audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate)); audio.Add(new XElement("channels", movieFile.MediaInfo.AudioChannels)); - audio.Add(new XElement("codec", GetAudioCodec(movieFile.MediaInfo.AudioFormat))); + audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName))); audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages)); streamDetails.Add(audio); @@ -251,16 +254,6 @@ private string GetMovieMetadataFilename(string movieFilePath) } } - private string GetAudioCodec(string audioCodec) - { - if (audioCodec == "AC-3") - { - return "AC3"; - } - - return audioCodec; - } - private bool GetExistingWatchedStatus(Movie movie, string movieFilePath) { var fullPath = Path.Combine(movie.Path, GetMovieMetadataFilename(movieFilePath)); diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs index 597817ac62..1552706898 100644 --- a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs @@ -46,6 +46,7 @@ public void Execute(MoviesSearchCommand message) if (!movies.Monitored) { _logger.Debug("Movie {0} is not monitored, skipping search", movies.Title); + continue; } var decisions = _nzbSearchService.MovieSearch(movieId, false);//_nzbSearchService.SeasonSearch(message.MovieId, season.SeasonNumber, false, message.Trigger == CommandTrigger.Manual); diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 4ad1b65df5..7133803ca7 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -71,7 +71,7 @@ public void Handle(ApplicationStartedEvent message) var defaultTasks = new[] { - new ScheduledTask{ Interval = 0.25f, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, + new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(PreDBSyncCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = updateInterval, TypeName = typeof(ApplicationUpdateCommand).FullName}, diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs new file mode 100644 index 0000000000..a2d6f9f71e --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -0,0 +1,470 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NLog.Fluent; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Instrumentation.Extensions; + +namespace NzbDrone.Core.MediaFiles.MediaInfo +{ + public static class MediaInfoFormatter + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter)); + + public static decimal FormatAudioChannels(MediaInfoModel mediaInfo) + { + var audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo); + + if (audioChannels == null) + { + audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo); + } + + if (audioChannels == null) + { + audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo); + } + + return audioChannels ?? 0; + } + + 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 audioAdditionalFeatures = mediaInfo.AudioAdditionalFeatures ?? 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")) + { + if (audioAdditionalFeatures.StartsWithIgnoreCase("XLL")) + { + if (audioAdditionalFeatures.EndsWithIgnoreCase("X")) + { + return "DTS-X"; + } + + return "DTS-HD MA"; + } + + if (audioAdditionalFeatures.EqualsIgnoreCase("ES")) + { + return "DTS-ES"; + } + + if (audioAdditionalFeatures.EqualsIgnoreCase("XBR")) + { + return "DTS-HD HRA"; + } + + return "DTS"; + } + + if (audioFormat.EqualsIgnoreCase("FLAC")) + { + return "FLAC"; + } + + if (audioFormat.Trim().EqualsIgnoreCase("mp3")) + { + return "MP3"; + } + + if (audioFormat.EqualsIgnoreCase("MPEG Audio")) + { + if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3") + { + return "MP3"; + } + + if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2") + { + return "MP2"; + } + } + + if (audioFormat.EqualsIgnoreCase("Opus")) + { + return "Opus"; + } + + if (audioFormat.EqualsIgnoreCase("PCM")) + { + return "PCM"; + } + + if (audioFormat.EqualsIgnoreCase("MLP FBA")) + { + if (audioAdditionalFeatures == "16-ch") + { + return "TrueHD Atmos"; + } + + return "TrueHD"; + } + + if (audioFormat.EqualsIgnoreCase("Vorbis")) + { + return "Vorbis"; + } + + if (audioFormat == "WMA") + { + return "WMA"; + } + + Logger.Debug("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioAdditionalFeatures, audioCodecLibrary), sceneName); + + return audioFormat; + } + + public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName) + { + var audioFormat = mediaInfo.AudioFormat; + + if (audioFormat.IsNullOrWhiteSpace()) + { + return audioFormat; + } + + if (audioFormat.EqualsIgnoreCase("AC-3")) + { + return "AC3"; + } + + if (audioFormat.EqualsIgnoreCase("E-AC-3")) + { + return "EAC3"; + } + + if (audioFormat.EqualsIgnoreCase("AAC")) + { + return "AAC"; + } + + if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3") + { + return "MP3"; + } + + if (audioFormat.EqualsIgnoreCase("DTS")) + { + return "DTS"; + } + + if (audioFormat.EqualsIgnoreCase("TrueHD")) + { + return "TrueHD"; + } + + if (audioFormat.EqualsIgnoreCase("FLAC")) + { + return "FLAC"; + } + + if (audioFormat.EqualsIgnoreCase("Vorbis")) + { + return "Vorbis"; + } + + if (audioFormat.EqualsIgnoreCase("Opus")) + { + return "Opus"; + } + + 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; + + var result = videoFormat; + + if (videoFormat.IsNullOrWhiteSpace()) + { + return result; + } + + if (videoFormat == "x264") + { + return "x264"; + } + + if (videoFormat == "AVC" || videoFormat == "V.MPEG4/ISO/AVC") + { + if (videoCodecLibrary.StartsWithIgnoreCase("x264")) + { + return "x264"; + } + + return GetSceneNameMatch(sceneName, "AVC", "x264", "h264"); + } + + if (videoFormat == "HEVC" || videoFormat == "V_MPEGH/ISO/HEVC") + { + if (videoCodecLibrary.StartsWithIgnoreCase("x265")) + { + return "x265"; + } + + return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265"); + } + + if (videoFormat == "MPEG Video") + { + if (videoCodecID == "2" || videoCodecID == "V_MPEG2") + { + return "MPEG2"; + } + + if (videoCodecID.IsNullOrWhiteSpace()) + { + return "MPEG"; + } + } + + if (videoFormat == "MPEG-2 Video") + { + return "MPEG2"; + } + + if (videoFormat == "MPEG-4 Visual") + { + if (videoCodecID.ContainsIgnoreCase("XVID") || + videoCodecLibrary.StartsWithIgnoreCase("XviD")) + { + return "XviD"; + } + + if (videoCodecID.ContainsIgnoreCase("DIV3") || + videoCodecID.ContainsIgnoreCase("DIVX") || + videoCodecID.ContainsIgnoreCase("DX50") || + videoCodecLibrary.StartsWithIgnoreCase("DivX")) + { + return "DivX"; + } + } + + if (videoFormat == "MPEG-4 Visual" || videoFormat == "mp4v") + { + result = GetSceneNameMatch(sceneName, "XviD", "DivX", ""); + if (result.IsNotNullOrWhiteSpace()) + { + return result; + } + } + + if (videoFormat == "VC-1") + { + return "VC1"; + } + + if (videoFormat.EqualsIgnoreCase("VP6") || videoFormat.EqualsIgnoreCase("VP7") || + videoFormat.EqualsIgnoreCase("VP8") || videoFormat.EqualsIgnoreCase("VP9")) + { + return videoFormat.ToUpperInvariant(); + } + + if (videoFormat == "WMV2") + { + return "WMV"; + } + + if (videoFormat.EqualsIgnoreCase("DivX") || videoFormat.EqualsIgnoreCase("div3")) + { + return "DivX"; + } + + if (videoFormat.EqualsIgnoreCase("XviD")) + { + return "XviD"; + } + + Logger.Debug("Unknown video format: '{0}' in '{1}'.", string.Join(", ", videoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName); + + return result; + } + + public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName) + { + var videoCodec = mediaInfo.VideoCodec; + + if (videoCodec.IsNullOrWhiteSpace()) + { + return videoCodec; + } + + if (videoCodec == "AVC") + { + return GetSceneNameMatch(sceneName, "AVC", "h264", "x264"); + } + + if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC") + { + return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265"); + } + + if (videoCodec == "MPEG-2 Video") + { + return "MPEG2"; + } + + if (videoCodec == "MPEG-4 Visual") + { + return GetSceneNameMatch(sceneName, "DivX", "XviD"); + } + + if (videoCodec.StartsWithIgnoreCase("XviD")) + { + return "XviD"; + } + + if (videoCodec.StartsWithIgnoreCase("DivX")) + { + return "DivX"; + } + + if (videoCodec.EqualsIgnoreCase("VC-1")) + { + return "VC1"; + } + + return videoCodec; + } + + private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoModel mediaInfo) + { + var audioChannelPositions = mediaInfo.AudioChannelPositions; + + if (audioChannelPositions.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + Logger.Debug("Formatting audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions); + + if (audioChannelPositions.Contains("+")) + { + return audioChannelPositions.Split('+') + .Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture)); + } + + if (audioChannelPositions.Contains("/")) + { + return Regex.Replace(audioChannelPositions, @"^\d+\sobjects", "", RegexOptions.Compiled | RegexOptions.IgnoreCase) + .Replace("Object Based / ", "") + .Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault() + ?.Split('/') + .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); + } + } + catch (Exception e) + { + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositions'"); + } + + return null; + } + + private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel mediaInfo) + { + var audioChannelPositionsText = mediaInfo.AudioChannelPositionsText; + var audioChannels = mediaInfo.AudioChannels; + + if (audioChannelPositionsText.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + Logger.Debug("Formatting audio channels using 'AudioChannelPositionsText', with a value of: '{0}'", audioChannelPositionsText); + + return audioChannelPositionsText.ContainsIgnoreCase("LFE") ? audioChannels - 1 + 0.1m : audioChannels; + } + catch (Exception e) + { + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText'"); + } + + return null; + } + + private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel mediaInfo) + { + var audioChannels = mediaInfo.AudioChannels; + + if (mediaInfo.SchemaRevision >= 3) + { + Logger.Debug("Formatting audio channels using 'AudioChannels', with a value of: '{0}'", audioChannels); + + return audioChannels; + } + + return null; + } + + private static string GetSceneNameMatch(string sceneName, params string[] tokens) + { + sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; + + foreach (var token in tokens) + { + if (sceneName.ContainsIgnoreCase(token)) + { + return token; + } + } + + // Last token is the default. + return tokens.Last(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 8d8b0342e8..fe62216cd1 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Linq; -using System.Linq.Expressions; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -10,12 +9,23 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo { public class MediaInfoModel : IEmbeddedDocument { + public string ContainerFormat { get; set; } 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 VideoMultiViewCount { get; set; } + public string VideoColourPrimaries { get; set; } + public string VideoTransferCharacteristics { 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 string AudioAdditionalFeatures { get; set; } public int AudioBitrate { get; set; } public TimeSpan RunTime { get; set; } public int AudioStreamCount { get; set; } @@ -28,40 +38,5 @@ public class MediaInfoModel : IEmbeddedDocument public string Subtitles { get; set; } public string ScanType { get; set; } public int SchemaRevision { get; set; } - - [JsonIgnore] - public decimal FormattedAudioChannels - { - get - { - try - { - return - AudioChannelPositions.Replace("Object Based /", "").Replace(" / ", "$") - .Split('$') - .First() - .Split('/') - .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); - } - catch - { - - if (AudioChannelPositionsText.IsNullOrWhiteSpace()) - { - if (SchemaRevision >= 3) - { - return AudioChannels; - } - - return 0; - } - - return AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? AudioChannels - 1 + 0.1m : AudioChannels; - - - } - - } - } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index e1ab325d9b..0c10a33c8a 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -18,8 +18,6 @@ public class UpdateMediaInfoService : IHandle private readonly IConfigService _configService; private readonly Logger _logger; - private const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 3; - public UpdateMediaInfoService(IDiskProvider diskProvider, IMediaFileService mediaFileService, IVideoFileInfoReader videoFileInfoReader, @@ -49,7 +47,6 @@ private void UpdateMediaInfo(Movie movie, List mediaFiles) if (mediaFile.MediaInfo != null) { - mediaFile.MediaInfo.SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION; _mediaFileService.Update(mediaFile); _logger.Debug("Updated MediaInfo for '{0}'", path); } @@ -65,7 +62,7 @@ public void Handle(MovieScannedEvent message) } var allMediaFiles = _mediaFileService.GetFilesByMovie(message.Movie.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 < VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION).ToList(); UpdateMediaInfo(message.Movie, filteredMediaFiles); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 5f141b1886..a3f03eed15 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -17,6 +17,8 @@ public class VideoFileInfoReader : IVideoFileInfoReader private readonly IDiskProvider _diskProvider; private readonly Logger _logger; + public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 4; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 5; public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger) { @@ -90,6 +92,7 @@ public MediaInfoModel GetMediaInfo(string filename) int audioChannels; int videoBitDepth; decimal videoFrameRate; + int videoMultiViewCount; string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List"); string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType"); @@ -102,65 +105,60 @@ public MediaInfoModel GetMediaInfo(string filename) } decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate); int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth); + int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "MultiView_Count"), out videoMultiViewCount); //Runtime int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime); 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"), - VideoBitrate = videoBitRate, - VideoBitDepth = videoBitDepth, - Height = height, - Width = width, - AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), - AudioBitrate = audioBitRate, - RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime), - AudioStreamCount = streamCount, - AudioChannels = audioChannels, - AudioChannelPositions = audioChannelPositions, - AudioChannelPositionsText = audioChannelPositionsText, - AudioProfile = audioProfile.Trim(), - VideoFps = videoFrameRate, - AudioLanguages = audioLanguages, - Subtitles = subtitles, - ScanType = scanType - }; + { + 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, + VideoMultiViewCount = videoMultiViewCount, + VideoColourPrimaries = mediaInfo.Get(StreamKind.Video, 0, "colour_primaries"), + VideoTransferCharacteristics = mediaInfo.Get(StreamKind.Video, 0, "transfer_characteristics"), + 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"), + AudioAdditionalFeatures = mediaInfo.Get(StreamKind.Audio, 0, "Format_AdditionalFeatures"), + AudioBitrate = audioBitRate, + RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime), + AudioStreamCount = streamCount, + AudioChannels = audioChannels, + AudioChannelPositions = audioChannelPositions, + AudioChannelPositionsText = audioChannelPositionsText, + VideoFps = videoFrameRate, + AudioLanguages = audioLanguages, + Subtitles = subtitles, + ScanType = scanType, + SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION + }; return mediaInfoModel; } @@ -175,7 +173,7 @@ public MediaInfoModel GetMediaInfo(string filename) } catch (Exception ex) { - _logger.Error(ex, "Unable to parse media info from file: " + filename); + _logger.Error(ex, "Unable to parse media info from file: {0}", filename); } finally { diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs index 0414611c87..da51c9f3a8 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFile.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Qualities; using NzbDrone.Core.Movies; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.MediaFiles { @@ -27,5 +28,20 @@ public override string ToString() { return string.Format("[{0}] {1}", Id, RelativePath); } + + public string GetSceneOrFileName() + { + if (SceneName.IsNotNullOrWhiteSpace()) + { + return SceneName; + } + + if (RelativePath.IsNotNullOrWhiteSpace()) + { + return System.IO.Path.GetFileName(RelativePath); + } + + return string.Empty; +} } } diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs index 9b6899253c..610e61f7b1 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportDecisionMaker.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -37,6 +38,7 @@ public class ImportDecisionMaker : IMakeImportDecision private readonly IQualityDefinitionService _qualitiesService; private readonly IConfigService _config; private readonly IHistoryService _historyService; + private readonly ICached _warnedFiles; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, @@ -48,6 +50,7 @@ public ImportDecisionMaker(IEnumerable speci IQualityDefinitionService qualitiesService, IConfigService config, IHistoryService historyService, + ICacheManager cacheManager, Logger logger) { _specifications = specifications; @@ -59,6 +62,7 @@ public ImportDecisionMaker(IEnumerable speci _qualitiesService = qualitiesService; _config = config; _historyService = historyService; + _warnedFiles = cacheManager.GetCache(this.GetType()); _logger = logger; } @@ -148,7 +152,16 @@ private ImportDecision GetDecision(string file, Movie movie, DownloadClientItem if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) { - _logger.Warn("Unable to parse movie info from path {0}", file); + if (_warnedFiles.Find(file) == null) + { + _warnedFiles.Set(file, "warned"); + _logger.Warn("Unable to parse movie info from path {0}", file); + } + else + { + _logger.Trace("Already warned user that we are unable to parse movie info from path: {0}", file); + } + } decision = new ImportDecision(localMovie, new Rejection("Unable to parse file")); diff --git a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs index ece7770537..d77f94001e 100644 --- a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileQualityService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.History; using NzbDrone.Core.MediaFiles.Commands; @@ -9,6 +10,7 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using RestSharp.Extensions; namespace NzbDrone.Core.MediaFiles { @@ -51,6 +53,7 @@ public void Execute(UpdateMovieFileQualityCommand command) var history = _historyService.FindByMovieId(movieFile.MovieId).OrderByDescending(h => h.Date); var latestImported = history.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadFolderImported); + var latestImportedName = latestImported?.SourceTitle; var latestGrabbed = history.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed); var sizeMovie = new LocalMovie(); sizeMovie.Size = movieFile.Size; @@ -67,7 +70,18 @@ public void Execute(UpdateMovieFileQualityCommand command) helpers.Add(latestGrabbed); } - var parsedMovieInfo = _parsingService.ParseMovieInfo(latestImported?.SourceTitle ?? movieFile.RelativePath, helpers); + ParsedMovieInfo parsedMovieInfo = null; + + if (latestImportedName?.IsNotNullOrWhiteSpace() == true) + { + parsedMovieInfo = _parsingService.ParseMovieInfo(latestImportedName, helpers); + } + + if (parsedMovieInfo == null) + { + _logger.Debug("Could not parse movie info from history source title, using current path instead: {0}.", movieFile.RelativePath); + parsedMovieInfo = _parsingService.ParseMovieInfo(movieFile.RelativePath, helpers); + } //Only update Custom formats for now. if (parsedMovieInfo != null) @@ -78,7 +92,7 @@ public void Execute(UpdateMovieFileQualityCommand command) } else { - _logger.Debug("Could not update custom formats for {0}, since it's title could not be parsed!", movieFile); + _logger.Warn("Could not update custom formats for {0}, since it's title could not be parsed!", movieFile); } count++; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index b253cad5ef..59f5a23e45 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -1,9 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Notifications.Xbmc.Model; using NzbDrone.Core.Movies; +using NzbDrone.Common.Disk; +using System.IO; namespace NzbDrone.Core.Notifications.Xbmc { @@ -85,6 +87,7 @@ private void UpdateMovieLibrary(XbmcSettings settings, Movie movie) if (moviePath != null) { + moviePath = new OsPath(moviePath).Directory.FullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); _logger.Debug("Updating movie {0} (Path: {1}) on XBMC host: {2}", movie, moviePath, settings.Address); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 866ba4b6a2..691004d60d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -847,6 +847,7 @@ Code + @@ -1318,11 +1319,11 @@ - - \ No newline at end of file + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 71b06837d6..e31c573716 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -9,6 +9,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Qualities; using NzbDrone.Core.Movies; @@ -333,12 +334,12 @@ private void AddImdbIdTokens(Dictionary> tokenH tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; } - private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) + private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile movieFile) { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(movieFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(movieFile); //tokenHandlers["{IMDb Id}"] = m => - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Radarr"); + tokenHandlers["{Release Group}"] = m => movieFile.ReleaseGroup ?? m.DefaultValue("Radarr"); } private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) @@ -366,98 +367,22 @@ private void AddMediaInfoTokens(Dictionary> tok { if (movieFile.MediaInfo == null) return; - string videoCodec; - switch (movieFile.MediaInfo.VideoCodec) + var sceneName = movieFile.GetSceneOrFileName(); + + var videoCodec = MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName); + var audioCodec = MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName); + var audioChannels = MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo); + + // Workaround until https://github.com/MediaArea/MediaInfo/issues/299 is fixed and release + if (audioCodec.EqualsIgnoreCase("DTS-X")) { - case "AVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = movieFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (movieFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "Atmos / TrueHD": - audioCodec = "Atmos TrueHD"; - break; - - case "MPEG Audio": - if (movieFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - if (movieFile.MediaInfo.AudioProfile == "ES" || movieFile.MediaInfo.AudioProfile == "ES Discrete" || movieFile.MediaInfo.AudioProfile == "ES Matrix") - { - audioCodec = "DTS-ES"; - } - else if (movieFile.MediaInfo.AudioProfile == "MA") - { - audioCodec = "DTS-HD MA"; - } - else if (movieFile.MediaInfo.AudioProfile == "HRA") - { - audioCodec = "DTS-HD HRA"; - } - else if (movieFile.MediaInfo.AudioProfile == "X") - { - audioCodec = "DTS-X"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - default: - audioCodec = movieFile.MediaInfo.AudioFormat; - break; + audioChannels = audioChannels - 1 + 0.1m; } var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; } var mediaInfoAudioLanguagesAll = mediaInfoAudioLanguages; if (mediaInfoAudioLanguages == "[EN]") @@ -465,17 +390,32 @@ private void AddMediaInfoTokens(Dictionary> tok mediaInfoAudioLanguages = string.Empty; } - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; } var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? - movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; + var audioChannelsFormatted = audioChannels > 0 ? + audioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + var mediaInfo3D = movieFile.MediaInfo.VideoMultiViewCount > 1 ? "3D" : string.Empty; + + var videoColourPrimaries = movieFile.MediaInfo.VideoColourPrimaries ?? string.Empty; + var videoTransferCharacteristics = movieFile.MediaInfo.VideoTransferCharacteristics ?? string.Empty; + var mediaInfoHDR = string.Empty; + + if (movieFile.MediaInfo.VideoBitDepth >= 10 && !videoColourPrimaries.IsNullOrWhiteSpace() && !videoTransferCharacteristics.IsNullOrWhiteSpace()) + { + string[] validTransferFunctions = new string[] { "PQ", "HLG" }; + + if (videoColourPrimaries.EqualsIgnoreCase("BT.2020") && validTransferFunctions.Any(videoTransferCharacteristics.Contains)) + { + mediaInfoHDR = "HDR"; + } + } tokenHandlers["{MediaInfo Video}"] = m => videoCodec; tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; @@ -483,14 +423,17 @@ private void AddMediaInfoTokens(Dictionary> tok tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted; tokenHandlers["{MediaInfo AudioLanguages}"] = m => mediaInfoAudioLanguages; tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => mediaInfoAudioLanguagesAll; + tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => mediaInfoSubtitleLanguages; + + tokenHandlers["{MediaInfo 3D}"] = m => mediaInfo3D; + tokenHandlers["{MediaInfo HDR}"] = m => mediaInfoHDR; + + tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; + tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{mediaInfoAudioLanguages} {mediaInfoSubtitleLanguages}"; } private string GetLanguagesToken(string mediaInfoLanguages) @@ -610,24 +553,24 @@ private string GetQualityReal(Movie movie, QualityModel quality) return string.Empty; } - private string GetOriginalTitle(MovieFile episodeFile) + private string GetOriginalTitle(MovieFile movieFile) { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) + if (movieFile.SceneName.IsNullOrWhiteSpace()) { - return GetOriginalFileName(episodeFile); + return GetOriginalFileName(movieFile); } - return episodeFile.SceneName; + return movieFile.SceneName; } - private string GetOriginalFileName(MovieFile episodeFile) + private string GetOriginalFileName(MovieFile movieFile) { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + if (movieFile.RelativePath.IsNullOrWhiteSpace()) { - return Path.GetFileNameWithoutExtension(episodeFile.Path); + return Path.GetFileNameWithoutExtension(movieFile.Path); } - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + return Path.GetFileNameWithoutExtension(movieFile.RelativePath); } } diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 58d785b98a..28cb60982a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -26,8 +26,11 @@ public FileNameSampleService(IBuildFileNames buildFileNames) var mediaInfo = new MediaInfoModel() { - VideoCodec = "AVC", - VideoBitDepth = 8, + VideoFormat = "AVC", + VideoBitDepth = 10, + VideoMultiViewCount = 2, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", AudioFormat = "DTS", AudioChannels = 6, AudioChannelPositions = "3/2/0.1", @@ -37,8 +40,11 @@ public FileNameSampleService(IBuildFileNames buildFileNames) var mediaInfoAnime = new MediaInfoModel() { - VideoCodec = "AVC", - VideoBitDepth = 8, + VideoFormat = "AVC", + VideoBitDepth = 10, + VideoMultiViewCount = 2, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", AudioFormat = "DTS", AudioChannels = 6, AudioChannelPositions = "3/2/0.1", diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 2c32ef7e64..dcf5568ceb 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -26,7 +26,7 @@ public interface IParsingService MappingResult Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null); ParsedMovieInfo ParseMovieInfo(string title, List helpers); ParsedMovieInfo ParseMoviePathInfo(string path, List helpers); - ParsedMovieInfo ParseMinimalMovieInfo(string path); + ParsedMovieInfo ParseMinimalMovieInfo(string path, bool isDir = false); ParsedMovieInfo ParseMinimalPathMovieInfo(string path); List ParseCustomFormat(ParsedMovieInfo movieInfo); List MatchFormatTags(ParsedMovieInfo movieInfo); @@ -192,16 +192,16 @@ public LocalMovie GetLocalMovie(string filename, ParsedMovieInfo minimalInfo, Mo }; } - public ParsedMovieInfo ParseMinimalMovieInfo(string file) + public ParsedMovieInfo ParseMinimalMovieInfo(string file, bool isDir = false) { - return Parser.ParseMovieTitle(file, _config.ParsingLeniency > 0); + return Parser.ParseMovieTitle(file, _config.ParsingLeniency > 0, isDir); } public ParsedMovieInfo ParseMinimalPathMovieInfo(string path) { var fileInfo = new FileInfo(path); - var result = ParseMinimalMovieInfo(fileInfo.Name); + var result = ParseMinimalMovieInfo(fileInfo.Name, true); if (result == null) { diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 1b635cd05a..e498b2d5ec 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -104,13 +104,6 @@ public static QualityModel ParseQuality(string name) } } - if (RawHDRegex.IsMatch(normalizedName)) - { - result.Modifier = Modifier.RAWHD; - result.Source = Source.TV; - return result; - } - var sourceMatch = SourceRegex.Matches(normalizedName).OfType().LastOrDefault(); var resolution = ParseResolution(normalizedName); var codecRegex = CodecRegex.Match(normalizedName); @@ -129,6 +122,13 @@ public static QualityModel ParseQuality(string name) result.Source = Source.BLURAY; return result; //We found remux! } + + if (RawHDRegex.IsMatch(normalizedName) && result.Modifier != Modifier.BRDISK) + { + result.Modifier = Modifier.RAWHD; + result.Source = Source.TV; + return result; + } if (sourceMatch != null && sourceMatch.Success) { diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index 229c5f8baa..437e102a70 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -15,6 +15,7 @@ public interface IRootFolderService { List All(); List AllWithUnmappedFolders(); + List AllWithSpace(); RootFolder Add(RootFolder rootDir); void Remove(int id); RootFolder Get(int id); @@ -62,6 +63,31 @@ public List All() return rootFolders; } + public List AllWithSpace() + { + var rootFolders = _rootFolderRepository.All().ToList(); + + rootFolders.ForEach(folder => + { + try + { + if (folder.Path.IsPathValid() && _diskProvider.FolderExists(folder.Path)) + { + folder.FreeSpace = _diskProvider.GetAvailableSpace(folder.Path); + folder.TotalSpace = _diskProvider.GetTotalSize(folder.Path); + } + } + //We don't want an exception to prevent the root folders from loading in the UI, so they can still be deleted + catch (Exception ex) + { + folder.FreeSpace = 0; + _logger.Error(ex, "Unable to get free space for root folder {0}", folder.Path); + } + }); + + return rootFolders; + } + public List AllWithUnmappedFolders() { var rootFolders = _rootFolderRepository.All().ToList(); diff --git a/src/NzbDrone.Mono/Disk/DiskProvider.cs b/src/NzbDrone.Mono/Disk/DiskProvider.cs index 2f1b4c93a3..6be2ee4a34 100644 --- a/src/NzbDrone.Mono/Disk/DiskProvider.cs +++ b/src/NzbDrone.Mono/Disk/DiskProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -189,8 +189,6 @@ private uint GetGroupId(string group) } return g.gr_gid; - - } } } diff --git a/src/NzbDrone.Mono/Disk/ProcMountProvider.cs b/src/NzbDrone.Mono/Disk/ProcMountProvider.cs index 48dc9f0a9a..f810fd89f8 100644 --- a/src/NzbDrone.Mono/Disk/ProcMountProvider.cs +++ b/src/NzbDrone.Mono/Disk/ProcMountProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -111,6 +111,18 @@ private IMount ParseLine(string line) return null; } + if (mount.StartsWith("/var/lib/")) + { + // Could be /var/lib/docker when docker uses zfs. Very unlikely that a useful mount is located in /var/lib. + return null; + } + + if (mount.StartsWith("/snap/")) + { + // Mount point for snap packages + return null; + } + var driveType = FindDriveType.Find(type); if (name.StartsWith("/dev/") || GetFileSystems().GetValueOrDefault(type, false)) diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 2a8af263dd..0bed10417e 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 +# Visual Studio 14 VisualStudioVersion = 14.0.24720.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" diff --git a/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs b/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs index 62529c9557..5c21849931 100644 --- a/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs +++ b/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs @@ -1,2 +1,4 @@ {{path}}
-{{#if movieFile.relativePath}} {{movieFile.relativePath}}{{else}} Movie File Not Found{{/if}} + +{{#if movieFile.relativePath}}┕ {{movieFile.relativePath}}{{else}}┕ Movie File Not Found{{/if}} + diff --git a/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs b/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs index 2527b21e6b..d1f3da9ba3 100644 --- a/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs +++ b/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs @@ -1,5 +1,5 @@ {{#if_gt proper compare="1"}} - {{movieFile.quality.qualityDefinition.title}} + {{movieFile.quality.quality.name}} {{else}} - {{movieFile.quality.qualityDefinition.title}} + {{movieFile.quality.quality.name}} {{/if_gt}} diff --git a/src/UI/AddMovies/addMovies.less b/src/UI/AddMovies/addMovies.less index af5a114c8d..0a04e83c22 100644 --- a/src/UI/AddMovies/addMovies.less +++ b/src/UI/AddMovies/addMovies.less @@ -1,3 +1,4 @@ +@import "../Content/Bootstrap/mixins"; @import "../Shared/Styles/card.less"; @import "../Shared/Styles/clickable.less"; @@ -130,6 +131,13 @@ .hint { color : #999999; font-style : italic; + + &.path { + .text-overflow(); + + display: block; + font-size: 12px; + } } .monitor-tooltip-contents { diff --git a/src/UI/Cells/MediaInfoCell.js b/src/UI/Cells/MediaInfoCell.js index ed42380a38..f19ef871a6 100644 --- a/src/UI/Cells/MediaInfoCell.js +++ b/src/UI/Cells/MediaInfoCell.js @@ -12,7 +12,7 @@ module.exports = NzbDroneCell.extend({ if (runtime) { runtime = runtime.split(".")[0]; } - var video = "{0} ({1}x{2}) ({3})".format(info.videoCodec, info.width, info.height, runtime); + var video = "{0} ({1}x{2}) ({3})".format(info.videoFormat || info.videoCodec, info.width, info.height, runtime); var audio = "{0} ({1})".format(info.audioFormat, info.audioLanguages); this.$el.html(video + " " + audio); } diff --git a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs index 853f3a42b0..0239c2d95b 100644 --- a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs +++ b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs @@ -31,11 +31,15 @@ -
+
+ + + +
diff --git a/src/UI/Settings/NetImport/Edit/NetImportEditView.js b/src/UI/Settings/NetImport/Edit/NetImportEditView.js index cd8490f37e..8d31ef6935 100644 --- a/src/UI/Settings/NetImport/Edit/NetImportEditView.js +++ b/src/UI/Settings/NetImport/Edit/NetImportEditView.js @@ -44,11 +44,9 @@ var view = Marionette.ItemView.extend({ onRender : function() { var rootFolder = this.model.get("rootFolderPath"); if (rootFolder !== "") { - //this.ui.rootFolder.val(rootFolder); this.ui.rootFolder.children().filter(function() { - //may want to use $.trim in here - return $(this).text() === rootFolder; - }).attr('selected', true); + return $.trim($(this).text()) === rootFolder; + }).prop('selected', true); } else { var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); if (RootFolders.get(defaultRoot)) { @@ -58,7 +56,7 @@ var view = Marionette.ItemView.extend({ }, _onBeforeSave : function() { - var profile = this.ui.profile.val(); + var profile = parseInt(this.ui.profile.val(), 10); var minAvail = this.ui.minimumAvailability.val(); var rootFolderPath = this.ui.rootFolder.children(':selected').text(); this.model.set({