From 9cb220bf2a977765364dac9519d9d231552a37b5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 21 Nov 2013 08:13:40 -0800 Subject: [PATCH 01/30] dllmap added for MediaInfo.DLL Fall back to filesize check if mediainfo is not available Ubuntu package depends on sqlite3 and mediainfo New: mediainfo now used on mono to check runtime when available --- build.ps1 | 15 ++-- debian/control | 2 +- src/MediaInfoDotNet.dll.config | 5 ++ .../NotSampleSpecificationFixture.cs | 40 ++++------- .../MediaInfo/VideoFileInfoReaderFixture.cs | 10 +++ .../Specifications/NotSampleSpecification.cs | 17 +++-- .../MediaFiles/MediaInfo/MediaInfoModel.cs | 6 +- .../MediaInfo/VideoFileInfoReader.cs | 68 ++++++++----------- 8 files changed, 84 insertions(+), 79 deletions(-) create mode 100644 src/MediaInfoDotNet.dll.config diff --git a/build.ps1 b/build.ps1 index 486549865..a84a1de8e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,6 +3,7 @@ $outputFolder = '.\_output' $outputFolderMono = '.\_output_mono' $testPackageFolder = '.\_tests\' $testSearchPattern = '*.Test\bin\x86\Release' +$sourceFolder = '.\src' Function Build() { @@ -84,6 +85,9 @@ Function PackageMono() get-childitem $outputFolderMono -File -Filter sqlite3.* -Recurse | foreach ($_) {remove-item $_.fullname} get-childitem $outputFolderMono -File -Filter MediaInfo.* -Recurse | foreach ($_) {remove-item $_.fullname} + Write-Host "Adding MediaInfoDotNet.dll.config (for dllmap)" + Copy-Item "$sourceFolder\MediaInfoDotNet.dll.config" $outputFolderMono + Write-Host Renaming NzbDrone.Console.exe to NzbDrone.exe get-childitem $outputFolderMono -File -Filter NzbDrone.exe -Recurse | foreach ($_) {remove-item $_.fullname} Rename-Item "$outputFolderMono\NzbDrone.Console.exe" "NzbDrone.exe" @@ -103,7 +107,7 @@ Function PackageTests() { Write-Host Packaging Tests - Write-Host "##teamcity[progressStart 'Creating Mono Package']" + Write-Host "##teamcity[progressStart 'Creating Test Package']" if(Test-Path $testPackageFolder) { @@ -117,8 +121,8 @@ Function PackageTests() .\src\.nuget\NuGet.exe install NUnit.Runners -Version 2.6.1 -Output $testPackageFolder - Copy-Item $outputFolder\*.dll -Destination $testPackageFolder -Force - Copy-Item $outputFolder\*.pdb -Destination $testPackageFolder -Force + Copy-Item $outputFolder\*.dll -Destination $testPackageFolder -Force + Copy-Item $outputFolder\*.pdb -Destination $testPackageFolder -Force Copy-Item .\*.sh -Destination $testPackageFolder -Force @@ -126,7 +130,10 @@ Function PackageTests() CleanFolder $testPackageFolder - Write-Host "##teamcity[progressFinish 'Creating Mono Package']" + Write-Host "Adding MediaInfoDotNet.dll.config (for dllmap)" + Copy-Item "$sourceFolder\MediaInfoDotNet.dll.config" -Destination $testPackageFolder -Force + + Write-Host "##teamcity[progressFinish 'Creating Test Package']" } diff --git a/debian/control b/debian/control index e7f6a7b11..99d801309 100644 --- a/debian/control +++ b/debian/control @@ -8,5 +8,5 @@ Vcs-Browser: https://github.com/NzbDrone/NzbDrone Package: nzbdrone Architecture: all -Depends: libmono-cil-dev (>= 2.10.1) +Depends: libmono-cil-dev (>= 2.10.1), sqlite3 (>= 3.7), mediainfo (>= 0.7.52) Description: NZBDrone is a PVR for newsgroup users diff --git a/src/MediaInfoDotNet.dll.config b/src/MediaInfoDotNet.dll.config new file mode 100644 index 000000000..3de7bdea3 --- /dev/null +++ b/src/MediaInfoDotNet.dll.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index b8fe97056..9088e46ee 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -85,21 +85,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications } [Test] - public void should_not_run_runtime_check_on_linux() + public void should_use_runtime() { - LinuxOnly(); - GivenFileSize(1000.Megabytes()); - - Subject.IsSatisfiedBy(_localEpisode); - - Mocker.GetMock().Verify(v => v.GetRunTime(It.IsAny()), Times.Never()); - } - - [Test] - public void should_run_runtime_check_on_windows() - { - WindowsOnly(); - GivenRuntime(120); GivenFileSize(1000.Megabytes()); @@ -111,7 +98,6 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_false_if_runtime_is_less_than_minimum() { - WindowsOnly(); GivenRuntime(60); Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); @@ -120,32 +106,30 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications [Test] public void should_return_true_if_runtime_greater_than_than_minimum() { - WindowsOnly(); GivenRuntime(120); Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } [Test] - public void should_return_false_if_file_size_is_under_minimum() + public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_acceptable_size() { - LinuxOnly(); + Mocker.GetMock() + .Setup(s => s.GetRunTime(It.IsAny())) + .Throws(); - GivenRuntime(120); - GivenFileSize(20.Megabytes()); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + GivenFileSize(1000.Megabytes()); + Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } [Test] - public void should_return_false_if_file_size_is_under_minimum_for_larger_limits() + public void should_fall_back_to_file_size_if_mediainfo_dll_not_found_undersize() { - LinuxOnly(); - - GivenRuntime(120); - GivenFileSize(120.Megabytes()); - _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); + Mocker.GetMock() + .Setup(s => s.GetRunTime(It.IsAny())) + .Throws(); + GivenFileSize(1.Megabytes()); Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index c3267089b..dbf6aba36 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -1,6 +1,8 @@ using System.IO; using FluentAssertions; +using Moq; using NUnit.Framework; +using NzbDrone.Common; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.Categories; @@ -11,6 +13,14 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo [DiskAccessTest] public class VideoFileInfoReaderFixture : CoreTest { + [SetUp] + public void Setup() + { + Mocker.GetMock() + .Setup(s => s.FileExists(It.IsAny())) + .Returns(true); + } + [Test] public void get_runtime() { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index ccf833ce7..7e2fd7ed6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return true; } - if (OsInfo.IsWindows) + try { var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path); @@ -76,12 +76,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Trace("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime); return false; } - - _logger.Trace("Runtime is over 2 minutes, skipping file size check"); - return true; } - return CheckSize(localEpisode); + catch (DllNotFoundException) + { + _logger.Trace("Falling back to file size detection"); + + return CheckSize(localEpisode); + } + + _logger.Trace("Runtime is over 90 seconds"); + return true; } private bool CheckSize(LocalEpisode localEpisode) @@ -90,12 +95,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { if (localEpisode.Size < SampleSizeLimit * 2) { + _logger.Trace("1080p file is less than sample limit"); return false; } } if (localEpisode.Size < SampleSizeLimit) { + _logger.Trace("File is less than sample limit"); return false; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 69e8f4760..c18ec0870 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Core.MediaFiles.MediaInfo +using System; + +namespace NzbDrone.Core.MediaFiles.MediaInfo { public class MediaInfoModel { @@ -8,7 +10,7 @@ public int Height { get; set; } public string AudioFormat { get; set; } public int AudioBitrate { get; set; } - public int RunTime { get; set; } + public TimeSpan RunTime { get; set; } public int AudioStreamCount { get; set; } public int AudioChannels { get; set; } public string AudioProfile { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 2ad10fa14..60cee01a4 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -30,10 +30,11 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo if (!_diskProvider.FileExists(filename)) throw new FileNotFoundException("Media file does not exist: " + filename); - var mediaInfo = new MediaInfoLib.MediaInfo(); + MediaInfoLib.MediaInfo mediaInfo = null; try { + mediaInfo = new MediaInfoLib.MediaInfo(); _logger.Trace("Getting media info from {0}", filename); mediaInfo.Option("ParseSpeed", "0.2"); @@ -56,26 +57,26 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate); string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate"); - int ABindex = aBitRate.IndexOf(" /"); - if (ABindex > 0) - aBitRate = aBitRate.Remove(ABindex); + int aBindex = aBitRate.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); + if (aBindex > 0) + aBitRate = aBitRate.Remove(aBindex); Int32.TryParse(aBitRate, out audioBitRate); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out runTime); Int32.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount); string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)"); - int ACindex = audioChannelsStr.IndexOf(" /"); - if (ACindex > 0) - audioChannelsStr = audioChannelsStr.Remove(ACindex); + int aCindex = audioChannelsStr.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); + if (aCindex > 0) + audioChannelsStr = audioChannelsStr.Remove(aCindex); string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List"); decimal videoFrameRate = Decimal.Parse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate")); string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile"); - int APindex = audioProfile.IndexOf(" /"); - if (APindex > 0) - audioProfile = audioProfile.Remove(APindex); + int aPindex = audioProfile.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); + if (aPindex > 0) + audioProfile = audioProfile.Remove(aPindex); Int32.TryParse(audioChannelsStr, out audioChannels); var mediaInfoModel = new MediaInfoModel @@ -84,9 +85,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo VideoBitrate = videoBitRate, Height = height, Width = width, + AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), AudioBitrate = audioBitRate, - RunTime = (runTime / 1000), //InSeconds + RunTime = TimeSpan.FromMilliseconds(runTime), AudioStreamCount = streamCount, AudioChannels = audioChannels, AudioProfile = audioProfile.Trim(), @@ -100,34 +102,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return mediaInfoModel; } } - catch (Exception ex) + catch (DllNotFoundException ex) { - _logger.ErrorException("Unable to parse media info from file: " + filename, ex); - mediaInfo.Close(); - } - - return null; - } - - public TimeSpan GetRunTime(string filename) - { - MediaInfoLib.MediaInfo mediaInfo = null; - try - { - mediaInfo = new MediaInfoLib.MediaInfo(); - _logger.Trace("Getting media info from {0}", filename); - - mediaInfo.Option("ParseSpeed", "0.2"); - int open = mediaInfo.Open(filename); - - if (open != 0) - { - int runTime; - Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out runTime); - - mediaInfo.Close(); - return TimeSpan.FromMilliseconds(runTime); - } + _logger.ErrorException("mediainfo is required but was not found", ex); + throw; } catch (Exception ex) { @@ -141,7 +119,19 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo } } - return new TimeSpan(); + return null; + } + + public TimeSpan GetRunTime(string filename) + { + var info = GetMediaInfo(filename); + + if (info == null) + { + return new TimeSpan(); + } + + return info.RunTime; } } } From 37ae2d04e3161eb5d8435c17d639d6c9797d99d4 Mon Sep 17 00:00:00 2001 From: kayone Date: Mon, 2 Dec 2013 14:11:17 -0800 Subject: [PATCH 02/30] fixed newznab validation when URL is null. --- .../NewznabTests/NewznabSettingFixture.cs | 20 ++++++++++++++++++- .../Indexers/Newznab/NewznabSettings.cs | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs index 7ef91def5..4bd26817d 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/NewznabTests/NewznabSettingFixture.cs @@ -21,7 +21,25 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests setting.Validate().IsValid.Should().BeFalse(); setting.Validate().Errors.Should().Contain(c => c.PropertyName == "ApiKey"); - + + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(null)] + public void invalid_url_should_not_apikey(string url) + { + var setting = new NewznabSettings + { + ApiKey = "", + Url = url + }; + + + setting.Validate().IsValid.Should().BeFalse(); + setting.Validate().Errors.Should().NotContain(c => c.PropertyName == "ApiKey"); + setting.Validate().Errors.Should().Contain(c => c.PropertyName == "Url"); + } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index c5a0eb230..012e09a7d 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -23,6 +23,11 @@ namespace NzbDrone.Core.Indexers.Newznab private static bool ShouldHaveApiKey(NewznabSettings settings) { + if (settings.Url == null) + { + return false; + } + return ApiKeyWhiteList.Any(c => settings.Url.ToLowerInvariant().Contains(c)); } From d5cbd5dc33f96e1a2790015318b56837f5e3f404 Mon Sep 17 00:00:00 2001 From: kayone Date: Mon, 2 Dec 2013 14:41:19 -0800 Subject: [PATCH 03/30] added nzbindex.in to list of newznab indexers that require API Key. --- src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index 012e09a7d..7012b2f1d 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Indexers.Newznab "nzbplanet.net", "nzbid.org", "nzbndx.com", + "nzbindex.in" }; private static bool ShouldHaveApiKey(NewznabSettings settings) From b055fc5ade365f324904a8e4bcb2558df1bdf2e4 Mon Sep 17 00:00:00 2001 From: kayone Date: Mon, 2 Dec 2013 22:41:08 -0800 Subject: [PATCH 04/30] added test for HistoryRepository.Grabbed() --- .../HistoryTests/HistoryRepositoryFixture.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs index 4ccc2364b..eeb6ef0c3 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs @@ -41,5 +41,19 @@ namespace NzbDrone.Core.Test.HistoryTests StoredModel.Data.Should().HaveCount(2); } + + [Test] + public void grabbed_should_return_grabbed_items() + { + var history = Builder + .CreateListOfSize(5) + .Random(3) + .With(c => c.EventType = HistoryEventType.Grabbed) + .BuildListOfNew(); + + Subject.InsertMany(history); + + Subject.Grabbed().Should().HaveCount(3); + } } } \ No newline at end of file From 76bc4aaa9ce851b5df58f7883185198a7a2cca8c Mon Sep 17 00:00:00 2001 From: kayone Date: Mon, 2 Dec 2013 22:41:40 -0800 Subject: [PATCH 05/30] Replaced manual argument validations with Ensure. --- src/NzbDrone.Common/DiskProvider.cs | 10 ++-------- src/NzbDrone.Core/Configuration/ConfigService.cs | 10 ++++------ src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs | 1 - src/NzbDrone.Core/Fluent.cs | 5 +++-- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/NzbDrone.Common/DiskProvider.cs b/src/NzbDrone.Common/DiskProvider.cs index ef5242fc7..13379c91c 100644 --- a/src/NzbDrone.Common/DiskProvider.cs +++ b/src/NzbDrone.Common/DiskProvider.cs @@ -506,10 +506,7 @@ namespace NzbDrone.Common private static long DriveFreeSpaceEx(string folderName) { - if (string.IsNullOrEmpty(folderName)) - { - throw new ArgumentNullException("folderName"); - } + Ensure.That(folderName, () => folderName).IsValidPath(); if (!folderName.EndsWith("\\")) { @@ -530,10 +527,7 @@ namespace NzbDrone.Common private static long DriveTotalSizeEx(string folderName) { - if (string.IsNullOrEmpty(folderName)) - { - throw new ArgumentNullException("folderName"); - } + Ensure.That(folderName, () => folderName).IsValidPath(); if (!folderName.EndsWith("\\")) { diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 89f542670..373385aa2 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Nzbget; @@ -305,9 +306,11 @@ namespace NzbDrone.Core.Configuration public string GetValue(string key, object defaultValue, bool persist = false) { + key = key.ToLowerInvariant(); + Ensure.That(key, () => key).IsNotNullOrWhiteSpace(); + EnsureCache(); - key = key.ToLowerInvariant(); string dbValue; if (_cache.TryGetValue(key, out dbValue) && dbValue != null && !String.IsNullOrEmpty(dbValue)) @@ -336,11 +339,6 @@ namespace NzbDrone.Core.Configuration { key = key.ToLowerInvariant(); - if (String.IsNullOrEmpty(key)) - throw new ArgumentOutOfRangeException("key"); - if (value == null) - throw new ArgumentNullException("key"); - _logger.Trace("Writing Setting to file. Key:'{0}' Value:'{1}'", key, value); var dbValue = _repository.Get(key); diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index c7e7b3441..f8dfed427 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -20,7 +20,6 @@ namespace NzbDrone.Core.DataAugmentation.Xem IXemProxy xemProxy, ISeriesService seriesService, ICacheManger cacheManger, Logger logger) { - if (seriesService == null) throw new ArgumentNullException("seriesService"); _episodeService = episodeService; _xemProxy = xemProxy; _seriesService = seriesService; diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 8ec82a8ab..b7ae4824b 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Common.EnsureThat; namespace NzbDrone.Core { @@ -9,8 +10,8 @@ namespace NzbDrone.Core { public static string WithDefault(this string actual, object defaultValue) { - if (defaultValue == null) - throw new ArgumentNullException("defaultValue"); + Ensure.That(defaultValue, () => defaultValue).IsNotNull(); + if (String.IsNullOrWhiteSpace(actual)) { return defaultValue.ToString(); From d4a9bd25bdb6d20c22c80f5315f6032498eb287d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 2 Dec 2013 23:31:31 -0800 Subject: [PATCH 06/30] Decimal.TryParse the frame rate instead of Decimal.Parse --- .../MediaFiles/MediaInfo/VideoFileInfoReader.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 60cee01a4..9ae5421af 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -49,12 +49,15 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo int runTime; int streamCount; int audioChannels; + decimal videoFrameRate; string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List"); string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType"); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate); + Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out runTime); + Decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), out videoFrameRate); string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate"); int aBindex = aBitRate.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); @@ -62,8 +65,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo aBitRate = aBitRate.Remove(aBindex); Int32.TryParse(aBitRate, out audioBitRate); - Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out runTime); Int32.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount); + string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)"); int aCindex = audioChannelsStr.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); @@ -71,7 +74,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo audioChannelsStr = audioChannelsStr.Remove(aCindex); string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List"); - decimal videoFrameRate = Decimal.Parse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate")); string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile"); int aPindex = audioProfile.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); From 149f94b0064fe5a581eb68b1c4c1a6c4fbc43599 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Dec 2013 07:30:13 -0800 Subject: [PATCH 07/30] Fixed event binding for episode file collection on episode modal --- src/UI/Episode/Summary/EpisodeSummaryLayout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayout.js b/src/UI/Episode/Summary/EpisodeSummaryLayout.js index cca3914fc..ca6665eab 100644 --- a/src/UI/Episode/Summary/EpisodeSummaryLayout.js +++ b/src/UI/Episode/Summary/EpisodeSummaryLayout.js @@ -93,7 +93,7 @@ define( }); } - this.listenTo(this.episodeFileCollection, 'all', this._collectionChanged); + this.listenTo(this.episodeFileCollection, 'add remove', this._collectionChanged); } else { From c3bd5e0053bcc08010775577c02dfff5f33c52e6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Dec 2013 07:39:02 -0800 Subject: [PATCH 08/30] Upgraded Filesize.js to 2.0.0 Fixed: File size/disk space in UI now shows actual size instead of manufacturer size (111 GB instead of 120 GB) --- src/UI/JsLibraries/filesize.js | 256 ++++++++++++++++----------------- src/UI/Shared/FormatHelpers.js | 2 +- 2 files changed, 123 insertions(+), 135 deletions(-) diff --git a/src/UI/JsLibraries/filesize.js b/src/UI/JsLibraries/filesize.js index b6f7b24ab..74f600064 100644 --- a/src/UI/JsLibraries/filesize.js +++ b/src/UI/JsLibraries/filesize.js @@ -6,148 +6,136 @@ * @license BSD-3 * @link http://filesizejs.com * @module filesize - * @version 1.10.0 + * @version 2.0.0 */ ( function ( global ) { - "use strict"; +"use strict"; - var base = 10, - right = /\.(.*)/, - bit = /b$/, - bite = /^B$/, - zero = /^0$/, - options; +var bit = /b$/, + bite = /^B$/, + radix = 10, + right = /\.(.*)/, + zero = /^0$/; - options = { - all : { - increments : [["B", 1], ["kb", 125], ["kB", 1000], ["Mb", 125000], ["MB", 1000000], ["Gb", 125000000], ["GB", 1000000000], ["Tb", 125000000000], ["TB", 1000000000000], ["Pb", 125000000000000], ["PB", 1000000000000000]], - nth : 11 - }, - bitless : { - increments : [["B", 1], ["kB", 1000], ["MB", 1000000], ["GB", 1000000000], ["TB", 1000000000000], ["PB", 1000000000000000]], - nth : 6 - } - }; +/** + * filesize + * + * @method filesize + * @param {Mixed} arg String, Int or Float to transform + * @param {Object} descriptor [Optional] Flags + * @return {String} Readable file size String + */ +function filesize ( arg, descriptor ) { + var result = "", + skip = false, + i = 6, + base, bits, neg, num, round, size, sizes, unix, spacer, suffix, z; - /** - * filesize - * - * @param {Mixed} arg String, Int or Float to transform - * @param {Mixed} pos [Optional] Position to round to, defaults to 2 if shrt is ommitted, or `true` for shrthand output - * @param {Boolean} bits [Optional] Determines if `bit` sizes are used for result calculation, default is true - * @return {String} Readable file size String - */ - function filesize ( arg) { - var result = "", - bits = true, - skip = false, - i, neg, num, pos, shrt, size, sizes, suffix, z; - - // Determining arguments - if (arguments[3] !== undefined) { - pos = arguments[1]; - shrt = arguments[2]; - bits = arguments[3]; - } - else { - typeof arguments[1] === "boolean" ? shrt = arguments[1] : pos = arguments[1]; - - if ( typeof arguments[2] === "boolean" ) { - bits = arguments[2]; - } - } - - if ( isNaN( arg ) || ( pos !== undefined && isNaN( pos ) ) ) { - throw new Error("Invalid arguments"); - } - - shrt = ( shrt === true ); - bits = ( bits === true ); - pos = shrt ? 1 : ( pos === undefined ? 2 : parseInt( pos, base ) ); - num = Number( arg ); - neg = ( num < 0 ); - - // Flipping a negative number to determine the size - if ( neg ) { - num = -num; - } - - // Zero is now a special case because bytes divide by 1 - if ( num === 0 ) { - if ( shrt ) { - result = "0"; - } - else { - result = "0 B"; - } - } - else { - if ( bits ) { - sizes = options.all.increments; - i = options.all.nth; - } - else { - sizes = options.bitless.increments; - i = options.bitless.nth; - } - - while ( i-- ) { - size = sizes[i][1]; - suffix = sizes[i][0]; - - if ( num >= size ) { - // Treating bytes as cardinal - if ( bite.test( suffix ) ) { - skip = true; - pos = 0; - } - - result = ( num / size ).toFixed( pos ); - - if ( !skip && shrt ) { - if ( bits && bit.test( suffix ) ) { - suffix = suffix.toLowerCase(); - } - - suffix = suffix.charAt( 0 ); - z = right.exec( result ); - - if ( suffix === "k" ) { - suffix = "K"; - } - - if ( z !== null && z[1] !== undefined && zero.test( z[1] ) ) { - result = parseInt( result, base ); - } - - result += suffix; - } - else if ( !shrt ) { - result += " " + suffix; - } - break; - } - } - } - - // Decorating a 'diff' - if ( neg ) { - result = "-" + result; - } - - return result; + if ( isNaN( arg ) ) { + throw new Error( "Invalid arguments" ); } - // CommonJS, AMD, script tag - if ( typeof exports !== "undefined" ) { - module.exports = filesize; + descriptor = descriptor || {}; + bits = ( descriptor.bits === true ); + unix = ( descriptor.unix === true ); + base = descriptor.base !== undefined ? descriptor.base : unix ? 2 : 10; + round = descriptor.round !== undefined ? descriptor.round : unix ? 1 : 2; + spacer = descriptor.spacer !== undefined ? descriptor.spacer : unix ? "" : " "; + num = Number( arg ); + neg = ( num < 0 ); + + // Flipping a negative number to determine the size + if ( neg ) { + num = -num; } - else if ( typeof define === "function" ) { - define( function () { - return filesize; - }); + + // Zero is now a special case because bytes divide by 1 + if ( num === 0 ) { + if ( unix ) { + result = "0"; + } + else { + result = "0" + spacer + "B"; + } } else { - global.filesize = filesize; + sizes = options[base][bits ? "bits" : "bytes"]; + + while ( i-- ) { + size = sizes[i][1]; + suffix = sizes[i][0]; + + if ( num >= size ) { + // Treating bytes as cardinal + if ( bite.test( suffix ) ) { + skip = true; + round = 0; + } + + result = ( num / size ).toFixed( round ); + + if ( !skip && unix ) { + if ( bits && bit.test( suffix ) ) { + suffix = suffix.toLowerCase(); + } + + suffix = suffix.charAt( 0 ); + z = right.exec( result ); + + if ( !bits && suffix === "k" ) { + suffix = "K"; + } + + if ( z !== null && z[1] !== undefined && zero.test( z[1] ) ) { + result = parseInt( result, radix ); + } + + result += spacer + suffix; + } + else if ( !unix ) { + result += spacer + suffix; + } + + break; + } + } } -})( this ); + + // Decorating a 'diff' + if ( neg ) { + result = "-" + result; + } + + return result; +} + +/** + * Size options + * + * @type {Object} + */ +var options = { + 2 : { + bits : [["B", 1], ["kb", 128], ["Mb", 131072], ["Gb", 134217728], ["Tb", 137438953472], ["Pb", 140737488355328]], + bytes : [["B", 1], ["kB", 1024], ["MB", 1048576], ["GB", 1073741824], ["TB", 1099511627776], ["PB", 1125899906842624]] + }, + 10 : { + bits : [["B", 1], ["kb", 125], ["Mb", 125000], ["Gb", 125000000], ["Tb", 125000000000], ["Pb", 125000000000000]], + bytes : [["B", 1], ["kB", 1000], ["MB", 1000000], ["GB", 1000000000], ["TB", 1000000000000], ["PB", 1000000000000000]] + } +}; + +// CommonJS, AMD, script tag +if ( typeof exports !== "undefined" ) { + module.exports = filesize; +} +else if ( typeof define === "function" ) { + define( function () { + return filesize; + } ); +} +else { + global.filesize = filesize; +} + +} )( this ); diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js index badfc18f3..ac23b473f 100644 --- a/src/UI/Shared/FormatHelpers.js +++ b/src/UI/Shared/FormatHelpers.js @@ -10,7 +10,7 @@ define( bytes: function (sourceSize) { var size = Number(sourceSize); - return Filesize(size, 1, false); + return Filesize(size, { base: 2, round: 1 }); }, dateHelper: function (sourceDate) { From 831c0a40b1059bf90198aef775955a80ab762391 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Dec 2013 11:00:51 -0800 Subject: [PATCH 09/30] Narrower episode title on calendar upcoming for longer episode numbers --- src/UI/Calendar/calendar.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index bad35c14f..50c84e0b2 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -108,7 +108,7 @@ .text-overflow; color : @linkColor; margin-top : 1px; - width : 150px; + width : 140px; display : inline-block; } } From 7ebc9e39808cf0405e356d6ba02a907bf610c5ea Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Dec 2013 15:19:26 -0800 Subject: [PATCH 10/30] Fixed broken test --- src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs index eeb6ef0c3..e2da6251c 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryRepositoryFixture.cs @@ -47,6 +47,8 @@ namespace NzbDrone.Core.Test.HistoryTests { var history = Builder .CreateListOfSize(5) + .All() + .With(c => c.EventType = HistoryEventType.Unknown) .Random(3) .With(c => c.EventType = HistoryEventType.Grabbed) .BuildListOfNew(); From 693dd8f6220513334b51880dacd2b27bd3dc0ee1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 3 Dec 2013 17:51:40 -0800 Subject: [PATCH 11/30] Stop double fecthing the collection on first load of series page --- src/UI/Series/Index/SeriesIndexLayout.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 6a78e0754..b2716cfd4 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -171,7 +171,6 @@ define( onShow: function () { this._showToolbar(); this._renderView(); - this._fetchCollection(); }, _fetchCollection: function () { From 87bda21b2805feada159b8e9074bca96310f15fa Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 4 Dec 2013 23:16:24 -0800 Subject: [PATCH 12/30] Fixed: Incorrect parsing as DVD for releases that contained 'pal' as part of another word --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 1 + src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 3eb98a5b9..b716883cf 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Two.and.a.Half.Men.S08E05.720p.HDTV.X264-DIMENSION", false)] [TestCase("Sonny.With.a.Chance.S02E15.mkv", false)] [TestCase(@"E:\Downloads\tv\The.Big.Bang.Theory.S01E01.720p.HDTV\ajifajjjeaeaeqwer_eppj.avi", false)] + [TestCase("Gem.Hunt.S01E08.Tourmaline.Nepal.720p.HDTV.x264-DHD", false)] public void should_parse_hdtv720p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.HDTV720p, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index bbfd7ed3b..d5a1d4e13 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SourceRegex = new Regex(@"(?BluRay)| (?WEB-DL|WEBDL|WEB\sDL|WEB\-DL|WebRip)| (?HDTV)| - (?BDRiP)|(?BRRip)|(?\bDVD\b|DVDRip|NTSC|PAL|xvidvd)| + (?BDRiP)|(?BRRip)|(?\b(?:DVD|DVDRip|NTSC|PAL|xvidvd)\b)| (?WS\sDSR|WS_DSR|WS\.DSR|DSR)|(?PDTV)|(?SDTV)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); From 54fcbc311fc3845de2f1d3eef7b1b9de31a3ab60 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 5 Dec 2013 09:31:26 -0800 Subject: [PATCH 13/30] Logging quality again when using it from folder during import --- .../MediaFiles/EpisodeImport/ImportDecisionMaker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 71ec33f0c..bd81e797d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -61,6 +61,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (quality != null && quality > parsedEpisode.Quality) { + _logger.Trace("Using quality from folder: {0}", quality); parsedEpisode.Quality = quality; } From 0de25988a55f10fda7c4ad640d60d52821fea78f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 5 Dec 2013 10:20:24 -0800 Subject: [PATCH 14/30] Added logging when folder quality is parsed --- src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index b3bcad533..eba7b84ac 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -100,6 +100,7 @@ namespace NzbDrone.Core.MediaFiles var cleanedUpName = GetCleanedUpFolderName(subfolderInfo.Name); var series = _parsingService.GetSeries(cleanedUpName); var quality = QualityParser.ParseQuality(cleanedUpName); + _logger.Trace("{0} folder quality: {1}", cleanedUpName, quality); if (series == null) { From c03f01172e246d80baadb0e4c39e7443c0179f8e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 5 Dec 2013 21:59:47 -0800 Subject: [PATCH 15/30] Use audio and general stream runtimes when video runtime is zero Fixed: Getting runtime from files should be more reliable --- .../MediaInfo/VideoFileInfoReader.cs | 29 +++++++++++++++---- .../{Controller.js => ModalController.js} | 0 2 files changed, 24 insertions(+), 5 deletions(-) rename src/UI/Shared/Modal/{Controller.js => ModalController.js} (100%) diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 9ae5421af..85ccdb263 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -46,7 +46,9 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo int height; int videoBitRate; int audioBitRate; - int runTime; + int audioRuntime; + int videoRuntime; + int generalRuntime; int streamCount; int audioChannels; decimal videoFrameRate; @@ -56,9 +58,13 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height); Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate); - Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out runTime); Decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), out videoFrameRate); + //Runtime + Int32.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime); + Int32.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime); + Int32.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) @@ -87,10 +93,9 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo VideoBitrate = videoBitRate, Height = height, Width = width, - AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), AudioBitrate = audioBitRate, - RunTime = TimeSpan.FromMilliseconds(runTime), + RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime), AudioStreamCount = streamCount, AudioChannels = audioChannels, AudioProfile = audioProfile.Trim(), @@ -100,7 +105,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo ScanType = scanType }; - mediaInfo.Close(); return mediaInfoModel; } } @@ -135,5 +139,20 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return info.RunTime; } + + private TimeSpan GetBestRuntime(int audio, int video, int general) + { + if (video == 0) + { + if (audio == 0) + { + return TimeSpan.FromMilliseconds(general); + } + + return TimeSpan.FromMilliseconds(audio); + } + + return TimeSpan.FromMilliseconds(video); + } } } diff --git a/src/UI/Shared/Modal/Controller.js b/src/UI/Shared/Modal/ModalController.js similarity index 100% rename from src/UI/Shared/Modal/Controller.js rename to src/UI/Shared/Modal/ModalController.js From 39607e240f7c16273a3ca8909f72d8ee82b1f8eb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 6 Dec 2013 14:29:50 -0800 Subject: [PATCH 16/30] ModalController --- src/UI/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/app.js b/src/UI/app.js index 5bc24b4da..51a0ee0e4 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -208,7 +208,7 @@ define( 'AppLayout', 'Series/SeriesController', 'Router', - 'Shared/Modal/Controller', + 'Shared/Modal/ModalController', 'System/StatusModel', 'Instrumentation/StringFormat', 'LifeCycle' From dcd1b55d1f296061f37cbf5b533e312a3be8a274 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 6 Dec 2013 17:34:40 -0800 Subject: [PATCH 17/30] Trigger change on input after adding token Fixed: Media Management settings not detecting changes to the UI so settings weren't saved --- src/UI/Settings/MediaManagement/Naming/NamingView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index a9ad9e7e9..2ae43fbef 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -88,6 +88,7 @@ define( } input.val(input.val() + token); + input.change(); this.ui.namingTokenHelper.removeClass('open'); input.focus(); From c122a94bc0b345c78d94d04e7d9ca13e953b140a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 6 Dec 2013 18:55:48 -0800 Subject: [PATCH 18/30] Fixed: Opening firewall ports when system has more than one network adapter --- src/NzbDrone.Host/AccessControl/FirewallAdapter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs index 3aa624d10..a2d7b207d 100644 --- a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs @@ -80,9 +80,10 @@ namespace NzbDrone.Host.AccessControl var netFwMgrType = Type.GetTypeFromProgID("HNetCfg.FwMgr", false); var mgr = (INetFwMgr)Activator.CreateInstance(netFwMgrType); - var ports = mgr.LocalPolicy.CurrentProfile.GloballyOpenPorts; - ports.Add(port); + //Adds ports for both the current profile and the 'standard' (private) profile + mgr.LocalPolicy.GetProfileByType(NET_FW_PROFILE_TYPE_.NET_FW_PROFILE_CURRENT).GloballyOpenPorts.Add(port); + mgr.LocalPolicy.GetProfileByType(NET_FW_PROFILE_TYPE_.NET_FW_PROFILE_STANDARD).GloballyOpenPorts.Add(port); } catch (Exception ex) { From a7e210bfb3a430bb54b1af5f8a8dbba6d9506af1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 20:57:45 -0800 Subject: [PATCH 19/30] Use Int64 for PushBullet device ID Fixed: Support for large Push Bullet device IDs --- .../Notifications/PushBullet/PushBulletSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs index 3e1f75ca1..1525f6413 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletSettings.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Notifications.PushBullet public String ApiKey { get; set; } [FieldDefinition(1, Label = "Device ID")] - public Int32 DeviceId { get; set; } + public Int64 DeviceId { get; set; } public bool IsValid { From f95f7c23203c1c87f750484f7b3beda3edffd8a8 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 20:59:04 -0800 Subject: [PATCH 20/30] Actual fix for multiple network interfaces --- .../AccessControl/FirewallAdapter.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs index a2d7b207d..c252e3e9e 100644 --- a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs @@ -13,9 +13,11 @@ namespace NzbDrone.Host.AccessControl public class FirewallAdapter : IFirewallAdapter { + private const NET_FW_PROFILE_TYPE_ FIREWALL_PROFILE = NET_FW_PROFILE_TYPE_.NET_FW_PROFILE_STANDARD; + private readonly IConfigFileProvider _configFileProvider; private readonly Logger _logger; - + public FirewallAdapter(IConfigFileProvider configFileProvider, Logger logger) { _configFileProvider = configFileProvider; @@ -47,11 +49,7 @@ namespace NzbDrone.Host.AccessControl var netFwMgrType = Type.GetTypeFromProgID("HNetCfg.FwMgr", false); var mgr = (INetFwMgr)Activator.CreateInstance(netFwMgrType); - - if (!mgr.LocalPolicy.CurrentProfile.FirewallEnabled) - return false; - - var ports = mgr.LocalPolicy.CurrentProfile.GloballyOpenPorts; + var ports = mgr.LocalPolicy.GetProfileByType(FIREWALL_PROFILE).GloballyOpenPorts; foreach (INetFwOpenPort p in ports) { @@ -81,9 +79,8 @@ namespace NzbDrone.Host.AccessControl var netFwMgrType = Type.GetTypeFromProgID("HNetCfg.FwMgr", false); var mgr = (INetFwMgr)Activator.CreateInstance(netFwMgrType); - //Adds ports for both the current profile and the 'standard' (private) profile - mgr.LocalPolicy.GetProfileByType(NET_FW_PROFILE_TYPE_.NET_FW_PROFILE_CURRENT).GloballyOpenPorts.Add(port); - mgr.LocalPolicy.GetProfileByType(NET_FW_PROFILE_TYPE_.NET_FW_PROFILE_STANDARD).GloballyOpenPorts.Add(port); + //Open the port for the standard profile, should help when the user has multiple network adapters + mgr.LocalPolicy.GetProfileByType(FIREWALL_PROFILE).GloballyOpenPorts.Add(port); } catch (Exception ex) { @@ -99,7 +96,7 @@ namespace NzbDrone.Host.AccessControl { var netFwMgrType = Type.GetTypeFromProgID("HNetCfg.FwMgr", false); var mgr = (INetFwMgr)Activator.CreateInstance(netFwMgrType); - return mgr.LocalPolicy.CurrentProfile.FirewallEnabled; + return mgr.LocalPolicy.GetProfileByType(FIREWALL_PROFILE).FirewallEnabled; } catch (Exception ex) { From d5bad8c6ef644dae4a188d663653fc4a26f7ad64 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 20:59:46 -0800 Subject: [PATCH 21/30] long not int --- src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index fe1567f3e..93d17bb62 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -7,14 +7,14 @@ namespace NzbDrone.Core.Notifications.PushBullet { public interface IPushBulletProxy { - void SendNotification(string title, string message, string apiKey, int deviceId); + void SendNotification(string title, string message, string apiKey, long deviceId); } public class PushBulletProxy : IPushBulletProxy, IExecute { private const string URL = "https://api.pushbullet.com/api/pushes"; - public void SendNotification(string title, string message, string apiKey, int deviceId) + public void SendNotification(string title, string message, string apiKey, long deviceId) { var client = new RestClient(URL); var request = new RestRequest(Method.POST); From 9dcdd06b6cc07c92b97cb04ad0770ee2e859acd9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 22:23:47 -0800 Subject: [PATCH 22/30] Default category is now empty for nzbget --- src/NzbDrone.Core/Configuration/ConfigService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 373385aa2..4a4bda1f3 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -210,7 +210,7 @@ namespace NzbDrone.Core.Configuration public String NzbgetTvCategory { - get { return GetValue("NzbgetTvCategory", "nzbget"); } + get { return GetValue("NzbgetTvCategory", ""); } set { SetValue("NzbgetTvCategory", value); } } From f76c4700a6124d537ac20181c7dc736d5f5d38ac Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 22:25:11 -0800 Subject: [PATCH 23/30] New: Release Group can now be used in rename patterns --- .../IndexerTests/BasicRssParserFixture.cs | 13 -------- .../ImportApprovedEpisodesFixture.cs | 6 +++- .../OrganizerTests/GetNewFilenameFixture.cs | 11 ++++++- .../ParserTests/ParserFixture.cs | 14 ++++++++ .../032_set_default_release_group.cs | 14 ++++++++ src/NzbDrone.Core/History/HistoryService.cs | 2 +- src/NzbDrone.Core/Indexers/RssParserBase.cs | 20 ----------- src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/FileNameBuilder.cs | 2 ++ .../Organizer/FilenameSampleService.cs | 9 +++-- .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 1 - src/NzbDrone.Core/Parser/Parser.cs | 33 +++++++++++++++++++ .../Naming/NamingViewTemplate.html | 2 ++ .../Partials/ReleaseGroupNamingPartial.html | 1 + 17 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs create mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.html diff --git a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs index 2eca3586b..30bfeadb0 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/BasicRssParserFixture.cs @@ -7,19 +7,6 @@ namespace NzbDrone.Core.Test.IndexerTests { public class BasicRssParserFixture : CoreTest { - - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] - [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] - [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] - [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] - [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] - [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] - public void parse_releaseGroup(string title, string expected) - { - RssParserBase.ParseReleaseGroup(title).Should().Be(expected); - } - - [TestCase("5.64 GB", 6055903887)] [TestCase("5.54 GiB", 5948529705)] [TestCase("398.62 MiB", 417983365)] diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 30b5f804a..7946b53aa 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -47,7 +47,11 @@ namespace NzbDrone.Core.Test.MediaFiles Series = series, Episodes = new List {episode}, Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(), - Quality = new QualityModel(Quality.Bluray720p) + Quality = new QualityModel(Quality.Bluray720p), + ParsedEpisodeInfo = new ParsedEpisodeInfo + { + ReleaseGroup = "DRONE" + } })); } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index cfad01e1d..749dff288 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .With(e => e.EpisodeNumber = 7) .Build(); - _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }; + _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "DRONE" }; } private void GivenProper() @@ -344,5 +344,14 @@ namespace NzbDrone.Core.Test.OrganizerTests Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) .Should().Be(title); } + + [Test] + public void should_should_replace_release_group() + { + _namingConfig.StandardEpisodeFormat = "{Release Group}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be(_episodeFile.ReleaseGroup); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 3d25c7086..f797c9f27 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -452,5 +452,19 @@ namespace NzbDrone.Core.Test.ParserTests Parser.Parser.ParseTitle(title).Should().BeNull(); ExceptionVerification.IgnoreWarns(); } + + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", "LOL")] + [TestCase("Castle 2009 S01E14 English HDTV XviD LOL", "LOL")] + [TestCase("Acropolis Now S05 EXTRAS DVDRip XviD RUNNER", "RUNNER")] + [TestCase("Punky.Brewster.S01.EXTRAS.DVDRip.XviD-RUNNER", "RUNNER")] + [TestCase("2020.NZ.2011.12.02.PDTV.XviD-C4TV", "C4TV")] + [TestCase("The.Office.S03E115.DVDRip.XviD-OSiTV", "OSiTV")] + [TestCase("The Office - S01E01 - Pilot [HTDV-480p]", "DRONE")] + [TestCase("The Office - S01E01 - Pilot [HTDV-720p]", "DRONE")] + [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] + public void parse_releaseGroup(string title, string expected) + { + Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs b/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs new file mode 100644 index 000000000..5ecc4e2c0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/032_set_default_release_group.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(32)] + public class set_default_release_group : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.Sql("UPDATE EpisodeFiles SET ReleaseGroup = 'DRONE' WHERE ReleaseGroup IS NULL"); + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 9fad974e0..636d092d0 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -101,7 +101,7 @@ namespace NzbDrone.Core.History history.Data.Add("Indexer", message.Episode.Release.Indexer); history.Data.Add("NzbInfoUrl", message.Episode.Release.InfoUrl); - history.Data.Add("ReleaseGroup", message.Episode.Release.ReleaseGroup); + history.Data.Add("ReleaseGroup", message.Episode.ParsedEpisodeInfo.ReleaseGroup); history.Data.Add("Age", message.Episode.Release.Age.ToString()); if (!String.IsNullOrWhiteSpace(message.DownloadClientId)) diff --git a/src/NzbDrone.Core/Indexers/RssParserBase.cs b/src/NzbDrone.Core/Indexers/RssParserBase.cs index 1065631aa..29ad27ee2 100644 --- a/src/NzbDrone.Core/Indexers/RssParserBase.cs +++ b/src/NzbDrone.Core/Indexers/RssParserBase.cs @@ -70,7 +70,6 @@ namespace NzbDrone.Core.Indexers reportInfo.Title = title; reportInfo.PublishDate = item.PublishDate(); - reportInfo.ReleaseGroup = ParseReleaseGroup(title); reportInfo.DownloadUrl = GetNzbUrl(item); reportInfo.InfoUrl = GetNzbInfoUrl(item); @@ -114,25 +113,6 @@ namespace NzbDrone.Core.Indexers return currentResult; } - public static string ParseReleaseGroup(string title) - { - title = title.Trim(); - var index = title.LastIndexOf('-'); - - if (index < 0) - index = title.LastIndexOf(' '); - - if (index < 0) - return String.Empty; - - var group = title.Substring(index + 1); - - if (@group.Length == title.Length) - return String.Empty; - - return @group.Trim('-', ' ', '[', ']'); - } - private static readonly Regex ReportSizeRegex = new Regex(@"(?\d+\.\d{1,2}|\d+\,\d+\.\d{1,2}|\d+)\W?(?GB|MB|GiB|MiB)", RegexOptions.IgnoreCase | RegexOptions.Compiled); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 943bc731a..7fac2d1ae 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Core.MediaFiles public long Size { get; set; } public DateTime DateAdded { get; set; } public string SceneName { get; set; } + public string ReleaseGroup { get; set; } public QualityModel Quality { get; set; } public LazyList Episodes { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 0173056f9..92b0bd059 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -68,6 +68,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.Quality = localEpisode.Quality; episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; + episodeFile.ReleaseGroup = localEpisode.ParsedEpisodeInfo.ReleaseGroup; if (newDownload) { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f3dddff70..98eafe518 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -187,6 +187,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 9032a2196..004ff68ac 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -89,6 +89,8 @@ namespace NzbDrone.Core.Organizer {"{Series Title}", series.Title} }; + tokenValues.Add("{Release Group}", episodeFile.ReleaseGroup); + if (series.SeriesType == SeriesTypes.Daily) { pattern = namingConfig.DailyEpisodeFormat; diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs index 2ea499b9b..668a20966 100644 --- a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -65,19 +65,22 @@ namespace NzbDrone.Core.Organizer _singleEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), - Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv" + Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv", + ReleaseGroup = "RlsGrp" }; _multiEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), - Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv" + Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv", + ReleaseGroup = "RlsGrp" }; _dailyEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), - Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv" + Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv", + ReleaseGroup = "RlsGrp" }; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index d4bbefaac..7ae94f647 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Parser.Model public String AirDate { get; set; } public Language Language { get; set; } public bool FullSeason { get; set; } + public string ReleaseGroup { get; set; } public ParsedEpisodeInfo() { diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index b5ae81ad8..c920bd8aa 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -28,7 +28,6 @@ namespace NzbDrone.Core.Parser.Model } } - public string ReleaseGroup { get; set; } public int TvRageId { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 119711b1d..de2b06ca7 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -162,6 +162,9 @@ namespace NzbDrone.Core.Parser result.Quality = QualityParser.ParseQuality(title); Logger.Trace("Quality parsed: {0}", result.Quality); + result.ReleaseGroup = ParseReleaseGroup(title); + Logger.Trace("Release Group parsed: {0}", result.ReleaseGroup); + return result; } } @@ -214,6 +217,36 @@ namespace NzbDrone.Core.Parser return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); } + public static string ParseReleaseGroup(string title) + { + const string defaultReleaseGroup = "DRONE"; + + title = title.Trim(); + var index = title.LastIndexOf('-'); + + if (index < 0) + index = title.LastIndexOf(' '); + + if (index < 0) + return defaultReleaseGroup; + + var group = title.Substring(index + 1); + + if (group.Length == title.Length) + return String.Empty; + + group = group.Trim('-', ' ', '[', ']'); + + if (group.ToLower() == "480p" || + group.ToLower() == "720p" || + group.ToLower() == "1080p") + { + return defaultReleaseGroup; + } + + return group; + } + private static SeriesTitleInfo GetSeriesTitleInfo(string title) { var seriesTitleInfo = new SeriesTitleInfo(); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index a8eb890d2..e7b2e4f61 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -41,6 +41,7 @@ {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} {{> QualityTitleNamingPartial}} + {{> ReleaseGroupNamingPartial}} {{> SeparatorNamingPartial}} @@ -69,6 +70,7 @@ {{> EpisodeNamingPartial}} {{> EpisodeTitleNamingPartial}} {{> QualityTitleNamingPartial}} + {{> ReleaseGroupNamingPartial}} {{> SeparatorNamingPartial}} diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.html new file mode 100644 index 000000000..137d9fce3 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.html @@ -0,0 +1 @@ +
  • Release Group
  • \ No newline at end of file From a9ece10144afb0d0980c8a333ba48d6695dd234f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 8 Dec 2013 21:25:27 -0800 Subject: [PATCH 24/30] New: Mass series editor --- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + src/NzbDrone.Api/Series/SeriesEditorModule.cs | 30 ++++ src/NzbDrone.Core/Tv/SeriesService.cs | 17 ++ src/UI/AppLayout.js | 12 +- src/UI/Cells/SeasonFolderCell.js | 16 ++ src/UI/Cells/cells.less | 4 + src/UI/Content/Backgrid/selectall.less | 7 +- src/UI/Content/Overrides/messenger.less | 5 + src/UI/Content/overrides.less | 1 + src/UI/Content/theme.less | 23 ++- src/UI/Controller.js | 12 +- src/UI/Router.js | 1 + .../Series/Editor/SeriesEditorFooterView.js | 150 +++++++++++++++++ .../SeriesEditorFooterViewTemplate.html | 50 ++++++ src/UI/Series/Editor/SeriesEditorLayout.js | 153 ++++++++++++++++++ .../Editor/SeriesEditorLayoutTemplate.html | 7 + src/UI/Series/Index/SeriesIndexLayout.js | 5 + src/UI/Series/SeriesCollection.js | 27 +++- src/UI/Series/series.less | 13 ++ .../ControlPanel/ControlPanelController.js | 24 +++ .../Shared/ControlPanel/ControlPanelRegion.js | 35 ++++ src/UI/Shared/Modal/ModalController.js | 10 +- src/UI/Shared/Modal/ModalRegion.js | 6 +- src/UI/app.js | 4 +- src/UI/index.html | 5 +- src/UI/vent.js | 22 +-- 26 files changed, 606 insertions(+), 34 deletions(-) create mode 100644 src/NzbDrone.Api/Series/SeriesEditorModule.cs create mode 100644 src/UI/Cells/SeasonFolderCell.js create mode 100644 src/UI/Content/Overrides/messenger.less create mode 100644 src/UI/Series/Editor/SeriesEditorFooterView.js create mode 100644 src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html create mode 100644 src/UI/Series/Editor/SeriesEditorLayout.js create mode 100644 src/UI/Series/Editor/SeriesEditorLayoutTemplate.html create mode 100644 src/UI/Shared/ControlPanel/ControlPanelController.js create mode 100644 src/UI/Shared/ControlPanel/ControlPanelRegion.js diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 9569bf6f3..00f5f96e3 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -152,6 +152,7 @@ + diff --git a/src/NzbDrone.Api/Series/SeriesEditorModule.cs b/src/NzbDrone.Api/Series/SeriesEditorModule.cs new file mode 100644 index 000000000..f64b4098c --- /dev/null +++ b/src/NzbDrone.Api/Series/SeriesEditorModule.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Remoting.Messaging; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Api.Mapping; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Series +{ + public class SeriesEditorModule : NzbDroneApiModule + { + private readonly ISeriesService _seriesService; + + public SeriesEditorModule(ISeriesService seriesService) + : base("/series/editor") + { + _seriesService = seriesService; + Put["/"] = series => SaveAll(); + } + + private Response SaveAll() + { + //Read from request + var series = Request.Body.FromJson>().InjectTo>(); + + return _seriesService.UpdateSeries(series).InjectTo>().AsResponse(); + } + } +} diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index c3767eaf6..18b67a732 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Core.Tv void DeleteSeries(int seriesId, bool deleteFiles); List GetAllSeries(); Series UpdateSeries(Series series); + List UpdateSeries(List series); bool SeriesPathExists(string folder); } @@ -138,6 +139,22 @@ namespace NzbDrone.Core.Tv return _seriesRepository.Update(series); } + public List UpdateSeries(List series) + { + foreach (var s in series) + { + if (!String.IsNullOrWhiteSpace(s.RootFolderPath)) + { + var folderName = new DirectoryInfo(s.Path).Name; + s.Path = Path.Combine(s.RootFolderPath, folderName); + } + } + + _seriesRepository.UpdateMany(series); + + return series; + } + public bool SeriesPathExists(string folder) { return _seriesRepository.SeriesPathExists(folder); diff --git a/src/UI/AppLayout.js b/src/UI/AppLayout.js index b063516f3..a0988a243 100644 --- a/src/UI/AppLayout.js +++ b/src/UI/AppLayout.js @@ -1,20 +1,22 @@ define( [ 'marionette', - 'Shared/Modal/ModalRegion' - ], function (Marionette, ModalRegion) { + 'Shared/Modal/ModalRegion', + 'Shared/ControlPanel/ControlPanelRegion' + ], function (Marionette, ModalRegion, ControlPanelRegion) { 'use strict'; var Layout = Marionette.Layout.extend({ regions: { - navbarRegion: '#nav-region', - mainRegion : '#main-region' + navbarRegion : '#nav-region', + mainRegion : '#main-region' }, initialize: function () { this.addRegions({ - modalRegion: ModalRegion + modalRegion : ModalRegion, + controlPanelRegion: ControlPanelRegion }); } }); diff --git a/src/UI/Cells/SeasonFolderCell.js b/src/UI/Cells/SeasonFolderCell.js new file mode 100644 index 000000000..5c9cc6d1b --- /dev/null +++ b/src/UI/Cells/SeasonFolderCell.js @@ -0,0 +1,16 @@ +'use strict'; +define( + [ + 'backgrid' + ], function (Backgrid) { + return Backgrid.Cell.extend({ + + className : 'season-folder-cell', + + render: function () { + var seasonFolder = this.model.get('seasonFolder'); + this.$el.html(seasonFolder.toString()); + return this; + } + }); + }); diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 6efd806ad..b2ba4d140 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -117,4 +117,8 @@ td.delete-episode-file-cell { i { .clickable(); } +} + +.series-status-cell { + width: 16px; } \ No newline at end of file diff --git a/src/UI/Content/Backgrid/selectall.less b/src/UI/Content/Backgrid/selectall.less index 86f6e0107..322853304 100644 --- a/src/UI/Content/Backgrid/selectall.less +++ b/src/UI/Content/Backgrid/selectall.less @@ -6,8 +6,7 @@ Licensed under the MIT @license. */ -.backgrid { - .select-row-cell, .select-all-header-cell { - text-align: center; - } +.select-row-cell, .select-all-header-cell { + text-align: center; + width: 16px; } \ No newline at end of file diff --git a/src/UI/Content/Overrides/messenger.less b/src/UI/Content/Overrides/messenger.less new file mode 100644 index 000000000..dec17fe5a --- /dev/null +++ b/src/UI/Content/Overrides/messenger.less @@ -0,0 +1,5 @@ +body.control-panel-visible { + ul.messenger.messenger-fixed.messenger-on-bottom { + bottom: 95px; + } +} \ No newline at end of file diff --git a/src/UI/Content/overrides.less b/src/UI/Content/overrides.less index 040b76e92..1c0ec7e73 100644 --- a/src/UI/Content/overrides.less +++ b/src/UI/Content/overrides.less @@ -2,3 +2,4 @@ @import "Overrides/browser"; @import "Overrides/bootstrap.toggle-switch"; @import "Overrides/fullcalendar"; +@import "Overrides/messenger"; diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index eb99df003..8bf71358e 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -69,6 +69,12 @@ color : white; } +.control-panel-visible { + #scroll-up { + bottom: 100px; + } +} + .label-large { padding : 4px 6px; font-size : 16px; @@ -104,7 +110,7 @@ body { } } -footer { +.footer { font-size : 13px; font-weight : lighter; padding-top : 0px; @@ -192,4 +198,19 @@ footer { .file-path { .mono-space(); +} + +.control-panel { + .card(#333333); + + color: #f5f5f5; + background-color: #333333; + margin: 0px; + margin-bottom: -100px; + position: fixed; + left: 0; + bottom: 0; + width: 100%; + height: 55px; + opacity: 0; } \ No newline at end of file diff --git a/src/UI/Controller.js b/src/UI/Controller.js index bd5be1819..db0b13e2b 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -12,7 +12,8 @@ define( 'Release/ReleaseLayout', 'System/SystemLayout', 'SeasonPass/SeasonPassLayout', - 'System/Update/UpdateLayout' + 'System/Update/UpdateLayout', + 'Series/Editor/SeriesEditorLayout' ], function (NzbDroneController, AppLayout, Marionette, @@ -24,7 +25,8 @@ define( ReleaseLayout, SystemLayout, SeasonPassLayout, - UpdateLayout) { + UpdateLayout, + SeriesEditorLayout) { return NzbDroneController.extend({ addSeries: function (action) { @@ -72,7 +74,13 @@ define( update: function () { this.setTitle('Updates'); this.showMainRegion(new UpdateLayout()); + }, + + seriesEditor: function () { + this.setTitle('Series Editor'); + this.showMainRegion(new SeriesEditorLayout()); } + }); }); diff --git a/src/UI/Router.js b/src/UI/Router.js index d8532756f..f2927787a 100644 --- a/src/UI/Router.js +++ b/src/UI/Router.js @@ -21,6 +21,7 @@ define( 'system' : 'system', 'system/:action' : 'system', 'seasonpass' : 'seasonPass', + 'serieseditor' : 'seriesEditor', ':whatever' : 'showNotFound' } }); diff --git a/src/UI/Series/Editor/SeriesEditorFooterView.js b/src/UI/Series/Editor/SeriesEditorFooterView.js new file mode 100644 index 000000000..27dca0d35 --- /dev/null +++ b/src/UI/Series/Editor/SeriesEditorFooterView.js @@ -0,0 +1,150 @@ +'use strict'; +define( + [ + 'underscore', + 'marionette', + 'backgrid', + 'vent', + 'Series/SeriesCollection', + 'Quality/QualityProfileCollection', + 'AddSeries/RootFolders/Collection', + 'Shared/Toolbar/ToolbarLayout', + 'AddSeries/RootFolders/Layout', + 'Config' + ], function (_, + Marionette, + Backgrid, + vent, + SeriesCollection, + QualityProfiles, + RootFolders, + ToolbarLayout, + RootFolderLayout, + Config) { + return Marionette.ItemView.extend({ + template: 'Series/Editor/SeriesEditorFooterViewTemplate', + + ui: { + monitored : '.x-monitored', + qualityProfile: '.x-quality-profiles', + seasonFolder : '.x-season-folder', + rootFolder : '.x-root-folder', + selectedCount : '.x-selected-count', + saveButton : '.x-save', + container : '.series-editor-footer' + }, + + events: { + 'click .x-save' : '_updateAndSave', + 'change .x-root-folder': '_rootFolderChanged' + }, + + templateHelpers: function () { + return { + qualityProfiles: QualityProfiles, + rootFolders : RootFolders.toJSON() + }; + }, + + initialize: function (options) { + RootFolders.fetch().done(function () { + RootFolders.synced = true; + }); + + this.editorGrid = options.editorGrid; + this.listenTo(SeriesCollection, 'backgrid:selected', this._updateInfo); + this.listenTo(RootFolders, 'all', this.render); + }, + + onRender: function () { + this._updateInfo(); + }, + + _updateAndSave: function () { + var selected = this.editorGrid.getSelectedModels(); + + var monitored = this.ui.monitored.val(); + var profile = this.ui.qualityProfile.val(); + var seasonFolder = this.ui.seasonFolder.val(); + var rootFolder = this.ui.rootFolder.val(); + + _.each(selected, function (model) { + if (monitored === 'true') { + model.set('monitored', true); + } + + else if (monitored === 'false') { + model.set('monitored', false); + } + + if (profile !== 'noChange') { + model.set('qualityProfileId', parseInt(profile, 10)); + } + + if (seasonFolder === 'true') { + model.set('seasonFolder', true); + } + + else if (seasonFolder === 'false') { + model.set('seasonFolder', false); + } + + if (rootFolder !== 'noChange') { + var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); + + model.set('rootFolderPath', rootFolderPath.get('path')); + } + + model.trigger('backgrid:select', model, false); + }); + + this.ui.monitored.val('noChange'); + this.ui.qualityProfile.val('noChange'); + this.ui.seasonFolder.val('noChange'); + this.ui.rootFolder.val('noChange'); + + SeriesCollection.save(); + }, + + _updateInfo: function () { + var selected = this.editorGrid.getSelectedModels(); + var selectedCount = selected.length; + + this.ui.selectedCount.html('{0} series selected'.format(selectedCount)); + + if (selectedCount === 0) { + this.ui.monitored.attr('disabled', ''); + this.ui.qualityProfile.attr('disabled', ''); + this.ui.seasonFolder.attr('disabled', ''); + this.ui.rootFolder.attr('disabled', ''); + this.ui.saveButton.attr('disabled', ''); + } + + else { + this.ui.monitored.removeAttr('disabled', ''); + this.ui.qualityProfile.removeAttr('disabled', ''); + this.ui.seasonFolder.removeAttr('disabled', ''); + this.ui.rootFolder.removeAttr('disabled', ''); + this.ui.saveButton.removeAttr('disabled', ''); + } + }, + + _rootFolderChanged: function () { + var rootFolderValue = this.ui.rootFolder.val(); + if (rootFolderValue === 'addNew') { + var rootFolderLayout = new RootFolderLayout(); + this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); + vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); + } + else { + Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); + } + }, + + _setRootFolder: function (options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.ui.rootFolder.val(options.model.id); + this._rootFolderChanged(); + } + }); + }); diff --git a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html new file mode 100644 index 000000000..ad5b16e14 --- /dev/null +++ b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.html @@ -0,0 +1,50 @@ + \ No newline at end of file diff --git a/src/UI/Series/Editor/SeriesEditorLayout.js b/src/UI/Series/Editor/SeriesEditorLayout.js new file mode 100644 index 000000000..c3e134a84 --- /dev/null +++ b/src/UI/Series/Editor/SeriesEditorLayout.js @@ -0,0 +1,153 @@ +'use strict'; +define( + [ + 'vent', + 'marionette', + 'backgrid', + 'Series/Index/EmptyView', + 'Series/SeriesCollection', + 'Cells/SeriesTitleCell', + 'Cells/QualityProfileCell', + 'Cells/SeriesStatusCell', + 'Cells/SeasonFolderCell', + 'Shared/Toolbar/ToolbarLayout', + 'Series/Editor/SeriesEditorFooterView' + ], function (vent, + Marionette, + Backgrid, + EmptyView, + SeriesCollection, + SeriesTitleCell, + QualityProfileCell, + SeriesStatusCell, + SeasonFolderCell, + ToolbarLayout, + FooterView) { + return Marionette.Layout.extend({ + template: 'Series/Editor/SeriesEditorLayoutTemplate', + + regions: { + seriesRegion: '#x-series-editor', + toolbar : '#x-toolbar' + }, + + ui: { + monitored : '.x-monitored', + qualityProfiles: '.x-quality-profiles', + rootFolder : '.x-root-folder', + selectedCount : '.x-selected-count' + }, + + events: { + 'click .x-save' : '_updateAndSave', + 'change .x-root-folder': '_rootFolderChanged' + }, + + columns: + [ + { + name : '', + cell : 'select-row', + headerCell: 'select-all', + sortable : false + }, + { + name : 'statusWeight', + label : '', + cell : SeriesStatusCell + }, + { + name : 'title', + label : 'Title', + cell : SeriesTitleCell, + cellValue: 'this' + }, + { + name : 'qualityProfileId', + label: 'Quality', + cell : QualityProfileCell + }, + { + name : 'monitored', + label: 'Season Folder', + cell : SeasonFolderCell + }, + { + name : 'path', + label: 'Path', + cell : 'string' + } + ], + + leftSideButtons: { + type : 'default', + storeState: false, + items : + [ + { + title : 'Season Pass', + icon : 'icon-bookmark', + route : 'seasonpass' + }, + { + title : 'Update Library', + icon : 'icon-refresh', + command : 'refreshseries', + successMessage: 'Library was updated!', + errorMessage : 'Library update failed!' + } + ] + }, + + initialize: function () { + this.listenTo(SeriesCollection, 'sync', this._showTable); + this.listenTo(SeriesCollection, 'remove', this._showTable); + }, + + onRender: function () { + this._showToolbar(); + this._showTable(); + + this._fetchCollection(); + }, + + onClose: function () { + vent.trigger(vent.Commands.CloseControlPanelCommand); + }, + + _showTable: function () { + if (SeriesCollection.length === 0) { + this.seriesRegion.show(new EmptyView()); + this.toolbar.close(); + return; + } + + this.editorGrid = new Backgrid.Grid({ + collection: SeriesCollection, + columns : this.columns, + className : 'table table-hover' + }); + + this.seriesRegion.show(this.editorGrid); + this._showFooter(); + }, + + _fetchCollection: function () { + SeriesCollection.fetch(); + }, + + _showToolbar: function () { + this.toolbar.show(new ToolbarLayout({ + left : + [ + this.leftSideButtons + ], + context: this + })); + }, + + _showFooter: function () { + vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ editorGrid: this.editorGrid })); + } + }); + }); diff --git a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html new file mode 100644 index 000000000..470e663eb --- /dev/null +++ b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.html @@ -0,0 +1,7 @@ +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index b2716cfd4..4d794cf07 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -109,6 +109,11 @@ define( icon : 'icon-bookmark', route : 'seasonpass' }, + { + title : 'Series Editor', + icon : 'icon-nd-edit', + route : 'serieseditor' + }, { title : 'RSS Sync', icon : 'icon-rss', diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index b7458ef52..110044110 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -1,10 +1,11 @@ 'use strict'; define( [ + 'underscore', 'backbone', 'Series/SeriesModel', 'api!series' - ], function (Backbone, SeriesModel, SeriesData) { + ], function (_, Backbone, SeriesModel, SeriesData) { var Collection = Backbone.Collection.extend({ url : window.NzbDrone.ApiRoot + '/series', model: SeriesModel, @@ -16,6 +17,30 @@ define( state: { sortKey: 'title', order : -1 + }, + + save: function () { + var self = this; + + var proxy = _.extend( new Backbone.Model(), + { + id: '', + + url: self.url + '/editor', + + toJSON: function() + { + return self.filter(function (model) { + return model.hasChanged(); + }); + } + }); + + this.listenTo(proxy, 'sync', function (proxyModel, models) { + this.add(models, { merge: true }); + }); + + return proxy.save(); } }); diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index fb116379e..098c2c091 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -288,4 +288,17 @@ font-size : 24px; margin-top : 3px; } +} + +//Editor + +.series-editor-footer { + width: 1100px; + color: #f5f5f5; + margin-left: auto; + margin-right: auto; + + .selected-count { + margin-right: 10px; + } } \ No newline at end of file diff --git a/src/UI/Shared/ControlPanel/ControlPanelController.js b/src/UI/Shared/ControlPanel/ControlPanelController.js new file mode 100644 index 000000000..4e2a1100c --- /dev/null +++ b/src/UI/Shared/ControlPanel/ControlPanelController.js @@ -0,0 +1,24 @@ +'use strict'; +define( + [ + 'vent', + 'AppLayout', + 'marionette' + ], function (vent, AppLayout, Marionette) { + + return Marionette.AppRouter.extend({ + + initialize: function () { + vent.on(vent.Commands.OpenControlPanelCommand, this._openControlPanel, this); + vent.on(vent.Commands.CloseControlPanelCommand, this._closeControlPanel, this); + }, + + _openControlPanel: function (view) { + AppLayout.controlPanelRegion.show(view); + }, + + _closeControlPanel: function () { + AppLayout.controlPanelRegion.closePanel(); + } + }); + }); diff --git a/src/UI/Shared/ControlPanel/ControlPanelRegion.js b/src/UI/Shared/ControlPanel/ControlPanelRegion.js new file mode 100644 index 000000000..3b4ac55d5 --- /dev/null +++ b/src/UI/Shared/ControlPanel/ControlPanelRegion.js @@ -0,0 +1,35 @@ +'use strict'; +define( + [ + 'jquery', + 'backbone', + 'marionette' + ], function ($,Backbone, Marionette) { + var region = Marionette.Region.extend({ + el: '#control-panel-region', + + constructor: function () { + Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); + this.on('show', this.showPanel, this); + }, + + getEl: function (selector) { + var $el = $(selector); + + return $el; + }, + + showPanel: function () { + $('body').addClass('control-panel-visible'); + this.$el.animate({ 'margin-bottom': 0, 'opacity': 1 }, { queue: false, duration: 300 }); + }, + + closePanel: function () { + $('body').removeClass('control-panel-visible'); + this.$el.animate({ 'margin-bottom': -100, 'opacity': 0 }, { queue: false, duration: 300 }); + this.reset(); + } + }); + + return region; + }); diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 177092de3..9fcedea9a 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -15,8 +15,8 @@ define( return Marionette.AppRouter.extend({ initialize: function () { - vent.on(vent.Commands.OpenModalCommand, this._openModal, this); - vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); + vent.on(vent.Commands.OpenModalCommand, this._openControlPanel, this); + vent.on(vent.Commands.CloseModalCommand, this._closeControlPanel, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); @@ -25,12 +25,12 @@ define( vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); }, - _openModal: function (view) { + _openControlPanel: function (view) { AppLayout.modalRegion.show(view); }, - _closeModal: function () { - AppLayout.modalRegion.closeModal(); + _closeControlPanel: function () { + AppLayout.modalRegion.closePanel(); }, _editSeries: function (options) { diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js index f2a776050..414138ee7 100644 --- a/src/UI/Shared/Modal/ModalRegion.js +++ b/src/UI/Shared/Modal/ModalRegion.js @@ -11,7 +11,7 @@ define( constructor: function () { Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showModal, this); + this.on('show', this.showPanel, this); }, getEl: function (selector) { @@ -20,7 +20,7 @@ define( return $el; }, - showModal: function () { + showPanel: function () { this.$el.addClass('modal hide fade'); //need tab index so close on escape works @@ -32,7 +32,7 @@ define( 'backdrop': 'static'}); }, - closeModal: function () { + closePanel: function () { $(this.el).modal('hide'); this.reset(); } diff --git a/src/UI/app.js b/src/UI/app.js index 51a0ee0e4..4edde0dc1 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -209,13 +209,15 @@ define( 'Series/SeriesController', 'Router', 'Shared/Modal/ModalController', + 'Shared/ControlPanel/ControlPanelController', 'System/StatusModel', 'Instrumentation/StringFormat', 'LifeCycle' - ], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, serverStatusModel) { + ], function ($, Backbone, Marionette, RouteBinder, SignalRBroadcaster, NavbarView, AppLayout, SeriesController, Router, ModalController, ControlPanelController, serverStatusModel) { new SeriesController(); new ModalController(); + new ControlPanelController(); new Router(); var app = new Marionette.Application(); diff --git a/src/UI/index.html b/src/UI/index.html index 84603cd00..e04003bae 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -48,7 +48,7 @@ -
    +
    + +