From ff5dc6f0ce3f2dc05541ad1440e7e0f8a90dbf64 Mon Sep 17 00:00:00 2001 From: Icer Addis Date: Thu, 2 Jan 2014 19:43:57 -0800 Subject: [PATCH 01/37] NLog - Added debugger target --- .../Instrumentation/LogTargets.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 4f028aa60..e5d1be2f0 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -15,6 +15,11 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration = new LoggingConfiguration(); + if (System.Diagnostics.Debugger.IsAttached) + { + RegisterDebugger(); + } + RegisterExceptron(); if (updateApp) @@ -35,6 +40,18 @@ namespace NzbDrone.Common.Instrumentation LogManager.ReconfigExistingLoggers(); } + private static void RegisterDebugger() + { + DebuggerTarget target = new DebuggerTarget(); + target.Name = "debuggerLogger"; + target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}${exception:format=ToString}${newline}}"; + + var loggingRule = new LoggingRule("*", LogLevel.Trace, target); + LogManager.Configuration.AddTarget("console", target); + LogManager.Configuration.LoggingRules.Add(loggingRule); + } + + private static void RegisterConsole() { var level = LogLevel.Trace; From 6c34acc8b323d407bc115ad1c1fb377b5049dbec Mon Sep 17 00:00:00 2001 From: Icer Addis Date: Thu, 2 Jan 2014 19:47:21 -0800 Subject: [PATCH 02/37] NLog - fixed debugger target name --- src/NzbDrone.Common/Instrumentation/LogTargets.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index e5d1be2f0..6f35a8a95 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Common.Instrumentation target.Layout = "[${level}] [${threadid}] ${logger}: ${message} ${onexception:inner=${newline}${newline}${exception:format=ToString}${newline}}"; var loggingRule = new LoggingRule("*", LogLevel.Trace, target); - LogManager.Configuration.AddTarget("console", target); + LogManager.Configuration.AddTarget("debugger", target); LogManager.Configuration.LoggingRules.Add(loggingRule); } From e2939847a5af292cad82d9565a9915f1a51dacd6 Mon Sep 17 00:00:00 2001 From: Icer Addis Date: Tue, 7 Jan 2014 04:12:55 -0800 Subject: [PATCH 03/37] Parser logging - changed Debug.WriteLine to Logger.Trace --- src/NzbDrone.Core/Parser/Parser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 7797a61cd..21d88b971 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -153,7 +153,7 @@ namespace NzbDrone.Core.Parser if (match.Count != 0) { - Debug.WriteLine(regex); + Logger.Trace(regex); try { var result = ParseMatchCollection(match); From 11aa82832f9abd10650fa8d48632a7c60bb7214b Mon Sep 17 00:00:00 2001 From: Icer Addis Date: Mon, 13 Jan 2014 21:25:03 -0800 Subject: [PATCH 04/37] Added using statement --- src/NzbDrone.Common/Instrumentation/LogTargets.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 6f35a8a95..ec21d2b5c 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using NLog; using NLog.Config; @@ -15,7 +16,7 @@ namespace NzbDrone.Common.Instrumentation LogManager.Configuration = new LoggingConfiguration(); - if (System.Diagnostics.Debugger.IsAttached) + if (Debugger.IsAttached) { RegisterDebugger(); } From 37231d1ef08a4bd1038c7a84b2722d560621c0df Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 5 Jan 2014 22:20:08 -0800 Subject: [PATCH 05/37] DiskProvider split to Windows and Mono projects --- build.ps1 | 42 ++-- src/Libraries/Mono.Posix.dll | Bin 0 -> 194048 bytes .../DirectoryLookupServiceFixture.cs | 1 + .../Directories/DirectoryLookupService.cs | 1 + .../Frontend/Mappers/IndexHtmlMapper.cs | 1 + .../Frontend/Mappers/LogFileMapper.cs | 1 + .../Frontend/Mappers/MediaCoverMapper.cs | 1 + .../Frontend/Mappers/StaticResourceMapper.cs | 1 + .../Mappers/StaticResourceMapperBase.cs | 1 + src/NzbDrone.Api/Logs/LogFileModule.cs | 1 + src/NzbDrone.App.Test/ContainerFixture.cs | 2 +- .../NzbDrone.Host.Test.csproj | 4 + ...rFixture.cs => DiskProviderFixtureBase.cs} | 15 +- .../DiskProviderTests/FreeSpaceFixture.cs | 43 ---- .../FreeSpaceFixtureBase.cs} | 39 +++- ...arentFixture.cs => IsParentFixtureBase.cs} | 4 +- .../NzbDrone.Common.Test.csproj | 6 +- .../Composition/ContainerBuilderBase.cs | 3 +- .../DiskProviderBase.cs} | 180 +-------------- src/NzbDrone.Common/Disk/IDiskProvider.cs | 50 ++++ .../EnvironmentInfo/AppFolderFactory.cs | 53 +++++ .../EnvironmentInfo/AppFolderInfo.cs | 31 +-- .../EnvironmentInfo/RuntimeInfo.cs | 2 - .../Instrumentation/LogTargets.cs | 14 +- src/NzbDrone.Common/NzbDrone.Common.csproj | 4 +- .../BlackholeProviderFixture.cs | 1 + .../PneumaticProviderFixture.cs | 1 + src/NzbDrone.Core.Test/Framework/CoreTest.cs | 12 +- .../CoverExistsSpecificationFixture.cs | 1 + .../MediaCoverServiceFixture.cs | 3 +- .../DownloadedEpisodesImportServiceFixture.cs | 1 + .../MoveEpisodeFileFixture.cs | 1 + .../FreeSpaceSpecificationFixture.cs | 1 + .../NotInUseSpecificationFixture.cs | 1 + .../NotUnpackingSpecificationFixture.cs | 1 + .../MediaFileTableCleanupServiceFixture.cs | 1 + .../MediaInfo/VideoFileInfoReaderFixture.cs | 1 + .../UpgradeMediaFileServiceFixture.cs | 1 + .../NzbDrone.Core.Test.csproj | 1 - .../GetVideoFilesFixture.cs | 1 + .../RecycleBinProviderTests/CleanupFixture.cs | 1 + .../DeleteDirectoryFixture.cs | 1 + .../DeleteFileFixture.cs | 1 + .../RecycleBinProviderTests/EmptyFixture.cs | 1 + .../FreeSpaceOnDrivesFixture.cs | 1 + .../RootFolderServiceFixture.cs | 1 + .../UpdateTests/UpdateServiceFixture.cs | 3 +- src/NzbDrone.Core/Datastore/DbFactory.cs | 2 - .../DiskSpace/DiskSpaceService.cs | 1 + .../Download/Clients/PneumaticClient.cs | 1 + .../Instrumentation/DeleteLogFilesService.cs | 1 + .../CoverAlreadyExistsSpecification.cs | 1 + .../MediaCover/MediaCoverService.cs | 1 + .../MediaFiles/DiskScanService.cs | 1 + .../DownloadedEpisodesImportService.cs | 1 + .../MediaFiles/EpisodeFileMovingService.cs | 1 + .../EpisodeImport/ImportApprovedEpisodes.cs | 1 + .../EpisodeImport/ImportDecisionMaker.cs | 1 + .../Specifications/FreeSpaceSpecification.cs | 1 + .../Specifications/NotInUseSpecification.cs | 1 + .../NotUnpackingSpecification.cs | 1 + .../MediaFileTableCleanupService.cs | 1 + .../MediaInfo/VideoFileInfoReader.cs | 1 + .../MediaFiles/RecycleBinProvider.cs | 1 + .../MediaFiles/UpgradeMediaFileService.cs | 1 + src/NzbDrone.Core/Parser/ParsingService.cs | 1 + .../RootFolders/RootFolderService.cs | 1 + .../Update/InstallUpdateService.cs | 1 + src/NzbDrone.Host/Bootstrap.cs | 4 +- src/NzbDrone.Host/MainAppContainerBuilder.cs | 31 ++- .../NzbDrone.Integration.Test.csproj | 4 + .../DiskProviderTests/DiskProviderFixture.cs | 14 ++ .../DiskProviderTests/FreeSpaceFixture.cs | 14 ++ .../DiskProviderTests/IsParentFixture.cs | 14 ++ .../NzbDrone.Mono.Test.csproj | 100 ++++++++ .../Properties/AssemblyInfo.cs | 36 +++ src/NzbDrone.Mono.Test/packages.config | 4 + src/NzbDrone.Mono/DiskProvider.cs | 96 ++++++++ src/NzbDrone.Mono/NzbDrone.Mono.csproj | 95 ++++++++ src/NzbDrone.Mono/Properties/AssemblyInfo.cs | 36 +++ src/NzbDrone.Mono/packages.config | 4 + src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs | 22 ++ .../InstallUpdateServiceFixture.cs | 1 + src/NzbDrone.Update/UpdateContainerBuilder.cs | 29 ++- .../UpdateEngine/BackupAndRestore.cs | 1 + .../UpdateEngine/InstallUpdateService.cs | 1 + .../DiskProviderTests/DiskProviderFixture.cs | 14 ++ .../DiskProviderTests/FreeSpaceFixture.cs | 14 ++ .../DiskProviderTests/IsParentFixture.cs | 14 ++ .../NzbDrone.Windows.Test.csproj | 101 +++++++++ .../Properties/AssemblyInfo.cs | 36 +++ src/NzbDrone.Windows.Test/packages.config | 4 + src/NzbDrone.Windows/DiskProvider.cs | 103 +++++++++ src/NzbDrone.Windows/NzbDrone.Windows.csproj | 86 +++++++ .../Properties/AssemblyInfo.cs | 36 +++ src/NzbDrone.Windows/packages.config | 4 + src/NzbDrone.sln | 213 ++++++++++++++++++ 97 files changed, 1366 insertions(+), 324 deletions(-) create mode 100644 src/Libraries/Mono.Posix.dll rename src/NzbDrone.Common.Test/DiskProviderTests/{DiskProviderFixture.cs => DiskProviderFixtureBase.cs} (97%) delete mode 100644 src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixture.cs rename src/{NzbDrone.Core.Test/ProviderTests/DiskProviderTests/FreeDiskSpaceFixture.cs => NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs} (51%) rename src/NzbDrone.Common.Test/DiskProviderTests/{IsParentFixture.cs => IsParentFixtureBase.cs} (87%) rename src/NzbDrone.Common/{DiskProvider.cs => Disk/DiskProviderBase.cs} (67%) create mode 100644 src/NzbDrone.Common/Disk/IDiskProvider.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/AppFolderFactory.cs create mode 100644 src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs create mode 100644 src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs create mode 100644 src/NzbDrone.Mono.Test/DiskProviderTests/IsParentFixture.cs create mode 100644 src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj create mode 100644 src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs create mode 100644 src/NzbDrone.Mono.Test/packages.config create mode 100644 src/NzbDrone.Mono/DiskProvider.cs create mode 100644 src/NzbDrone.Mono/NzbDrone.Mono.csproj create mode 100644 src/NzbDrone.Mono/Properties/AssemblyInfo.cs create mode 100644 src/NzbDrone.Mono/packages.config create mode 100644 src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs create mode 100644 src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs create mode 100644 src/NzbDrone.Windows.Test/DiskProviderTests/IsParentFixture.cs create mode 100644 src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj create mode 100644 src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs create mode 100644 src/NzbDrone.Windows.Test/packages.config create mode 100644 src/NzbDrone.Windows/DiskProvider.cs create mode 100644 src/NzbDrone.Windows/NzbDrone.Windows.csproj create mode 100644 src/NzbDrone.Windows/Properties/AssemblyInfo.cs create mode 100644 src/NzbDrone.Windows/packages.config diff --git a/build.ps1 b/build.ps1 index b7ce1cfac..639b28320 100644 --- a/build.ps1 +++ b/build.ps1 @@ -35,21 +35,18 @@ Function CleanFolder($path) Write-Host Removing XMLDoc files get-childitem $path -File -Filter *.xml -Recurse | foreach ($_) {remove-item $_.fullname} - get-childitem $path -File -Filter *.transform -Recurse | foreach ($_) {remove-item $_.fullname} - + get-childitem $path -File -Filter *.transform -Recurse | foreach ($_) {remove-item $_.fullname} - get-childitem $path -File -Filter *.dll.config -Recurse | foreach ($_) {remove-item $_.fullname} + get-childitem $path -File -Filter *.dll.config -Recurse | foreach ($_) {remove-item $_.fullname} Write-Host Removing FluentValidation.Resources files get-childitem $path -File -Filter FluentValidation.resources.dll -recurse | foreach ($_) {remove-item $_.fullname} - get-childitem $path -File -Filter app.config -Recurse | foreach ($_) {remove-item $_.fullname} - + get-childitem $path -File -Filter app.config -Recurse | foreach ($_) {remove-item $_.fullname} Write-Host Removing .less files get-childitem $path -File -Filter *.less -Recurse | foreach ($_) {remove-item $_.fullname} - Write-Host Removing NuGet Remove-Item -Recurse -Force "$path\NuGet" @@ -84,20 +81,23 @@ Function PackageMono() get-childitem $outputFolderMono -File -Filter *.pdb -Recurse | foreach ($_) {remove-item $_.fullname} Write-Host Removing Service helpers - get-childitem $outputFolderMono -File -Filter ServiceUninstall.* -Recurse | foreach ($_) {remove-item $_.fullname} - get-childitem $outputFolderMono -File -Filter ServiceInstall.* -Recurse | foreach ($_) {remove-item $_.fullname} + get-childitem $outputFolderMono -File -Filter ServiceUninstall.* -Recurse | foreach ($_) {remove-item $_.fullname} + get-childitem $outputFolderMono -File -Filter ServiceInstall.* -Recurse | foreach ($_) {remove-item $_.fullname} Write-Host Removing native windows binaries Sqlite, MediaInfo - get-childitem $outputFolderMono -File -Filter sqlite3.* -Recurse | foreach ($_) {remove-item $_.fullname} - get-childitem $outputFolderMono -File -Filter MediaInfo.* -Recurse | foreach ($_) {remove-item $_.fullname} + 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} + Get-ChildItem $outputFolderMono -File -Filter "NzbDrone.exe*" -Recurse | foreach ($_) {remove-item $_.fullname} - Get-ChildItem $outputFolderMono -File -Filter "NzbDrone.Console.exe*" -Recurse | foreach ($_) { + Write-Host Removing NzbDrone.Windows + get-childitem $outputFolderMono -File -Filter NzbDrone.Windows.* -Recurse | foreach ($_) {remove-item $_.fullname} + + Get-ChildItem $outputFolderMono -File -Filter "NzbDrone.Console.exe*" -Recurse | foreach ($_) { $newName = $_.fullname -Replace ".Console","" Rename-Item $_.fullname $newName @@ -108,12 +108,11 @@ Function PackageMono() Write-Host "##teamcity[progressFinish 'Creating Mono Package']" } - Function AddJsonNet() { - get-childitem $outputFolder -File -Filter Newtonsoft.Json.* -Recurse | foreach ($_) {remove-item $_.fullname} - Copy-Item .\src\packages\Newtonsoft.Json.5.*\lib\net35\*.dll -Destination $outputFolder - Copy-Item .\src\packages\Newtonsoft.Json.5.*\lib\net35\*.dll -Destination $outputFolder\NzbDrone.Update + get-childitem $outputFolder -File -Filter Newtonsoft.Json.* -Recurse | foreach ($_) {remove-item $_.fullname} + Copy-Item .\src\packages\Newtonsoft.Json.5.*\lib\net35\*.dll -Destination $outputFolder + Copy-Item .\src\packages\Newtonsoft.Json.5.*\lib\net35\*.dll -Destination $outputFolder\NzbDrone.Update } Function PackageTests() @@ -127,8 +126,7 @@ Function PackageTests() Remove-Item -Recurse -Force $testPackageFolder -ErrorAction Continue } - - Get-ChildItem -Recurse -Directory | Where-Object {$_.FullName -like $testSearchPattern} | foreach($_){ + Get-ChildItem -Recurse -Directory | Where-Object {$_.FullName -like $testSearchPattern} | foreach($_){ Copy-Item -Recurse ($_.FullName + "\*") $testPackageFolder -ErrorAction Ignore } @@ -149,7 +147,6 @@ Function PackageTests() Write-Host "##teamcity[progressFinish 'Creating Test Package']" } - Function RunGrunt() { Write-Host "##teamcity[progressStart 'Running Grunt']" @@ -174,7 +171,14 @@ Function CheckExitCode() } } +Function CleanupWindowsPackage() +{ + Write-Host Removing NzbDrone.Mono + get-childitem $outputFolder -File -Filter NzbDrone.Mono.* -Recurse | foreach ($_) {remove-item $_.fullname} +} + Build RunGrunt PackageMono PackageTests +CleanupWindowsPackage diff --git a/src/Libraries/Mono.Posix.dll b/src/Libraries/Mono.Posix.dll new file mode 100644 index 0000000000000000000000000000000000000000..8e219445f593bd0c9ac43269d6c156f73501d2e2 GIT binary patch literal 194048 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ⅇ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%0DqB6h99HlUx@p2xnc$a0MqRepjP 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 G@c#jP>B%_& literal 0 HcmV?d00001 diff --git a/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs index 849773ddb..3fbdac946 100644 --- a/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs @@ -6,6 +6,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Api.Directories; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Test.Common; namespace NzbDrone.Api.Test diff --git a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs b/src/NzbDrone.Api/Directories/DirectoryLookupService.cs index 38c6b38e4..6f372d0e9 100644 --- a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs +++ b/src/NzbDrone.Api/Directories/DirectoryLookupService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Common; +using NzbDrone.Common.Disk; namespace NzbDrone.Api.Directories { diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index 46af86380..501e0b6fc 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using Nancy; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; diff --git a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs index f251fff97..9d293ee65 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs @@ -1,6 +1,7 @@ using System.IO; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Frontend.Mappers diff --git a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs index 3fe35324b..f6864b06a 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs @@ -1,6 +1,7 @@ using System.IO; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Frontend.Mappers diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs index da9fac532..5dbd3b734 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs @@ -1,6 +1,7 @@ using System.IO; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Frontend.Mappers diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs index 4cc42f49f..1dd542692 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs @@ -3,6 +3,7 @@ using NLog; using Nancy; using Nancy.Responses; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Frontend.Mappers diff --git a/src/NzbDrone.Api/Logs/LogFileModule.cs b/src/NzbDrone.Api/Logs/LogFileModule.cs index 4fe48d21e..b896e6020 100644 --- a/src/NzbDrone.Api/Logs/LogFileModule.cs +++ b/src/NzbDrone.Api/Logs/LogFileModule.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Api.Logs diff --git a/src/NzbDrone.App.Test/ContainerFixture.cs b/src/NzbDrone.App.Test/ContainerFixture.cs index d542b9dee..7c9dcba5b 100644 --- a/src/NzbDrone.App.Test/ContainerFixture.cs +++ b/src/NzbDrone.App.Test/ContainerFixture.cs @@ -27,7 +27,7 @@ namespace NzbDrone.App.Test } [Test] - public void should_be_able_to_resolve_downlodclients() + public void should_be_able_to_resolve_downloadclients() { MainAppContainerBuilder.BuildContainer(args).Resolve>().Should().NotBeEmpty(); } diff --git a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj index 274499d34..badd7903b 100644 --- a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj +++ b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj @@ -97,6 +97,10 @@ + + xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Mono.*" "$(TargetDir)" +xcopy /s /y "$(SolutionDir)\..\_output\NzbDrone.Windows.*" "$(TargetDir)" + + \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..a2dbbc810 --- /dev/null +++ b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NzbDrone.Mono.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NzbDrone.Mono.Test")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("45299d3c-34ff-48ca-9093-de2f037c38ac")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Mono.Test/packages.config b/src/NzbDrone.Mono.Test/packages.config new file mode 100644 index 000000000..5c3ca54dd --- /dev/null +++ b/src/NzbDrone.Mono.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs new file mode 100644 index 000000000..58c6f7d20 --- /dev/null +++ b/src/NzbDrone.Mono/DiskProvider.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; +using System.Linq; +using Mono.Unix.Native; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Mono +{ + public class DiskProvider : DiskProviderBase + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(); + + public override long? GetAvailableSpace(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var root = GetPathRoot(path); + + if (!FolderExists(root)) + throw new DirectoryNotFoundException(root); + + try + { + return GetDriveInfoLinux(path).AvailableFreeSpace; + } + catch (InvalidOperationException e) + { + Logger.ErrorException("Couldn't get free space for " + path, e); + } + + return null; + } + + public override void InheritFolderPermissions(string filename) + { + Ensure.That(filename, () => filename).IsValidPath(); + + try + { + var fs = File.GetAccessControl(filename); + fs.SetAccessRuleProtection(false, false); + File.SetAccessControl(filename, fs); + } + catch (NotImplementedException) + { + } + } + + public override void SetFilePermissions(string path, string mask) + { + var filePermissions = NativeConvert.FromOctalPermissionString(mask); + + if (Syscall.chmod(path, filePermissions) < 0) + { + var error = Stdlib.GetLastError(); + + throw new Exception("Error setting file permissions: " + error); + } + } + + public override long? GetTotalSize(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var root = GetPathRoot(path); + + if (!FolderExists(root)) + throw new DirectoryNotFoundException(root); + + try + { + return GetDriveInfoLinux(path).TotalSize; + } + catch (InvalidOperationException e) + { + Logger.ErrorException("Couldn't get total space for " + path, e); + } + + return null; + } + + private DriveInfo GetDriveInfoLinux(string path) + { + var drives = DriveInfo.GetDrives(); + + return + drives.Where(drive => + drive.IsReady && path.StartsWith(drive.Name, StringComparison.CurrentCultureIgnoreCase)) + .OrderByDescending(drive => drive.Name.Length) + .First(); + } + } +} diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj new file mode 100644 index 000000000..a5364e8ce --- /dev/null +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -0,0 +1,95 @@ + + + + + Debug + AnyCPU + {15AD7579-A314-4626-B556-663F51D97CD1} + Library + Properties + NzbDrone.Mono + NzbDrone.Mono + v4.0 + 512 + + ..\ + true + + + true + full + false + ..\..\_output\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + ..\..\_output\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + ..\..\_output\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + False + ..\Libraries\Mono.Posix.dll + + + False + ..\packages\NLog.2.1.0\lib\net40\NLog.dll + + + + + + + + + + + + + + + + + + + {f2be0fdf-6e47-4827-a420-dd4ef82407f8} + NzbDrone.Common + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Mono/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6a7d1a9dc --- /dev/null +++ b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NzbDrone.Mono")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NzbDrone.Mono")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("01493ea5-494f-43bf-be18-8ae4d0708fc6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Mono/packages.config b/src/NzbDrone.Mono/packages.config new file mode 100644 index 000000000..d5e3be0f3 --- /dev/null +++ b/src/NzbDrone.Mono/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 35f24ff92..2c7e4b4f7 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -3,12 +3,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.Practices.Unity; using Moq; using Moq.Language.Flow; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Test.Common.AutoMoq.Unity; [assembly: InternalsVisibleTo("AutoMoq.Tests")] @@ -132,6 +135,8 @@ namespace NzbDrone.Test.Common.AutoMoq this.container = container; container.RegisterInstance(this); + RegisterPlatformLibrary(container); + registeredMocks = new Dictionary(); AddTheAutoMockingContainerExtensionToTheContainer(container); } @@ -164,6 +169,23 @@ namespace NzbDrone.Test.Common.AutoMoq return typeof(T); } + private void RegisterPlatformLibrary(IUnityContainer container) + { + var assemblyName = "NzbDrone.Windows"; + + if (OsInfo.IsLinux) + { + assemblyName = "NzbDrone.Mono"; + } + + if (!File.Exists(assemblyName + ".dll")) + { + return; + } + + Assembly.Load(assemblyName); + } + #endregion } } \ No newline at end of file diff --git a/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs b/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs index b7a4515ee..2e240decd 100644 --- a/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs +++ b/src/NzbDrone.Update.Test/InstallUpdateServiceFixture.cs @@ -3,6 +3,7 @@ using System.IO; using FluentAssertions; using NUnit.Framework; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Test.Common; using NzbDrone.Update.UpdateEngine; diff --git a/src/NzbDrone.Update/UpdateContainerBuilder.cs b/src/NzbDrone.Update/UpdateContainerBuilder.cs index 5f99a618c..f9d59d69f 100644 --- a/src/NzbDrone.Update/UpdateContainerBuilder.cs +++ b/src/NzbDrone.Update/UpdateContainerBuilder.cs @@ -1,19 +1,38 @@ -using NzbDrone.Common.Composition; +using System; +using System.Collections.Generic; +using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Update { public class UpdateContainerBuilder : ContainerBuilderBase { - private UpdateContainerBuilder(IStartupContext startupContext) - : base(startupContext, "NzbDrone.Update", "NzbDrone.Common") + private UpdateContainerBuilder(IStartupContext startupContext, string[] assemblies) + : base(startupContext, assemblies) { } public static IContainer Build(IStartupContext startupContext) { - return new UpdateContainerBuilder(startupContext).Container; + var assemblies = new List + { + "NzbDrone.Update", + "NzbDrone.Common" + }; + + if (OsInfo.IsWindows) + { + assemblies.Add("NzbDrone.Windows"); + } + + else + { + assemblies.Add("NzbDrone.Mono"); + } + + return new UpdateContainerBuilder(startupContext, assemblies.ToArray()).Container; } } -} \ No newline at end of file +} + \ No newline at end of file diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs index 91552105c..3787b25d3 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs @@ -1,5 +1,6 @@ using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Update.UpdateEngine diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index 0323360cc..0e6001677 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -2,6 +2,7 @@ using System; using System.IO; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; namespace NzbDrone.Update.UpdateEngine diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs new file mode 100644 index 000000000..68b2d1f0c --- /dev/null +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs @@ -0,0 +1,14 @@ +using NUnit.Framework; +using NzbDrone.Common.Test.DiskProviderTests; + +namespace NzbDrone.Windows.Test.DiskProviderTests +{ + [TestFixture] + public class DiskProviderFixture : DiskProviderFixtureBase + { + public DiskProviderFixture() + { + WindowsOnly(); + } + } +} diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs new file mode 100644 index 000000000..a642b49a9 --- /dev/null +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -0,0 +1,14 @@ +using NUnit.Framework; +using NzbDrone.Common.Test.DiskProviderTests; + +namespace NzbDrone.Windows.Test.DiskProviderTests +{ + [TestFixture] + public class FreeSpaceFixture : FreeSpaceFixtureBase + { + public FreeSpaceFixture() + { + WindowsOnly(); + } + } +} diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/IsParentFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/IsParentFixture.cs new file mode 100644 index 000000000..00b6aa4c2 --- /dev/null +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/IsParentFixture.cs @@ -0,0 +1,14 @@ +using NUnit.Framework; +using NzbDrone.Common.Test.DiskProviderTests; + +namespace NzbDrone.Windows.Test.DiskProviderTests +{ + [TestFixture] + public class IsParentFixtureFixture : IsParentFixtureBase + { + public IsParentFixtureFixture() + { + WindowsOnly(); + } + } +} diff --git a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj new file mode 100644 index 000000000..098c95b80 --- /dev/null +++ b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj @@ -0,0 +1,101 @@ + + + + + Debug + AnyCPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA} + Library + Properties + NzbDrone.Windows.Test + NzbDrone.Windows.Test + v4.0 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + False + ..\packages\NUnit.2.6.2\lib\nunit.framework.dll + + + + + + + + + + + + + + + + + + {bec74619-ddbb-4fba-b517-d3e20afc9997} + NzbDrone.Common.Test + + + {f2be0fdf-6e47-4827-a420-dd4ef82407f8} + NzbDrone.Common + + + {caddfce0-7509-4430-8364-2074e1eefca2} + NzbDrone.Test.Common + + + {911284d3-f130-459e-836c-2430b6fbf21d} + NzbDrone.Windows + + + + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..238a3d68a --- /dev/null +++ b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NzbDrone.Windows.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NzbDrone.Windows.Test")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("372cb8dc-5cdf-4fe4-9e1d-725827889bc7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Windows.Test/packages.config b/src/NzbDrone.Windows.Test/packages.config new file mode 100644 index 000000000..5c3ca54dd --- /dev/null +++ b/src/NzbDrone.Windows.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NzbDrone.Windows/DiskProvider.cs b/src/NzbDrone.Windows/DiskProvider.cs new file mode 100644 index 000000000..27b9debc4 --- /dev/null +++ b/src/NzbDrone.Windows/DiskProvider.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Windows +{ + public class DiskProvider : DiskProviderBase + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool GetDiskFreeSpaceEx(string lpDirectoryName, + out ulong lpFreeBytesAvailable, + out ulong lpTotalNumberOfBytes, + out ulong lpTotalNumberOfFreeBytes); + + public override long? GetAvailableSpace(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var root = GetPathRoot(path); + + if (!FolderExists(root)) + throw new DirectoryNotFoundException(root); + + return DriveFreeSpaceEx(root); + } + + public override void InheritFolderPermissions(string filename) + { + Ensure.That(filename, () => filename).IsValidPath(); + + var fs = File.GetAccessControl(filename); + fs.SetAccessRuleProtection(false, false); + File.SetAccessControl(filename, fs); + } + + public override void SetFilePermissions(string path, string mask) + { + throw new NotImplementedException(); + } + + public override long? GetTotalSize(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var root = GetPathRoot(path); + + if (!FolderExists(root)) + throw new DirectoryNotFoundException(root); + + return DriveTotalSizeEx(root); + } + + private static long DriveFreeSpaceEx(string folderName) + { + Ensure.That(folderName, () => folderName).IsValidPath(); + + if (!folderName.EndsWith("\\")) + { + folderName += '\\'; + } + + ulong free = 0; + ulong dummy1 = 0; + ulong dummy2 = 0; + + if (GetDiskFreeSpaceEx(folderName, out free, out dummy1, out dummy2)) + { + return (long)free; + } + + return 0; + } + + private static long DriveTotalSizeEx(string folderName) + { + Ensure.That(folderName, () => folderName).IsValidPath(); + + if (!folderName.EndsWith("\\")) + { + folderName += '\\'; + } + + ulong total = 0; + ulong dummy1 = 0; + ulong dummy2 = 0; + + if (GetDiskFreeSpaceEx(folderName, out dummy1, out total, out dummy2)) + { + return (long)total; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj new file mode 100644 index 000000000..607f09a4e --- /dev/null +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {911284D3-F130-459E-836C-2430B6FBF21D} + Library + Properties + NzbDrone.Windows + NzbDrone.Windows + v4.0 + 512 + ..\ + true + + + true + full + false + ..\..\_output\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + ..\..\_output\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + ..\..\_output\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + ..\packages\NLog.2.1.0\lib\net40\NLog.dll + + + + + + + + + + + + + + + + + + + {f2be0fdf-6e47-4827-a420-dd4ef82407f8} + NzbDrone.Common + + + + + + \ No newline at end of file diff --git a/src/NzbDrone.Windows/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8a24ac703 --- /dev/null +++ b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NzbDrone.Windows")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NzbDrone.Windows")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cea28fa9-43d0-4682-99f2-d364377adbdf")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Windows/packages.config b/src/NzbDrone.Windows/packages.config new file mode 100644 index 000000000..d5e3be0f3 --- /dev/null +++ b/src/NzbDrone.Windows/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index c6f82ef3f..6607c1c36 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -66,108 +66,316 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Ow EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono", "NzbDrone.Mono\NzbDrone.Mono.csproj", "{15AD7579-A314-4626-B556-663F51D97CD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows", "NzbDrone.Windows\NzbDrone.Windows.csproj", "{911284D3-F130-459E-836C-2430B6FBF21D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono.Test", "NzbDrone.Mono.Test\NzbDrone.Mono.Test.csproj", "{40D72824-7D02-4A77-9106-8FE0EEA2B997}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Any CPU.ActiveCfg = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|Mixed Platforms.Build.0 = Debug|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.ActiveCfg = Debug|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.Build.0 = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|Any CPU.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|Mixed Platforms.Build.0 = Release|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.ActiveCfg = Release|x86 {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Any CPU.ActiveCfg = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|Mixed Platforms.Build.0 = Debug|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.ActiveCfg = Debug|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.Build.0 = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|Any CPU.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|Mixed Platforms.Build.0 = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.ActiveCfg = Release|x86 {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.Build.0 = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Any CPU.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|Mixed Platforms.Build.0 = Debug|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.ActiveCfg = Debug|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|Any CPU.ActiveCfg = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|Mixed Platforms.Build.0 = Release|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.ActiveCfg = Release|x86 {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.Build.0 = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Any CPU.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|Mixed Platforms.Build.0 = Debug|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.ActiveCfg = Debug|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|Any CPU.ActiveCfg = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|Mixed Platforms.Build.0 = Release|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86 {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Any CPU.ActiveCfg = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|Mixed Platforms.Build.0 = Debug|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.ActiveCfg = Debug|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.Build.0 = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|Any CPU.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|Mixed Platforms.Build.0 = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.ActiveCfg = Release|x86 {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.Build.0 = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Any CPU.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|Mixed Platforms.Build.0 = Debug|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.ActiveCfg = Debug|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|Any CPU.ActiveCfg = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|Mixed Platforms.Build.0 = Release|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.ActiveCfg = Release|x86 {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.Build.0 = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Any CPU.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|Mixed Platforms.Build.0 = Debug|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.ActiveCfg = Debug|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|Any CPU.ActiveCfg = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|Mixed Platforms.Build.0 = Release|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.ActiveCfg = Release|x86 {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|Mixed Platforms.Build.0 = Debug|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.ActiveCfg = Debug|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.Build.0 = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|Any CPU.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|Mixed Platforms.Build.0 = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.ActiveCfg = Release|x86 {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.Build.0 = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Any CPU.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|Mixed Platforms.Build.0 = Debug|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.ActiveCfg = Debug|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|Any CPU.ActiveCfg = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|Mixed Platforms.Build.0 = Release|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.ActiveCfg = Release|x86 {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.Build.0 = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Any CPU.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|Mixed Platforms.Build.0 = Debug|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.ActiveCfg = Debug|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|Any CPU.ActiveCfg = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|Mixed Platforms.Build.0 = Release|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.ActiveCfg = Release|x86 {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.Build.0 = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Any CPU.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|Mixed Platforms.Build.0 = Debug|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.ActiveCfg = Debug|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.Build.0 = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|Any CPU.ActiveCfg = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|Mixed Platforms.Build.0 = Release|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.ActiveCfg = Release|x86 {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.Build.0 = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Any CPU.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|Mixed Platforms.Build.0 = Debug|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.ActiveCfg = Debug|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.Build.0 = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|Any CPU.ActiveCfg = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|Mixed Platforms.Build.0 = Release|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.ActiveCfg = Release|x86 {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.Build.0 = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Any CPU.ActiveCfg = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|Mixed Platforms.Build.0 = Debug|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.ActiveCfg = Debug|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.Build.0 = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|Any CPU.ActiveCfg = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|Mixed Platforms.Build.0 = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.ActiveCfg = Release|x86 {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.Build.0 = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Any CPU.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|Mixed Platforms.Build.0 = Debug|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.ActiveCfg = Debug|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|Any CPU.ActiveCfg = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|Mixed Platforms.Build.0 = Release|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.ActiveCfg = Release|x86 {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|Mixed Platforms.Build.0 = Debug|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.ActiveCfg = Debug|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.Build.0 = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|Any CPU.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|Mixed Platforms.Build.0 = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.ActiveCfg = Release|x86 {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Any CPU.ActiveCfg = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|Mixed Platforms.Build.0 = Debug|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.ActiveCfg = Debug|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.Build.0 = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|Any CPU.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|Mixed Platforms.Build.0 = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.ActiveCfg = Release|x86 {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.Build.0 = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Any CPU.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|Mixed Platforms.Build.0 = Debug|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.ActiveCfg = Debug|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|Any CPU.ActiveCfg = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|Mixed Platforms.Build.0 = Release|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.ActiveCfg = Release|x86 {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.Build.0 = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Any CPU.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|Mixed Platforms.Build.0 = Debug|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.ActiveCfg = Debug|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|Any CPU.ActiveCfg = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|Mixed Platforms.Build.0 = Release|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.ActiveCfg = Release|x86 {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.Build.0 = Release|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Debug|Any CPU.ActiveCfg = Debug|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Debug|Mixed Platforms.Build.0 = Debug|x86 {B1784698-592E-4132-BDFA-9817409E3A96}.Debug|x86.ActiveCfg = Debug|x86 {B1784698-592E-4132-BDFA-9817409E3A96}.Debug|x86.Build.0 = Debug|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Release|Any CPU.ActiveCfg = Release|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {B1784698-592E-4132-BDFA-9817409E3A96}.Release|Mixed Platforms.Build.0 = Release|x86 {B1784698-592E-4132-BDFA-9817409E3A96}.Release|x86.ActiveCfg = Release|x86 {B1784698-592E-4132-BDFA-9817409E3A96}.Release|x86.Build.0 = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Any CPU.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|Mixed Platforms.Build.0 = Debug|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.ActiveCfg = Debug|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|Any CPU.ActiveCfg = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|Mixed Platforms.Build.0 = Release|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.ActiveCfg = Release|x86 {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.Build.0 = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Any CPU.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|Mixed Platforms.Build.0 = Debug|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.ActiveCfg = Debug|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|Any CPU.ActiveCfg = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|Mixed Platforms.Build.0 = Release|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.ActiveCfg = Release|x86 {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.Build.0 = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Any CPU.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|Mixed Platforms.Build.0 = Debug|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.ActiveCfg = Debug|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Any CPU.ActiveCfg = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|Mixed Platforms.Build.0 = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Any CPU.ActiveCfg = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|Mixed Platforms.Build.0 = Debug|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.ActiveCfg = Debug|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.Build.0 = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Any CPU.ActiveCfg = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|Mixed Platforms.Build.0 = Release|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.ActiveCfg = Release|x86 {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.Build.0 = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Any CPU.ActiveCfg = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|Mixed Platforms.Build.0 = Debug|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.ActiveCfg = Debug|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.Build.0 = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Any CPU.ActiveCfg = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|Mixed Platforms.Build.0 = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.ActiveCfg = Release|x86 {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.Build.0 = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.ActiveCfg = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.Build.0 = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Any CPU.Build.0 = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.Build.0 = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.ActiveCfg = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.Build.0 = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Any CPU.Build.0 = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Any CPU.Build.0 = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Any CPU.Build.0 = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -182,6 +390,7 @@ Global {CBF6B8B0-A015-413A-8C86-01238BB45770} = {57A04B72-8088-4F75-A582-1158CF8291F7} {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB} = {57A04B72-8088-4F75-A582-1158CF8291F7} {CC26800D-F67E-464B-88DE-8EB1A0C227A3} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {4EACDBBC-BCD7-4765-A57B-3E08331E4749} = {57A04B72-8088-4F75-A582-1158CF8291F7} {FAFB5948-A222-4CF6-AD14-026BE7564802} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} {CADDFCE0-7509-4430-8364-2074E1EEFCA2} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} {6BCE712F-846D-4846-9D1B-A66B858DA755} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} @@ -192,6 +401,10 @@ Global {1B9A82C4-BCA1-4834-A33E-226F17BE070B} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} {2B8C6DAD-4D85-41B1-83FD-248D9F347522} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {911284D3-F130-459E-836C-2430B6FBF21D} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {15AD7579-A314-4626-B556-663F51D97CD1} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} + {40D72824-7D02-4A77-9106-8FE0EEA2B997} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 From 5459b5fed4e47417e62f3eab7160c53f67bc7fe5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 14 Jan 2014 11:21:20 -0800 Subject: [PATCH 06/37] New: Setting file permissions on import (Linux) --- build.ps1 | 3 + src/NzbDrone.Common/Disk/DiskProviderBase.cs | 4 +- src/NzbDrone.Common/Disk/IDiskProvider.cs | 8 ++- .../Configuration/ConfigService.cs | 16 ++++- .../Configuration/IConfigService.cs | 2 + .../MediaFiles/EpisodeFileMovingService.cs | 34 ++++++++++- .../Specifications/NotInUseSpecification.cs | 1 - .../MediaFiles/SetMediaFilePermissions.cs | 60 +++++++++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Mono/DiskProvider.cs | 4 +- src/NzbDrone.Windows/DiskProvider.cs | 5 +- 11 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs diff --git a/build.ps1 b/build.ps1 index 639b28320..e7c323dcb 100644 --- a/build.ps1 +++ b/build.ps1 @@ -27,6 +27,9 @@ Function Build() AddJsonNet + Write-Host "Removing Mono.Posix.dll" + Remove-Item "$outputFolder\Mono.Posix.dll" + Write-Host "##teamcity[progressFinish 'Build']" } diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index a6242f6ef..14ce5f4fa 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -1,12 +1,10 @@ using System; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using NLog; using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Common.Disk @@ -23,7 +21,7 @@ namespace NzbDrone.Common.Disk public abstract long? GetAvailableSpace(string path); public abstract void InheritFolderPermissions(string filename); - public abstract void SetFilePermissions(string path, string mask); + public abstract void SetPermissions(string path, string mask); public abstract long? GetTotalSize(string path); public DateTime GetLastFolderWrite(string path) diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index a5a635dc2..5426c18db 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -13,6 +13,11 @@ namespace NzbDrone.Common.Disk { public interface IDiskProvider { + long? GetAvailableSpace(string path); + void InheritFolderPermissions(string filename); + void SetPermissions(string path, string mask); + long? GetTotalSize(string path); + DateTime GetLastFolderWrite(string path); DateTime GetLastFileWrite(string path); void EnsureFolder(string path); @@ -29,8 +34,6 @@ namespace NzbDrone.Common.Disk void DeleteFile(string path); void MoveFile(string source, string destination); void DeleteFolder(string path, bool recursive); - void InheritFolderPermissions(string filename); - long? GetAvailableSpace(string path); string ReadAllText(string filePath); void WriteAllText(string filename, string contents); void FileSetLastWriteTimeUtc(string path, DateTime dateTime); @@ -44,7 +47,6 @@ namespace NzbDrone.Common.Disk FileAttributes GetFileAttributes(string path); void EmptyFolder(string path); string[] GetFixedDrives(); - long? GetTotalSize(string path); string GetVolumeLabel(string path); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 3b66cad13..1427f998f 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -278,12 +278,26 @@ namespace NzbDrone.Core.Configuration set { SetValue("CreateEmptySeriesFolders", value); } } - public string DownloadClientWorkingFolders + public String DownloadClientWorkingFolders { get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); } set { SetValue("DownloadClientWorkingFolders", value); } } + public String FileChmod + { + get { return GetValue("FileChmod", "0755"); } + + set { SetValue("FileChmod", value); } + } + + public String FolderChmod + { + get { return GetValue("FolderChmod", "0755"); } + + set { SetValue("FolderChmod", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 60f98aada..70982dedd 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -42,5 +42,7 @@ namespace NzbDrone.Core.Configuration Boolean EnableFailedDownloadHandling { get; set; } Boolean CreateEmptySeriesFolders { get; set; } void SaveValues(Dictionary configValues); + String FileChmod { get; set; } + String FolderChmod { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 60bd7ed13..626cdb020 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -6,7 +6,7 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -24,16 +24,19 @@ namespace NzbDrone.Core.MediaFiles private readonly IEpisodeService _episodeService; private readonly IBuildFileNames _buildFileNames; private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; private readonly Logger _logger; public EpisodeFileMovingService(IEpisodeService episodeService, IBuildFileNames buildFileNames, IDiskProvider diskProvider, + IConfigService configService, Logger logger) { _episodeService = episodeService; _buildFileNames = buildFileNames; _diskProvider = diskProvider; + _configService = configService; _logger = logger; } @@ -85,6 +88,7 @@ namespace NzbDrone.Core.MediaFiles { _logger.Trace("Setting last write time on series folder: {0}", series.Path); _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded); + SetFolderPermissions(series.Path); if (series.SeasonFolder) { @@ -92,6 +96,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); + SetFolderPermissions(seasonFolder); } } @@ -122,6 +127,33 @@ namespace NzbDrone.Core.MediaFiles } } } + + else + { + SetPermissions(destinationFilename, _configService.FileChmod); + } + } + + private void SetPermissions(string path, string permissions) + { + try + { + _diskProvider.SetPermissions(path, permissions); + } + + catch (Exception ex) + { + if (ex is UnauthorizedAccessException || ex is InvalidOperationException) + { + _logger.Debug("Unable to apply permissions to: ", path); + _logger.TraceException(ex.Message, ex); + } + } + } + + private void SetFolderPermissions(string path) + { + SetPermissions(path, _configService.FolderChmod); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs index b27b8e89d..ce9ee9ff5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs @@ -1,5 +1,4 @@ using NLog; -using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs b/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs new file mode 100644 index 000000000..1b379db76 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Core.MediaFiles +{ + public interface ISetMediaFilePermissions + { + void SetPermissions(string filename); + } + + public class SetMediaFilePermissions : ISetMediaFilePermissions + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public SetMediaFilePermissions(IDiskProvider diskProvider, IConfigService configService, Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _logger = logger; + } + + public void SetPermissions(string filename) + { + if (OsInfo.IsWindows) + { + //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. + try + { + _diskProvider.InheritFolderPermissions(filename); + } + catch (Exception ex) + { + if (ex is UnauthorizedAccessException || ex is InvalidOperationException) + { + _logger.Debug("Unable to apply folder permissions to: ", filename); + _logger.TraceException(ex.Message, ex); + } + + else + { + throw; + } + } + } + + else + { + _diskProvider.SetPermissions(filename, _configService.FileChmod); + } + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5dbf4fd48..894868c1c 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -290,6 +290,7 @@ + diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs index 58c6f7d20..dedfe57b3 100644 --- a/src/NzbDrone.Mono/DiskProvider.cs +++ b/src/NzbDrone.Mono/DiskProvider.cs @@ -49,8 +49,10 @@ namespace NzbDrone.Mono } } - public override void SetFilePermissions(string path, string mask) + public override void SetPermissions(string path, string mask) { + Logger.Trace("Setting permissions: {0} on {1}", mask, path); + var filePermissions = NativeConvert.FromOctalPermissionString(mask); if (Syscall.chmod(path, filePermissions) < 0) diff --git a/src/NzbDrone.Windows/DiskProvider.cs b/src/NzbDrone.Windows/DiskProvider.cs index 27b9debc4..6eb2863f2 100644 --- a/src/NzbDrone.Windows/DiskProvider.cs +++ b/src/NzbDrone.Windows/DiskProvider.cs @@ -4,7 +4,6 @@ using System.Runtime.InteropServices; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; namespace NzbDrone.Windows @@ -41,9 +40,9 @@ namespace NzbDrone.Windows File.SetAccessControl(filename, fs); } - public override void SetFilePermissions(string path, string mask) + public override void SetPermissions(string path, string mask) { - throw new NotImplementedException(); + } public override long? GetTotalSize(string path) From 49168cad25f0bda75998db4e78032eb95c991313 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jan 2014 00:57:14 -0800 Subject: [PATCH 07/37] UI and opt-in for setting permissions --- .../Configuration/ConfigService.cs | 9 ++- .../Configuration/IConfigService.cs | 1 + .../MediaFiles/EpisodeFileMovingService.cs | 20 ++++++- .../MediaFiles/SetMediaFilePermissions.cs | 60 ------------------- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 - src/NzbDrone.Mono/DiskProvider.cs | 7 +++ src/UI/Handlebars/Helpers/Os.js | 9 +++ .../backbone.marionette.templates.js | 1 + .../MediaManagement/MediaManagementLayout.js | 9 ++- .../MediaManagementLayoutTemplate.html | 1 + .../Permissions/PermissionsView.js | 39 ++++++++++++ .../Permissions/PermissionsViewTemplate.html | 48 +++++++++++++++ 12 files changed, 137 insertions(+), 68 deletions(-) delete mode 100644 src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs create mode 100644 src/UI/Handlebars/Helpers/Os.js create mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsView.js create mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 1427f998f..17b5eba03 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -284,9 +284,16 @@ namespace NzbDrone.Core.Configuration set { SetValue("DownloadClientWorkingFolders", value); } } + public Boolean SetPermissionsLinux + { + get { return GetValueBoolean("SetPermissionsLinux", false); } + + set { SetValue("SetPermissionsLinux", value); } + } + public String FileChmod { - get { return GetValue("FileChmod", "0755"); } + get { return GetValue("FileChmod", "0644"); } set { SetValue("FileChmod", value); } } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 70982dedd..d70d36def 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -42,6 +42,7 @@ namespace NzbDrone.Core.Configuration Boolean EnableFailedDownloadHandling { get; set; } Boolean CreateEmptySeriesFolders { get; set; } void SaveValues(Dictionary configValues); + Boolean SetPermissionsLinux { get; set; } String FileChmod { get; set; } String FolderChmod { get; set; } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 626cdb020..a7921453b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -79,7 +79,18 @@ namespace NzbDrone.Core.MediaFiles throw new SameFilenameException("File not moved, source and destination are the same", episodeFile.Path); } - _diskProvider.CreateFolder(new FileInfo(destinationFilename).DirectoryName); + var directoryName = new FileInfo(destinationFilename).DirectoryName; + + if (_diskProvider.FolderExists(directoryName)) + { + _diskProvider.CreateFolder(directoryName); + SetFolderPermissions(directoryName); + + if (!directoryName.PathEquals(series.Path)) + { + SetFolderPermissions(series.Path); + } + } _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); _diskProvider.MoveFile(episodeFile.Path, destinationFilename); @@ -88,7 +99,6 @@ namespace NzbDrone.Core.MediaFiles { _logger.Trace("Setting last write time on series folder: {0}", series.Path); _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded); - SetFolderPermissions(series.Path); if (series.SeasonFolder) { @@ -96,7 +106,6 @@ namespace NzbDrone.Core.MediaFiles _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); - SetFolderPermissions(seasonFolder); } } @@ -136,6 +145,11 @@ namespace NzbDrone.Core.MediaFiles private void SetPermissions(string path, string permissions) { + if (!_configService.SetPermissionsLinux) + { + return; + } + try { _diskProvider.SetPermissions(path, permissions); diff --git a/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs b/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs deleted file mode 100644 index 1b379db76..000000000 --- a/src/NzbDrone.Core/MediaFiles/SetMediaFilePermissions.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.MediaFiles -{ - public interface ISetMediaFilePermissions - { - void SetPermissions(string filename); - } - - public class SetMediaFilePermissions : ISetMediaFilePermissions - { - private readonly IDiskProvider _diskProvider; - private readonly IConfigService _configService; - private readonly Logger _logger; - - public SetMediaFilePermissions(IDiskProvider diskProvider, IConfigService configService, Logger logger) - { - _diskProvider = diskProvider; - _configService = configService; - _logger = logger; - } - - public void SetPermissions(string filename) - { - if (OsInfo.IsWindows) - { - //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. - try - { - _diskProvider.InheritFolderPermissions(filename); - } - catch (Exception ex) - { - if (ex is UnauthorizedAccessException || ex is InvalidOperationException) - { - _logger.Debug("Unable to apply folder permissions to: ", filename); - _logger.TraceException(ex.Message, ex); - } - - else - { - throw; - } - } - } - - else - { - _diskProvider.SetPermissions(filename, _configService.FileChmod); - } - } - } -} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 894868c1c..5dbf4fd48 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -290,7 +290,6 @@ - diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs index dedfe57b3..7582eb7c9 100644 --- a/src/NzbDrone.Mono/DiskProvider.cs +++ b/src/NzbDrone.Mono/DiskProvider.cs @@ -61,6 +61,13 @@ namespace NzbDrone.Mono throw new Exception("Error setting file permissions: " + error); } + + if (Syscall.chown(path, Syscall.getuid(), Syscall.getgid()) < 0) + { + var error = Stdlib.GetLastError(); + + throw new Exception("Error setting file owner: " + error); + } } public override long? GetTotalSize(string path) diff --git a/src/UI/Handlebars/Helpers/Os.js b/src/UI/Handlebars/Helpers/Os.js new file mode 100644 index 000000000..0d46f7391 --- /dev/null +++ b/src/UI/Handlebars/Helpers/Os.js @@ -0,0 +1,9 @@ +'use strict'; +define( + [ + 'handlebars' + ], function (Handlebars) { + Handlebars.registerHelper('LinuxOnly', function () { + return new Handlebars.SafeString(''); + }); + }); diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js index 2c589a5cb..f849da4f1 100644 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ b/src/UI/Handlebars/backbone.marionette.templates.js @@ -10,6 +10,7 @@ define( 'Handlebars/Helpers/Series', 'Handlebars/Helpers/Quality', 'Handlebars/Helpers/System', + 'Handlebars/Helpers/Os', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayout.js b/src/UI/Settings/MediaManagement/MediaManagementLayout.js index 56b34c814..3b5ac123e 100644 --- a/src/UI/Settings/MediaManagement/MediaManagementLayout.js +++ b/src/UI/Settings/MediaManagement/MediaManagementLayout.js @@ -5,15 +5,17 @@ define( 'marionette', 'Settings/MediaManagement/Naming/NamingView', 'Settings/MediaManagement/Sorting/View', - 'Settings/MediaManagement/FileManagement/FileManagementView' - ], function (Marionette, NamingView, SortingView, FileManagementView) { + 'Settings/MediaManagement/FileManagement/FileManagementView', + 'Settings/MediaManagement/Permissions/PermissionsView' + ], function (Marionette, NamingView, SortingView, FileManagementView, PermissionsView) { return Marionette.Layout.extend({ template: 'Settings/MediaManagement/MediaManagementLayoutTemplate', regions: { episodeNaming : '#episode-naming', sorting : '#sorting', - fileManagement : '#file-management' + fileManagement : '#file-management', + permissions : '#permissions' }, initialize: function (options) { @@ -25,6 +27,7 @@ define( this.episodeNaming.show(new NamingView({ model: this.namingSettings })); this.sorting.show(new SortingView({ model: this.settings })); this.fileManagement.show(new FileManagementView({ model: this.settings })); + this.permissions.show(new PermissionsView({ model: this.settings })); } }); }); diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html index 4720aa606..05a416998 100644 --- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html +++ b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html @@ -2,4 +2,5 @@
+
\ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js new file mode 100644 index 000000000..e1a098106 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js @@ -0,0 +1,39 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AutoComplete' + ], function (Marionette, AsModelBoundView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/MediaManagement/Permissions/PermissionsViewTemplate', + + ui: { + recyclingBin : '.x-path', + failedDownloadHandlingCheckbox: '.x-failed-download-handling', + failedDownloadOptions : '.x-failed-download-options' + }, + + events: { + 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' + }, + + onShow: function () { + this.ui.recyclingBin.autoComplete('/directories'); + }, + + _setFailedDownloadOptionsVisibility: function () { + var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.failedDownloadOptions.slideDown(); + } + + else { + this.ui.failedDownloadOptions.slideUp(); + } + } + }); + + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html new file mode 100644 index 000000000..9528724e6 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -0,0 +1,48 @@ +
+ Permissions + +
+ + +
+
+ +
+ + +
+ + + {{LinuxOnly}} + + +
+
+ +
+ + +
+ + + {{LinuxOnly}} + + +
+
+
\ No newline at end of file From 25ae19bf804dd9fbc70d9a4d72fd4f5f3dd21d82 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jan 2014 14:16:12 -0800 Subject: [PATCH 08/37] Hiding permissions when OS is windows --- src/UI/Handlebars/Helpers/Os.js | 9 --------- src/UI/Handlebars/Helpers/System.js | 2 +- src/UI/Handlebars/backbone.marionette.templates.js | 1 - .../Permissions/PermissionsViewTemplate.html | 9 ++++----- 4 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 src/UI/Handlebars/Helpers/Os.js diff --git a/src/UI/Handlebars/Helpers/Os.js b/src/UI/Handlebars/Helpers/Os.js deleted file mode 100644 index 0d46f7391..000000000 --- a/src/UI/Handlebars/Helpers/Os.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; -define( - [ - 'handlebars' - ], function (Handlebars) { - Handlebars.registerHelper('LinuxOnly', function () { - return new Handlebars.SafeString(''); - }); - }); diff --git a/src/UI/Handlebars/Helpers/System.js b/src/UI/Handlebars/Helpers/System.js index f5517db84..13cead2f7 100644 --- a/src/UI/Handlebars/Helpers/System.js +++ b/src/UI/Handlebars/Helpers/System.js @@ -19,7 +19,7 @@ define( if (StatusModel.get('isLinux')) { return options.fn(this); - } + } return options.inverse(this); }); diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js index f849da4f1..2c589a5cb 100644 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ b/src/UI/Handlebars/backbone.marionette.templates.js @@ -10,7 +10,6 @@ define( 'Handlebars/Helpers/Series', 'Handlebars/Helpers/Quality', 'Handlebars/Helpers/System', - 'Handlebars/Helpers/Os', 'Handlebars/Handlebars.Debug' ], function (Templates) { return function () { diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html index 9528724e6..1c6a0c81e 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -1,4 +1,5 @@ -
+{{#if_linux}} +
Permissions
@@ -16,7 +17,6 @@ - {{LinuxOnly}}
@@ -28,7 +28,6 @@
- {{LinuxOnly}}
@@ -40,9 +39,9 @@
- {{LinuxOnly}}
-
\ No newline at end of file +
+{{/if_linux}} \ No newline at end of file From 543cc4e6c1777517f9ca28dd0023909a00029722 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jan 2014 14:58:12 -0800 Subject: [PATCH 09/37] If folder does not exist... --- src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index a7921453b..4ef693ae9 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -81,7 +81,7 @@ namespace NzbDrone.Core.MediaFiles var directoryName = new FileInfo(destinationFilename).DirectoryName; - if (_diskProvider.FolderExists(directoryName)) + if (!_diskProvider.FolderExists(directoryName)) { _diskProvider.CreateFolder(directoryName); SetFolderPermissions(directoryName); From 24430aef0706d270359ee8d96b145fe4d2a6066c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jan 2014 15:12:06 -0800 Subject: [PATCH 10/37] Use proper path for episode file moving --- .../EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs index b0a6db570..00b1302ec 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs @@ -25,12 +25,16 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests [SetUp] public void Setup() { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\Series") + .Build(); + _episodeFile = Builder.CreateNew() .With(f => f.Path = @"C:\Test\File.avi") .Build(); _localEpisode = Builder.CreateNew() - .With(l => l.Series = Builder.CreateNew().Build()) + .With(l => l.Series = _series) .With(l => l.Episodes = Builder.CreateListOfSize(1).Build().ToList()) .Build(); From 47a8d93c18e80c90230419bae98a33a259436c60 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 26 Jan 2014 17:08:28 -0800 Subject: [PATCH 11/37] Added .ogv as a file extension --- src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index 9416e4d4d..1876a4a56 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.MediaFiles { ".asf", Quality.SDTV }, { ".asx", Quality.SDTV }, { ".ogm", Quality.SDTV }, + { ".ogv", Quality.SDTV }, { ".m2v", Quality.SDTV }, { ".avi", Quality.SDTV }, { ".bin", Quality.SDTV }, From 63bf00ed41e6973c732c1dce06ca0924c39d18af Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 14:10:37 -0800 Subject: [PATCH 12/37] Added CLA and CONTRIBUTING.md files --- CLA.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 39 ++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 CLA.md create mode 100644 CONTRIBUTING.md diff --git a/CLA.md b/CLA.md new file mode 100644 index 000000000..3198fdb1e --- /dev/null +++ b/CLA.md @@ -0,0 +1,68 @@ +# NzbDrone Individual Contributor License Agreement # + +Thank you for your interest in contributing to NzbDrone ("We" or "Us"). +This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please complete the form below. This is a legally binding document, so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us. + +## 1. Definitions ## + +"You" means the individual who Submits a Contribution to Us. +"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please follow the instructions in . +"Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. +"Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. +"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." +"Submission Date" means the date on which You Submit a Contribution to Us. +"Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier. + +## 2. Grant of Rights ## + +### 2.1 Copyright License ### +(a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. +(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. + +### 2.2 Patent License ### +For patent claims including, without limitation, method, process, and apparatus claims which You own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. + +### 2.3 Outbound License ### +As a condition on the grant of rights in Sections 2.1 and 2.2, We agree to license the Contribution only under the terms of the license or licenses which We are using on the Submission Date for the Material (including any rights to adopt any future version of a license if permitted). + +### 2.4 Moral Rights. ### +If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. + +### 2.5 Our Rights. ### +You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. + +### 2.6 Reservation of Rights. ### +Any rights not expressly licensed under this section are expressly reserved by You. + +## 3. Agreement ## + +You confirm that: +(a) You have the legal authority to enter into this Agreement. +(b) You own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. +(c) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. +(d) You have followed the instructions in , if You do not own the Copyright in the entire work of authorship Submitted. + +## 4. Disclaimer ## + +EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. + +## 5. Consequential Damage Waiver ## + +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. + +## 6. Miscellaneous ## + +### 6.1 ### +This Agreement will be governed by and construed in accordance with the laws of excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement. + +### 6.2 ### +This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. + +### 6.3 ### +If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. + +### 6.4 ### +The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. + +### 6.5 ### +If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..465170559 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# How to Contribute # + +We're always looking for people to help make NzbDrone even better, there are a number of ways to contribute. To get started, sign the Contributor License Agreement. + +## Documentation ## +Setup guides, FAQ, the more information we have on the wiki the better. + +## Development ## + +### Tools required ### +- Visual Studio 2012 +- HTML/Javascript editor of choice (Sublime Text/Webstorm/etc) +- npm (node package manager) +- git + +### Getting started ### + +1. Fork NzbDrone +2. Clone (develop branch) +3. Run `npm install` +4. Run `grunt` - Used to compile the UI components and copy them (leave this window open) +5. Compile in Visual Studio + +### Contributing Code ### +- If you're adding a new, already requested feature, please move it to In Progress on [Trello](http://trello.nzbdrone.com "Trello") so work is not duplicated. +- Rebase from NzbDrone's develop branch, don't merge +- Make meaningful commits, or squash them +- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements +- Reach out to us on the forums or on IRC if you have any questions +- Add tests (unit/integration) +- Commit with *nix line endings for consistency (We checkout Windows and commit *nix) +- Try to stick to one feature per request to keep things clean and easy to understand +- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge) + +### Pull Requesting ### +- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability +- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it + +If you have any questions about any of this, please let us know. \ No newline at end of file From 1cae7293e767a215ccd73167d44b95bdf4cf4c6f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 27 Jan 2014 14:38:45 -0800 Subject: [PATCH 13/37] Adding NzbDrone.Windows to update package --- build.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.ps1 b/build.ps1 index e7c323dcb..bf0f95d25 100644 --- a/build.ps1 +++ b/build.ps1 @@ -4,6 +4,7 @@ $outputFolderMono = '.\_output_mono' $testPackageFolder = '.\_tests\' $testSearchPattern = '*.Test\bin\x86\Release' $sourceFolder = '.\src' +$updateFolder = $outputFolder + '\NzbDrone.Update' Function Build() { @@ -178,6 +179,9 @@ Function CleanupWindowsPackage() { Write-Host Removing NzbDrone.Mono get-childitem $outputFolder -File -Filter NzbDrone.Mono.* -Recurse | foreach ($_) {remove-item $_.fullname} + + Write-Host Adding NzbDrone.Windows to UpdatePackage + Copy-Item $outputFolder\* $updateFolder -Filter NzbDrone.Windows.* } Build From cc660779438723a2d3b12e35459a9bbba3c9246b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 29 Jan 2014 21:52:50 -0800 Subject: [PATCH 14/37] Cancelling adding an indexer will stop listening to save event Fixed: Cancelling adding indexer will not break saving --- src/UI/Settings/Indexers/EditTemplate.html | 4 ++-- src/UI/Settings/Indexers/EditView.js | 10 +++++++++- src/UI/Settings/SettingsModelBase.js | 6 +++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/UI/Settings/Indexers/EditTemplate.html b/src/UI/Settings/Indexers/EditTemplate.html index f9c3779ca..0bf5bdc06 100644 --- a/src/UI/Settings/Indexers/EditTemplate.html +++ b/src/UI/Settings/Indexers/EditTemplate.html @@ -1,5 +1,5 @@  + +
+ + +
+ + + + +
+
+ +
+ + +
+ + + + +
+
{{/if_linux}} \ No newline at end of file From cf57188741934f2be10239afc4a99877fd23f783 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 30 Jan 2014 22:23:48 -0800 Subject: [PATCH 23/37] Added sourceTitle to failed downloads --- src/UI/History/Details/HistoryDetailsViewTemplate.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/UI/History/Details/HistoryDetailsViewTemplate.html b/src/UI/History/Details/HistoryDetailsViewTemplate.html index 9e2a1433e..9c313a9ae 100644 --- a/src/UI/History/Details/HistoryDetailsViewTemplate.html +++ b/src/UI/History/Details/HistoryDetailsViewTemplate.html @@ -36,6 +36,9 @@ {{/if_eq}} {{#if_eq eventType compare="downloadFailed"}}
+
Source Title
+
{{sourceTitle}}
+ {{#with data}}
Message
{{message}}
From caf6f86573747a22ecc848710feb8898c7874da6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 31 Jan 2014 08:11:19 -0800 Subject: [PATCH 24/37] Do not try to chown when user or group is blank --- src/NzbDrone.Mono/DiskProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs index f09ae650e..cfbaa98c4 100644 --- a/src/NzbDrone.Mono/DiskProvider.cs +++ b/src/NzbDrone.Mono/DiskProvider.cs @@ -62,6 +62,12 @@ namespace NzbDrone.Mono throw new LinuxPermissionsException("Error setting file permissions: " + error); } + if (String.IsNullOrWhiteSpace(user) || String.IsNullOrWhiteSpace(group)) + { + Logger.Trace("User or Group for chown not configured, skipping chown."); + return; + } + uint userId; uint groupId; From c84bd6f21f1b807ac5bbd2e4fec4d09eb4124231 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 31 Jan 2014 08:17:50 -0800 Subject: [PATCH 25/37] Fixed chown config keys --- src/NzbDrone.Core/Configuration/ConfigService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index e0a026835..6ebe823ff 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -307,16 +307,16 @@ namespace NzbDrone.Core.Configuration public String ChownUser { - get { return GetValue("User", ""); } + get { return GetValue("ChownUser", ""); } - set { SetValue("User", value); } + set { SetValue("ChownUser", value); } } public String ChownGroup { - get { return GetValue("User", ""); } + get { return GetValue("ChownGroup", ""); } - set { SetValue("User", value); } + set { SetValue("ChownGroup", value); } } private string GetValue(string key) From d80d89e687094c4c3430376d76c9b8e0df631abf Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 31 Jan 2014 17:47:59 -0800 Subject: [PATCH 26/37] Calendar and series details show download progress --- src/UI/Calendar/CalendarView.js | 17 +++++++++++++++ src/UI/Calendar/calendar.less | 12 ++++++++--- src/UI/Cells/EpisodeStatusCell.js | 10 ++++++++- src/UI/Cells/cells.less | 6 ++++++ src/UI/Content/progress-bars.less | 36 +++++++++++++++++++++++++++++++ src/UI/Content/theme.less | 30 +------------------------- src/UI/Content/variables.less | 3 ++- 7 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 src/UI/Content/progress-bars.less diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index 702a0a73c..e687abc90 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -43,6 +43,12 @@ define( eventRender : function (event, element) { self.$(element).addClass(event.statusLevel); self.$(element).children('.fc-event-inner').addClass(event.statusLevel); + + if (event.progress > 0) { + self.$(element).find('.fc-event-time') + .after('{0}%' + .format(event.progress.toFixed(0))); + } }, eventClick : function (event) { vent.trigger(vent.Commands.ShowEpisodeDetails, {episode: event.model}); @@ -85,6 +91,7 @@ define( end : end, allDay : false, statusLevel : _instance._getStatusLevel(model, end), + progress : _instance._getDownloadProgress(model), model : model }; @@ -129,6 +136,16 @@ define( _reloadCalendarEvents: function () { this.$el.fullCalendar('removeEvents'); this._setEventData(this.collection); + }, + + _getDownloadProgress: function (element) { + var downloading = QueueCollection.findEpisode(element.get('id')); + + if (!downloading) { + return 0; + } + + return 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); } }); }); diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 50c84e0b2..29d5a48bd 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -2,6 +2,7 @@ @import "../Content/Bootstrap/variables"; @import "../Content/Bootstrap/buttons"; @import "../Shared/Styles/clickable"; +@import "../Content/variables"; .calendar { th, td { @@ -100,7 +101,7 @@ } .purple { - border-color : #7932ea; + border-color : @nzbdronePurple; } .episode-title { @@ -148,7 +149,12 @@ } .purple { - border-color : #7932ea; - background-color : #7932ea; + border-color : @nzbdronePurple; + background-color : @nzbdronePurple; + } + + .downloading-progress { + margin-right: 3px; + font-size: 11px; } } diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index 8a7fc89ac..d8ab15259 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -57,7 +57,15 @@ define( var model = this.model; var downloading = QueueCollection.findEpisode(model.get('id')); - if (downloading || this.model.get('downloading')) { + if (downloading) { + var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); + + this.$el.html('
'.format(progress.toFixed(1)) + + '
'.format(progress)); + return; + } + + else if (this.model.get('downloading')) { icon = 'icon-nd-downloading'; tooltip = 'Episode is downloading'; } diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index b2ba4d140..8869f0779 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -51,6 +51,12 @@ td.episode-status-cell, td.quality-cell { .badge { font-size: 9px; } + + .progress { + height : 10px; + margin-top : 5px; + margin-bottom : 0px; + } } .history-details-cell { diff --git a/src/UI/Content/progress-bars.less b/src/UI/Content/progress-bars.less new file mode 100644 index 000000000..40af0839b --- /dev/null +++ b/src/UI/Content/progress-bars.less @@ -0,0 +1,36 @@ +@import "Bootstrap/mixins"; +@import "Bootstrap/variables"; +@import "variables"; + +.progress.episode-progress { + width : 125px; + position : relative; + margin-bottom : 2px; + + .progressbar-back-text, .progressbar-front-text { + font-size : 11.844px; + font-weight : bold; + text-align : center; + cursor : default; + } + + .progressbar-back-text { + position : absolute; + width : 100%; + height : 100%; + } + + .progressbar-front-text { + display : block; + width : 125px; + } + + .bar { + position : absolute; + overflow : hidden; + } +} + +.progress-purple .bar, .progress .bar-purple { + #gradient > .vertical(@purple, @nzbdronePurple); +} \ No newline at end of file diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 4e72e020b..5f1dd6725 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -10,39 +10,11 @@ @import "checkbox-button"; @import "spinner"; @import "legend"; +@import "progress-bars"; @import "../Shared/Styles/clickable"; @import "../Shared/Styles/card"; @import "../Rename/rename"; -.progress.episode-progress { - width : 125px; - position : relative; - margin-bottom : 2px; - - .progressbar-back-text, .progressbar-front-text { - font-size : 11.844px; - font-weight : bold; - text-align : center; - cursor : default; - } - - .progressbar-back-text { - position : absolute; - width : 100%; - height : 100%; - } - - .progressbar-front-text { - display : block; - width : 125px; - } - - .bar { - position : absolute; - overflow : hidden; - } -} - .page-toolbar { margin-top : 10px; margin-bottom : 30px; diff --git a/src/UI/Content/variables.less b/src/UI/Content/variables.less index 8a29e0a50..0495a3c19 100644 --- a/src/UI/Content/variables.less +++ b/src/UI/Content/variables.less @@ -1 +1,2 @@ -@nzbdroneRed: #C4273C; +@nzbdroneRed: #c4273c; +@nzbdronePurple: #7932ea; \ No newline at end of file From 953e59d7e372b5b4ecd36e6cc09a40ae2cb8dc8f Mon Sep 17 00:00:00 2001 From: markus101 Date: Fri, 31 Jan 2014 22:05:16 -0800 Subject: [PATCH 27/37] New: Progress bar on series details/calendar when episode is downloading --- src/NzbDrone.sln.DotSettings | 1 + src/UI/Calendar/CalendarView.js | 16 +- src/UI/Calendar/calendar.less | 6 +- src/UI/Content/progress-bars.less | 2 +- src/UI/JsLibraries/jquery.easypiechart.js | 357 ++++++++++++++++++++++ src/UI/app.js | 7 + 6 files changed, 382 insertions(+), 7 deletions(-) create mode 100644 src/UI/JsLibraries/jquery.easypiechart.js diff --git a/src/NzbDrone.sln.DotSettings b/src/NzbDrone.sln.DotSettings index e67821bf5..fdd2a0b42 100644 --- a/src/NzbDrone.sln.DotSettings +++ b/src/NzbDrone.sln.DotSettings @@ -61,6 +61,7 @@ True 2 False + True True True True diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index e687abc90..c21e0d488 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -9,7 +9,8 @@ define( 'System/StatusModel', 'History/Queue/QueueCollection', 'Mixins/backbone.signalr.mixin', - 'fullcalendar' + 'fullcalendar', + 'jquery.easypiechart' ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) { var _instance; @@ -18,6 +19,7 @@ define( initialize: function () { this.collection = new CalendarCollection().bindSignalR({ updateOnly: true }); this.listenTo(this.collection, 'change', this._reloadCalendarEvents); + this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); }, render : function () { @@ -46,8 +48,16 @@ define( if (event.progress > 0) { self.$(element).find('.fc-event-time') - .after('{0}%' - .format(event.progress.toFixed(0))); + .after(''.format(event.progress)); + + self.$(element).find('.chart').easyPieChart({ + barColor : '#ffffff', + trackColor: false, + scaleColor: false, + lineWidth : 2, + size : 14, + animate : false + }); } }, eventClick : function (event) { diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 29d5a48bd..39a5e284a 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -153,8 +153,8 @@ background-color : @nzbdronePurple; } - .downloading-progress { - margin-right: 3px; - font-size: 11px; + .chart { + margin-top : 2px; + margin-right: 2px; } } diff --git a/src/UI/Content/progress-bars.less b/src/UI/Content/progress-bars.less index 40af0839b..20b69c063 100644 --- a/src/UI/Content/progress-bars.less +++ b/src/UI/Content/progress-bars.less @@ -33,4 +33,4 @@ .progress-purple .bar, .progress .bar-purple { #gradient > .vertical(@purple, @nzbdronePurple); -} \ No newline at end of file +} diff --git a/src/UI/JsLibraries/jquery.easypiechart.js b/src/UI/JsLibraries/jquery.easypiechart.js new file mode 100644 index 000000000..c600fb85f --- /dev/null +++ b/src/UI/JsLibraries/jquery.easypiechart.js @@ -0,0 +1,357 @@ +/**! + * easyPieChart + * Lightweight plugin to render simple, animated and retina optimized pie charts + * + * @license + * @author Robert Fleischmann (http://robert-fleischmann.de) + * @version 2.1.3 + **/ + +(function(root, factory) { + if(typeof exports === 'object') { + module.exports = factory(require('jquery')); + } + else if(typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } + else { + factory(root.jQuery); + } +}(this, function($) { +/** + * Renderer to render the chart on a canvas object + * @param {DOMElement} el DOM element to host the canvas (root of the plugin) + * @param {object} options options object of the plugin + */ +var CanvasRenderer = function(el, options) { + var cachedBackground; + var canvas = document.createElement('canvas'); + + el.appendChild(canvas); + + if (typeof(G_vmlCanvasManager) !== 'undefined') { + G_vmlCanvasManager.initElement(canvas); + } + + var ctx = canvas.getContext('2d'); + + canvas.width = canvas.height = options.size; + + // canvas on retina devices + var scaleBy = 1; + if (window.devicePixelRatio > 1) { + scaleBy = window.devicePixelRatio; + canvas.style.width = canvas.style.height = [options.size, 'px'].join(''); + canvas.width = canvas.height = options.size * scaleBy; + ctx.scale(scaleBy, scaleBy); + } + + // move 0,0 coordinates to the center + ctx.translate(options.size / 2, options.size / 2); + + // rotate canvas -90deg + ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); + + var radius = (options.size - options.lineWidth) / 2; + if (options.scaleColor && options.scaleLength) { + radius -= options.scaleLength + 2; // 2 is the distance between scale and bar + } + + // IE polyfill for Date + Date.now = Date.now || function() { + return +(new Date()); + }; + + /** + * Draw a circle around the center of the canvas + * @param {strong} color Valid CSS color string + * @param {number} lineWidth Width of the line in px + * @param {number} percent Percentage to draw (float between -1 and 1) + */ + var drawCircle = function(color, lineWidth, percent) { + percent = Math.min(Math.max(-1, percent || 0), 1); + var isNegative = percent <= 0 ? true : false; + + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, isNegative); + + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + + ctx.stroke(); + }; + + /** + * Draw the scale of the chart + */ + var drawScale = function() { + var offset; + var length; + + ctx.lineWidth = 1; + ctx.fillStyle = options.scaleColor; + + ctx.save(); + for (var i = 24; i > 0; --i) { + if (i % 6 === 0) { + length = options.scaleLength; + offset = 0; + } else { + length = options.scaleLength * 0.6; + offset = options.scaleLength - length; + } + ctx.fillRect(-options.size/2 + offset, 0, length, 1); + ctx.rotate(Math.PI / 12); + } + ctx.restore(); + }; + + /** + * Request animation frame wrapper with polyfill + * @return {function} Request animation frame method or timeout fallback + */ + var reqAnimationFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + }; + }()); + + /** + * Draw the background of the plugin including the scale and the track + */ + var drawBackground = function() { + if(options.scaleColor) drawScale(); + if(options.trackColor) drawCircle(options.trackColor, options.lineWidth, 1); + }; + + /** + * Canvas accessor + */ + this.getCanvas = function() { + return canvas; + }; + + /** + * Canvas 2D context 'ctx' accessor + */ + this.getCtx = function() { + return ctx; + }; + + /** + * Clear the complete canvas + */ + this.clear = function() { + ctx.clearRect(options.size / -2, options.size / -2, options.size, options.size); + }; + + /** + * Draw the complete chart + * @param {number} percent Percent shown by the chart between -100 and 100 + */ + this.draw = function(percent) { + // do we need to render a background + if (!!options.scaleColor || !!options.trackColor) { + // getImageData and putImageData are supported + if (ctx.getImageData && ctx.putImageData) { + if (!cachedBackground) { + drawBackground(); + cachedBackground = ctx.getImageData(0, 0, options.size * scaleBy, options.size * scaleBy); + } else { + ctx.putImageData(cachedBackground, 0, 0); + } + } else { + this.clear(); + drawBackground(); + } + } else { + this.clear(); + } + + ctx.lineCap = options.lineCap; + + // if barcolor is a function execute it and pass the percent as a value + var color; + if (typeof(options.barColor) === 'function') { + color = options.barColor(percent); + } else { + color = options.barColor; + } + + // draw bar + drawCircle(color, options.lineWidth, percent / 100); + }.bind(this); + + /** + * Animate from some percent to some other percentage + * @param {number} from Starting percentage + * @param {number} to Final percentage + */ + this.animate = function(from, to) { + var startTime = Date.now(); + options.onStart(from, to); + var animation = function() { + var process = Math.min(Date.now() - startTime, options.animate.duration); + var currentValue = options.easing(this, process, from, to - from, options.animate.duration); + this.draw(currentValue); + options.onStep(from, to, currentValue); + if (process >= options.animate.duration) { + options.onStop(from, to); + } else { + reqAnimationFrame(animation); + } + }.bind(this); + + reqAnimationFrame(animation); + }.bind(this); +}; + +var EasyPieChart = function(el, opts) { + var defaultOptions = { + barColor: '#ef1e25', + trackColor: '#f9f9f9', + scaleColor: '#dfe0e0', + scaleLength: 5, + lineCap: 'round', + lineWidth: 3, + size: 110, + rotate: 0, + animate: { + duration: 1000, + enabled: true + }, + easing: function (x, t, b, c, d) { // more can be found here: http://gsgd.co.uk/sandbox/jquery/easing/ + t = t / (d/2); + if (t < 1) { + return c / 2 * t * t + b; + } + return -c/2 * ((--t)*(t-2) - 1) + b; + }, + onStart: function(from, to) { + return; + }, + onStep: function(from, to, currentValue) { + return; + }, + onStop: function(from, to) { + return; + } + }; + + // detect present renderer + if (typeof(CanvasRenderer) !== 'undefined') { + defaultOptions.renderer = CanvasRenderer; + } else if (typeof(SVGRenderer) !== 'undefined') { + defaultOptions.renderer = SVGRenderer; + } else { + throw new Error('Please load either the SVG- or the CanvasRenderer'); + } + + var options = {}; + var currentValue = 0; + + /** + * Initialize the plugin by creating the options object and initialize rendering + */ + var init = function() { + this.el = el; + this.options = options; + + // merge user options into default options + for (var i in defaultOptions) { + if (defaultOptions.hasOwnProperty(i)) { + options[i] = opts && typeof(opts[i]) !== 'undefined' ? opts[i] : defaultOptions[i]; + if (typeof(options[i]) === 'function') { + options[i] = options[i].bind(this); + } + } + } + + // check for jQuery easing + if (typeof(options.easing) === 'string' && typeof(jQuery) !== 'undefined' && jQuery.isFunction(jQuery.easing[options.easing])) { + options.easing = jQuery.easing[options.easing]; + } else { + options.easing = defaultOptions.easing; + } + + // process earlier animate option to avoid bc breaks + if (typeof(options.animate) === 'number') { + options.animate = { + duration: options.animate, + enabled: true + }; + } + + if (typeof(options.animate) === 'boolean' && !options.animate) { + options.animate = { + duration: 1000, + enabled: options.animate + }; + } + + // create renderer + this.renderer = new options.renderer(el, options); + + // initial draw + this.renderer.draw(currentValue); + + // initial update + if (el.dataset && el.dataset.percent) { + this.update(parseFloat(el.dataset.percent)); + } else if (el.getAttribute && el.getAttribute('data-percent')) { + this.update(parseFloat(el.getAttribute('data-percent'))); + } + }.bind(this); + + /** + * Update the value of the chart + * @param {number} newValue Number between 0 and 100 + * @return {object} Instance of the plugin for method chaining + */ + this.update = function(newValue) { + newValue = parseFloat(newValue); + if (options.animate.enabled) { + this.renderer.animate(currentValue, newValue); + } else { + this.renderer.draw(newValue); + } + currentValue = newValue; + return this; + }.bind(this); + + /** + * Disable animation + * @return {object} Instance of the plugin for method chaining + */ + this.disableAnimation = function() { + options.animate.enabled = false; + return this; + }; + + /** + * Enable animation + * @return {object} Instance of the plugin for method chaining + */ + this.enableAnimation = function() { + options.animate.enabled = true; + return this; + }; + + init(); +}; + +$.fn.easyPieChart = function(options) { + return this.each(function() { + var instanceOptions; + + if (!$.data(this, 'easyPieChart')) { + instanceOptions = $.extend({}, options, $(this).data()); + $.data(this, 'easyPieChart', new EasyPieChart(this, instanceOptions)); + } + }); +}; + +})); diff --git a/src/UI/app.js b/src/UI/app.js index 736bd875a..d8dde2b5a 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -21,6 +21,7 @@ require.config({ 'marionette' : 'JsLibraries/backbone.marionette', 'signalR' : 'JsLibraries/jquery.signalR', 'jquery.knob' : 'JsLibraries/jquery.knob', + 'jquery.easypiechart' : 'JsLibraries/jquery.easypiechart', 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', 'messenger' : 'JsLibraries/messenger', 'jquery' : 'JsLibraries/jquery', @@ -111,6 +112,12 @@ require.config({ 'jquery' ] }, + 'jquery.easypiechart' : { + deps: + [ + 'jquery' + ] + }, 'jquery.dotdotdot' : { deps: [ From 2cd347b829794e8dea3c3c699fb88aca93651455 Mon Sep 17 00:00:00 2001 From: markus101 Date: Sat, 1 Feb 2014 14:07:30 -0800 Subject: [PATCH 28/37] New: Blacklist added to UI (under history) --- src/NzbDrone.Api/Blacklist/BlacklistModule.cs | 42 ++++++ .../Blacklist/BlacklistResource.cs | 16 +++ src/NzbDrone.Api/NzbDrone.Api.csproj | 2 + .../Blacklisting/BlacklistService.cs | 21 ++- .../Blacklisting/ClearBlacklistCommand.cs | 15 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../History/Blacklist/BlacklistActionsCell.js | 26 ++++ .../History/Blacklist/BlacklistCollection.js | 44 ++++++ src/UI/History/Blacklist/BlacklistLayout.js | 132 ++++++++++++++++++ .../Blacklist/BlacklistLayoutTemplate.html | 11 ++ src/UI/History/Blacklist/BlacklistModel.js | 21 +++ src/UI/History/HistoryLayout.js | 20 ++- src/UI/History/HistoryLayoutTemplate.html | 2 + src/UI/System/Logs/Files/LogFileLayout.js | 6 +- src/UI/System/Logs/Table/LogsTableLayout.js | 6 +- 15 files changed, 355 insertions(+), 10 deletions(-) create mode 100644 src/NzbDrone.Api/Blacklist/BlacklistModule.cs create mode 100644 src/NzbDrone.Api/Blacklist/BlacklistResource.cs create mode 100644 src/NzbDrone.Core/Blacklisting/ClearBlacklistCommand.cs create mode 100644 src/UI/History/Blacklist/BlacklistActionsCell.js create mode 100644 src/UI/History/Blacklist/BlacklistCollection.js create mode 100644 src/UI/History/Blacklist/BlacklistLayout.js create mode 100644 src/UI/History/Blacklist/BlacklistLayoutTemplate.html create mode 100644 src/UI/History/Blacklist/BlacklistModel.js diff --git a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs new file mode 100644 index 000000000..913810767 --- /dev/null +++ b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs @@ -0,0 +1,42 @@ +using System; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Api.Blacklist +{ + public class BlacklistModule : NzbDroneRestModule + { + private readonly IBlacklistService _blacklistService; + + public BlacklistModule(IBlacklistService blacklistService) + { + _blacklistService = blacklistService; + GetResourcePaged = GetBlacklist; + DeleteResource = Delete; + } + + private PagingResource GetBlacklist(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + //This is a hack to deal with backgrid setting the sortKey to the column name instead of sortValue + if (pagingSpec.SortKey.Equals("series", StringComparison.InvariantCultureIgnoreCase)) + { + pagingSpec.SortKey = "series.title"; + } + + return ApplyToPage(_blacklistService.Paged, pagingSpec); + } + + private void Delete(int id) + { + _blacklistService.Delete(id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs new file mode 100644 index 000000000..f2c27f2c4 --- /dev/null +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Api.REST; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Blacklist +{ + public class BlacklistResource : RestResource + { + public int SeriesId { get; set; } + public List EpisodeIds { get; set; } + public string SourceTitle { get; set; } + public QualityModel Quality { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 00f5f96e3..0371bd9cb 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -82,6 +82,8 @@ + + diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 8e11f02ad..8b3ab0a2d 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -1,5 +1,7 @@ using System; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Blacklisting @@ -7,9 +9,11 @@ namespace NzbDrone.Core.Blacklisting public interface IBlacklistService { bool Blacklisted(string sourceTitle); + PagingSpec Paged(PagingSpec pagingSpec); + void Delete(int id); } - public class BlacklistService : IBlacklistService, IHandle + public class BlacklistService : IBlacklistService, IHandle, IExecute { private readonly IBlacklistRepository _blacklistRepository; private readonly IRedownloadFailedDownloads _redownloadFailedDownloadService; @@ -25,6 +29,16 @@ namespace NzbDrone.Core.Blacklisting return _blacklistRepository.Blacklisted(sourceTitle); } + public PagingSpec Paged(PagingSpec pagingSpec) + { + return _blacklistRepository.GetPaged(pagingSpec); + } + + public void Delete(int id) + { + _blacklistRepository.Delete(id); + } + public void Handle(DownloadFailedEvent message) { var blacklist = new Blacklist @@ -40,5 +54,10 @@ namespace NzbDrone.Core.Blacklisting _redownloadFailedDownloadService.Redownload(message.SeriesId, message.EpisodeIds); } + + public void Execute(ClearBlacklistCommand message) + { + _blacklistRepository.Purge(); + } } } diff --git a/src/NzbDrone.Core/Blacklisting/ClearBlacklistCommand.cs b/src/NzbDrone.Core/Blacklisting/ClearBlacklistCommand.cs new file mode 100644 index 000000000..03e9b2e5e --- /dev/null +++ b/src/NzbDrone.Core/Blacklisting/ClearBlacklistCommand.cs @@ -0,0 +1,15 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Blacklisting +{ + public class ClearBlacklistCommand : Command + { + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c2be091cf..3c42a4d2a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -122,6 +122,7 @@ + diff --git a/src/UI/History/Blacklist/BlacklistActionsCell.js b/src/UI/History/Blacklist/BlacklistActionsCell.js new file mode 100644 index 000000000..3f6e9a766 --- /dev/null +++ b/src/UI/History/Blacklist/BlacklistActionsCell.js @@ -0,0 +1,26 @@ +'use strict'; + +define( + [ + 'Cells/NzbDroneCell' + ], function (NzbDroneCell) { + return NzbDroneCell.extend({ + + className: 'blacklist-controls-cell', + + events: { + 'click': '_delete' + }, + + render: function () { + this.$el.empty(); + this.$el.html(''); + + return this; + }, + + _delete: function () { + this.model.destroy(); + } + }); + }); diff --git a/src/UI/History/Blacklist/BlacklistCollection.js b/src/UI/History/Blacklist/BlacklistCollection.js new file mode 100644 index 000000000..3dcdb7cab --- /dev/null +++ b/src/UI/History/Blacklist/BlacklistCollection.js @@ -0,0 +1,44 @@ +'use strict'; +define( + [ + 'History/Blacklist/BlacklistModel', + 'backbone.pageable', + 'Mixins/AsPersistedStateCollection' + ], function (BlacklistModel, PageableCollection, AsPersistedStateCollection) { + var collection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/blacklist', + model: BlacklistModel, + + state: { + pageSize: 15, + sortKey : 'date', + order : 1 + }, + + queryParams: { + totalPages : null, + totalRecords: null, + pageSize : 'pageSize', + sortKey : 'sortKey', + order : 'sortDir', + directions : { + '-1': 'asc', + '1' : 'desc' + } + }, + + parseState: function (resp) { + return { totalRecords: resp.totalRecords }; + }, + + parseRecords: function (resp) { + if (resp) { + return resp.records; + } + + return resp; + } + }); + + return AsPersistedStateCollection.apply(collection); + }); diff --git a/src/UI/History/Blacklist/BlacklistLayout.js b/src/UI/History/Blacklist/BlacklistLayout.js new file mode 100644 index 000000000..8a6a74487 --- /dev/null +++ b/src/UI/History/Blacklist/BlacklistLayout.js @@ -0,0 +1,132 @@ +'use strict'; +define( + [ + 'vent', + 'marionette', + 'backgrid', + 'History/Blacklist/BlacklistCollection', + 'Cells/SeriesTitleCell', + 'Cells/QualityCell', + 'Cells/RelativeDateCell', + 'History/Blacklist/BlacklistActionsCell', + 'Shared/Grid/Pager', + 'Shared/LoadingView', + 'Shared/Toolbar/ToolbarLayout' + ], function (vent, + Marionette, + Backgrid, + BlacklistCollection, + SeriesTitleCell, + QualityCell, + RelativeDateCell, + BlacklistActionsCell, + GridPager, + LoadingView, + ToolbarLayout) { + return Marionette.Layout.extend({ + template: 'History/Blacklist/BlacklistLayoutTemplate', + + regions: { + blacklist : '#x-blacklist', + toolbar : '#x-toolbar', + pager : '#x-pager' + }, + + columns: + [ + { + name : 'series', + label: 'Series', + cell : SeriesTitleCell, + sortValue: 'series.title' + }, + { + name : 'sourceTitle', + label: 'Source Title', + cell : 'string', + sortValue: 'sourceTitle' + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + sortable: false + }, + { + name : 'date', + label: 'Date', + cell : RelativeDateCell + }, + { + name : 'this', + label : '', + cell : BlacklistActionsCell, + sortable: false + } + ], + + initialize: function () { + this.collection = new BlacklistCollection({ tableName: 'blacklist' }); + this.listenTo(this.collection, 'sync', this._showTable); + vent.on(vent.Events.CommandComplete, this._commandComplete, this); + }, + + onShow: function () { + this.blacklist.show(new LoadingView()); + this._showToolbar(); + this.collection.fetch(); + }, + + _showTable: function (collection) { + + this.blacklist.show(new Backgrid.Grid({ + columns : this.columns, + collection: collection, + className : 'table table-hover' + })); + + this.pager.show(new GridPager({ + columns : this.columns, + collection: collection + })); + }, + + _showToolbar: function () { + var leftSideButtons = { + type : 'default', + storeState: false, + items : + [ + { + title : 'Clear Blacklist', + icon : 'icon-trash', + command : 'clearBlacklist' + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : + [ + leftSideButtons + ], + context: this + })); + }, + + _refreshTable: function (buttonContext) { + this.collection.state.currentPage = 1; + var promise = this.collection.fetch({ reset: true }); + + if (buttonContext) { + buttonContext.ui.icon.spinForPromise(promise); + } + }, + + _commandComplete: function (options) { + if (options.command.get('name') === 'clearblacklist') { + this._refreshTable(); + } + } + }); + }); diff --git a/src/UI/History/Blacklist/BlacklistLayoutTemplate.html b/src/UI/History/Blacklist/BlacklistLayoutTemplate.html new file mode 100644 index 000000000..f90d55c39 --- /dev/null +++ b/src/UI/History/Blacklist/BlacklistLayoutTemplate.html @@ -0,0 +1,11 @@ +
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/History/Blacklist/BlacklistModel.js b/src/UI/History/Blacklist/BlacklistModel.js new file mode 100644 index 000000000..f8bf927aa --- /dev/null +++ b/src/UI/History/Blacklist/BlacklistModel.js @@ -0,0 +1,21 @@ +'use strict'; +define( + [ + 'backbone', + 'Series/SeriesCollection' + ], function (Backbone, SeriesCollection) { + return Backbone.Model.extend({ + + //Hack to deal with Backbone 1.0's bug + initialize: function () { + this.url = function () { + return this.collection.url + '/' + this.get('id'); + }; + }, + + parse: function (model) { + model.series = SeriesCollection.get(model.seriesId); + return model; + } + }); + }); diff --git a/src/UI/History/HistoryLayout.js b/src/UI/History/HistoryLayout.js index 04f5bc12f..fc13cd2fe 100644 --- a/src/UI/History/HistoryLayout.js +++ b/src/UI/History/HistoryLayout.js @@ -5,24 +5,28 @@ define( 'backbone', 'backgrid', 'History/Table/HistoryTableLayout', + 'History/Blacklist/BlacklistLayout', 'History/Queue/QueueLayout' - ], function (Marionette, Backbone, Backgrid, HistoryTableLayout, QueueLayout) { + ], function (Marionette, Backbone, Backgrid, HistoryTableLayout, BlacklistLayout, QueueLayout) { return Marionette.Layout.extend({ template: 'History/HistoryLayoutTemplate', regions: { history : '#history', + blacklist : '#blacklist', queueRegion: '#queue' }, ui: { historyTab: '.x-history-tab', + blacklistTab: '.x-blacklist-tab', queueTab : '.x-queue-tab' }, events: { - 'click .x-history-tab': '_showHistory', - 'click .x-queue-tab' : '_showQueue' + 'click .x-history-tab' : '_showHistory', + 'click .x-blacklist-tab' : '_showBlacklist', + 'click .x-queue-tab' : '_showQueue' }, initialize: function (options) { @@ -55,6 +59,16 @@ define( this._navigate('/history'); }, + _showBlacklist: function (e) { + if (e) { + e.preventDefault(); + } + + this.blacklist.show(new BlacklistLayout()); + this.ui.blacklistTab.tab('show'); + this._navigate('/history/blacklist'); + }, + _showQueue: function (e) { if (e) { e.preventDefault(); diff --git a/src/UI/History/HistoryLayoutTemplate.html b/src/UI/History/HistoryLayoutTemplate.html index 50c981bf0..4faa2d28b 100644 --- a/src/UI/History/HistoryLayoutTemplate.html +++ b/src/UI/History/HistoryLayoutTemplate.html @@ -1,9 +1,11 @@ 
+
\ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayout.js b/src/UI/System/Logs/Files/LogFileLayout.js index af8de87a9..0b1431aee 100644 --- a/src/UI/System/Logs/Files/LogFileLayout.js +++ b/src/UI/System/Logs/Files/LogFileLayout.js @@ -81,7 +81,7 @@ define( title : 'Refresh', icon : 'icon-refresh', ownerContext : this, - callback : this._refreshLogs + callback : this._refreshTable }, { @@ -140,7 +140,7 @@ define( this.contents.show(new ContentsView({ model: model })); }, - _refreshLogs: function (buttonContext) { + _refreshTable: function (buttonContext) { this.contents.close(); var promise = this.collection.fetch(); @@ -152,7 +152,7 @@ define( _commandComplete: function (options) { if (options.command.get('name') === 'deletelogfiles') { - this._refreshLogs(); + this._refreshTable(); } } }); diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js index 219ebfc1b..b86bdbdf8 100644 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ b/src/UI/System/Logs/Table/LogsTableLayout.js @@ -97,7 +97,7 @@ define( title : 'Refresh', icon : 'icon-refresh', ownerContext : this, - callback : this._refreshLogs + callback : this._refreshTable }, { @@ -117,7 +117,7 @@ define( })); }, - _refreshLogs: function (buttonContext) { + _refreshTable: function (buttonContext) { this.collection.state.currentPage = 1; var promise = this.collection.fetch({ reset: true }); @@ -128,7 +128,7 @@ define( _commandComplete: function (options) { if (options.command.get('name') === 'clearlog') { - this._refreshLogs(); + this._refreshTable(); } } }); From af4c35142801ec55abd32689601ab8a40915478d Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:19 +0100 Subject: [PATCH 29/37] Series Index can now be filtered and no longer fetches twice when starting. --- src/UI/Mixins/AsFilteredCollection.js | 78 +++++++++++++++++++ src/UI/Mixins/AsPersistedStateCollection.js | 27 +++---- src/UI/Series/Index/SeriesIndexLayout.js | 69 +++++++++++++--- src/UI/Series/SeriesCollection.js | 17 +++- .../Shared/Toolbar/Radio/RadioButtonView.js | 6 +- src/UI/Shared/Toolbar/ToolbarLayout.js | 21 +++-- .../Shared/Toolbar/ToolbarLayoutTemplate.html | 10 +-- 7 files changed, 183 insertions(+), 45 deletions(-) create mode 100644 src/UI/Mixins/AsFilteredCollection.js diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js new file mode 100644 index 000000000..447f60edb --- /dev/null +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -0,0 +1,78 @@ +'use strict'; + +define( + [ + 'underscore', + 'backbone'], + function (_, Backbone) { + + return function () { + + this.prototype.setFilter = function(filter, options) { + options = _.extend({ reset: true }, options || {}); + + this.state.filterKey = filter[0]; + this.state.filterValue = filter[1]; + + if (options.reset) { + if (this.mode != 'server') { + this.fullCollection.resetFiltered(); + } else { + return this.fetch(); + } + } + }; + + this.prototype.setFilterMode = function(mode, options) { + this.setFilter(this.filterModes[mode], options); + }; + + var originalMakeFullCollection = this.prototype._makeFullCollection; + + this.prototype._makeFullCollection = function (models, options) { + var self = this; + + self.shadowCollection = originalMakeFullCollection.apply(this, [models, options]); + + var filterModel = function(model) { + if (!self.state.filterKey || !self.state.filterValue) + return true; + else + return model.get(self.state.filterKey) === self.state.filterValue; + }; + + self.shadowCollection.filtered = function() { + return this.filter(filterModel); + }; + + var filteredModels = self.shadowCollection.filtered(); + + var fullCollection = originalMakeFullCollection.apply(this, [filteredModels, options]); + + + fullCollection.resetFiltered = function(options) { + Backbone.Collection.prototype.reset.apply(this, [self.shadowCollection.filtered(), options]); + }; + + fullCollection.reset = function (models, options) { + self.shadowCollection.reset(models, options); + self.fullCollection.resetFiltered(); + }; + + return fullCollection; + }; + + _.extend(this.prototype.state, { + filterKey : null, + filterValue : null + }); + + _.extend(this.prototype.queryParams, { + filterKey : 'filterKey', + filterValue : 'filterValue' + }); + + return this; + }; + } +); \ No newline at end of file diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js index 879c05427..d4177f1a4 100644 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ b/src/UI/Mixins/AsPersistedStateCollection.js @@ -62,23 +62,20 @@ define( return '1'; }; + + var originalMakeComparator = this.prototype._makeComparator; + this.prototype._makeComparator = function (sortKey, order, sortValue) { + var state = this.state; - _.extend(this.prototype, { - initialSort: function () { - var key = this.state.sortKey; - var order = this.state.order; + sortKey = sortKey || state.sortKey; + order = order || state.order; - if (this[key] && this.mode === 'client') { - var sortValue = this[key]; - - this.setSorting(key, order, { sortValue: sortValue }); - - var comparator = this._makeComparator(key, order, sortValue); - this.fullCollection.comparator = comparator; - this.fullCollection.sort(); - } - } - }); + if (!sortKey || !order) return; + + if (!sortValue && this[sortKey]) sortValue = this[sortKey]; + + return originalMakeComparator.call(this, sortKey, order, sortValue); + }; return this; }; diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 416d1ec1a..ff7eb55c1 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -167,6 +167,44 @@ define( this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView); + this.filteringOptions = { + type : 'radio', + storeState : true, + menuKey : 'series.filterMode', + defaultAction: 'all', + items : + [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-circle-blank', + callback: this._setFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback: this._setFilter + }, + { + key : 'continuing', + title : '', + tooltip : 'Continuing Only', + icon : 'icon-play', + callback: this._setFilter + }, + { + key : 'ended', + title : '', + tooltip : 'Ended Only', + icon : 'icon-stop', + callback: this._setFilter + } + ] + }; + this.viewButtons = { type : 'radio', storeState : true, @@ -201,26 +239,30 @@ define( _showTable: function () { this.currentView = new Backgrid.Grid({ - collection: SeriesCollection, + collection: this.seriesCollection, columns : this.columns, className : 'table table-hover' }); - this._fetchCollection(); + this._renderView(); }, _showList: function () { - this.currentView = new ListCollectionView({ collection: SeriesCollection }); + this.currentView = new ListCollectionView({ + collection: this.seriesCollection + }); - this._fetchCollection(); + this._renderView(); }, _showPosters: function () { - this.currentView = new PosterCollectionView({ collection: SeriesCollection }); + this.currentView = new PosterCollectionView({ + collection: this.seriesCollection + }); - this._fetchCollection(); + this._renderView(); }, - + _renderView: function () { if (SeriesCollection.length === 0) { @@ -238,10 +280,17 @@ define( onShow: function () { this._showToolbar(); this._renderView(); + this._fetchCollection(); }, _fetchCollection: function () { - SeriesCollection.fetch(); + this.seriesCollection.fetch(); + }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.seriesCollection.setFilterMode(mode); }, _showToolbar: function () { @@ -251,11 +300,11 @@ define( } var rightButtons = [ + this.sortingOptions, + this.filteringOptions, this.viewButtons ]; - rightButtons.splice(0, 0, this.sortingOptions); - this.toolbar.show(new ToolbarLayout({ right : rightButtons, left : diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index 629ed799e..4f00e6c42 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -6,9 +6,10 @@ define( 'backbone.pageable', 'Series/SeriesModel', 'api!series', + 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection', 'moment' - ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection, Moment) { + ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsFilteredCollection, AsPersistedStateCollection, Moment) { var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', model: SeriesModel, @@ -47,6 +48,14 @@ define( return proxy.save(); }, + // Filter Modes + filterModes: { + 'all' : [null, null], + 'continuing' : ['status', 'continuing'], + 'ended' : ['status', 'ended'], + 'monitored' : ['monitored', true] + }, + //Sorters nextAiring: function (model, attr) { var nextAiring = model.get(attr); @@ -59,9 +68,9 @@ define( } }); - var MixedIn = AsPersistedStateCollection.call(Collection); - var collection = new MixedIn(SeriesData); - collection.initialSort(); + var FilteredCollection = AsFilteredCollection.call(Collection); + var MixedIn = AsPersistedStateCollection.call(FilteredCollection); + var collection = new MixedIn(SeriesData, { full: true }); return collection; }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index 1fb788250..baa67a5da 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -9,6 +9,10 @@ define( template : 'Shared/Toolbar/ButtonTemplate', className: 'btn', + ui: { + icon: 'i' + }, + events: { 'click': 'onClick' }, @@ -49,7 +53,7 @@ define( var callback = this.model.get('callback'); if (callback) { - callback.call(this.model.ownerContext); + callback.call(this.model.ownerContext, this); } } }); diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index 8a907dad6..a50828880 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -12,11 +12,9 @@ define( return Marionette.Layout.extend({ template: 'Shared/Toolbar/ToolbarLayoutTemplate', - regions: { - left_1 : '.x-toolbar-left-1', - left_2 : '.x-toolbar-left-2', - right_1: '.x-toolbar-right-1', - right_2: '.x-toolbar-right-2' + ui: { + left_x : '.x-toolbar-left', + right_x: '.x-toolbar-right' }, initialize: function (options) { @@ -97,8 +95,17 @@ define( break; } } - - this[position + '_' + (index + 1).toString()].show(buttonGroupView); + + var regionId = position + "_" + (index + 1); + var region = this[regionId]; + + if (!region) { + var regionClassName = "x-toolbar-" + position + "-" + (index + 1); + this.ui[position + '_x'].append('
\r\n'); + region = this.addRegion(regionId, "." + regionClassName); + } + + region.show(buttonGroupView); } }); }); diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html index b4cd4dcda..533f83bf9 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html @@ -1,8 +1,2 @@ -
-
-
-
-
-
-
-
+
+
From 9df0ad0bf7c2c96bcfb076d23361153f2184b01b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:22 +0100 Subject: [PATCH 30/37] System.Logs view can now be filtered by severity. --- src/NzbDrone.Api/Logs/LogModule.cs | 25 +++++++++ src/NzbDrone.Api/PagingResource.cs | 2 + src/NzbDrone.Api/REST/RestModule.cs | 10 ++++ .../Datastore/BasicRepository.cs | 18 ++++--- .../History/HistoryRepository.cs | 18 ++----- src/UI/System/Logs/LogsCollection.js | 15 +++++- src/UI/System/Logs/Table/LogsTableLayout.js | 53 ++++++++++++++++++- 7 files changed, 116 insertions(+), 25 deletions(-) diff --git a/src/NzbDrone.Api/Logs/LogModule.cs b/src/NzbDrone.Api/Logs/LogModule.cs index 59ea4975d..8684e3250 100644 --- a/src/NzbDrone.Api/Logs/LogModule.cs +++ b/src/NzbDrone.Api/Logs/LogModule.cs @@ -23,6 +23,31 @@ namespace NzbDrone.Api.Logs pageSpec.SortKey = "id"; } + if (pagingResource.FilterKey == "level") + { + switch (pagingResource.FilterValue) + { + case "Fatal": + pageSpec.FilterExpression = h => h.Level == "Fatal"; + break; + case "Error": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error"; + break; + case "Warn": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"; + break; + case "Info": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"; + break; + case "Debug": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"; + break; + case "Trace": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"; + break; + } + } + return ApplyToPage(_logService.Paged, pageSpec); } } diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/NzbDrone.Api/PagingResource.cs index ab53c24cd..96eeb7c48 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/NzbDrone.Api/PagingResource.cs @@ -9,6 +9,8 @@ namespace NzbDrone.Api public int PageSize { get; set; } public string SortKey { get; set; } public SortDirection SortDirection { get; set; } + public string FilterKey { get; set; } + public string FilterValue { get; set; } public int TotalRecords { get; set; } public List Records { get; set; } } diff --git a/src/NzbDrone.Api/REST/RestModule.cs b/src/NzbDrone.Api/REST/RestModule.cs index 1efcf0b3a..49cd14fa9 100644 --- a/src/NzbDrone.Api/REST/RestModule.cs +++ b/src/NzbDrone.Api/REST/RestModule.cs @@ -241,6 +241,16 @@ namespace NzbDrone.Api.REST } } + if (Request.Query.FilterKey != null) + { + pagingResource.FilterKey = Request.Query.FilterKey.ToString(); + + if (Request.Query.FilterValue != null) + { + pagingResource.FilterValue = Request.Query.FilterValue.ToString(); + } + } + return pagingResource; } } diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index f82a68354..f4125c0f2 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -210,18 +210,20 @@ namespace NzbDrone.Core.Datastore public virtual PagingSpec GetPaged(PagingSpec pagingSpec) { - var pagingQuery = Query.OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); - - pagingSpec.Records = pagingQuery.ToList(); - - //TODO: Use the same query for count and records - pagingSpec.TotalRecords = Count(); + pagingSpec.Records = GetPagedQuery(Query, pagingSpec).ToList(); + pagingSpec.TotalRecords = GetPagedQuery(Query, pagingSpec).GetRowCount(); return pagingSpec; } + protected virtual SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) + { + return query.Where(pagingSpec.FilterExpression) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); + } + public void DeleteAll() { DataMapper.Delete(c => c.Id > 0); diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 59d54c745..b1fdcad7a 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -67,22 +67,12 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } - public override PagingSpec GetPaged(PagingSpec pagingSpec) + protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { - pagingSpec.Records = GetPagedQuery(pagingSpec).ToList(); - pagingSpec.TotalRecords = GetPagedQuery(pagingSpec).GetRowCount(); + var baseQuery = query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) + .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id); - return pagingSpec; - } - - private SortBuilder GetPagedQuery(PagingSpec pagingSpec) - { - return Query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) - .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id) - .Where(pagingSpec.FilterExpression) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); + return base.GetPagedQuery(baseQuery, pagingSpec); } } } \ No newline at end of file diff --git a/src/UI/System/Logs/LogsCollection.js b/src/UI/System/Logs/LogsCollection.js index 350ed1522..045675796 100644 --- a/src/UI/System/Logs/LogsCollection.js +++ b/src/UI/System/Logs/LogsCollection.js @@ -4,9 +4,10 @@ define( [ 'backbone.pageable', 'System/Logs/LogsModel', + 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection' ], - function (PagableCollection, LogsModel, AsPersistedStateCollection) { + function (PagableCollection, LogsModel, AsFilteredCollection, AsPersistedStateCollection) { var collection = PagableCollection.extend({ url : window.NzbDrone.ApiRoot + '/log', model: LogsModel, @@ -30,6 +31,14 @@ define( } }, + // Filter Modes + filterModes: { + 'all' : [null, null], + 'info' : ['level', 'Info'], + 'warn' : ['level', 'Warn'], + 'error' : ['level', 'Error'] + }, + parseState: function (resp, queryParams, state) { return {totalRecords: resp.totalRecords}; }, @@ -43,5 +52,7 @@ define( } }); - return AsPersistedStateCollection.call(collection); + collection = AsFilteredCollection.apply(collection); + + return AsPersistedStateCollection.apply(collection); }); diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js index 219ebfc1b..9de384d68 100644 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ b/src/UI/System/Logs/Table/LogsTableLayout.js @@ -66,7 +66,6 @@ define( onRender: function () { this.grid.show(new LoadingView()); - this.collection.fetch(); }, onShow: function () { @@ -88,6 +87,44 @@ define( }, _showToolbar: function () { + var filterButtons = { + type : 'radio', + storeState : true, + menuKey : 'logs.filterMode', + defaultAction: 'all', + items : + [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-circle-blank', + callback : this._setFilter + }, + { + key : 'info', + title : '', + tooltip : 'Info', + icon : 'icon-info', + callback : this._setFilter + }, + { + key : 'warn', + title : '', + tooltip : 'Warn', + icon : 'icon-warn', + callback : this._setFilter + }, + { + key : 'error', + title : '', + tooltip : 'Error', + icon : 'icon-error', + callback : this._setFilter + } + ] + }; + var rightSideButtons = { type : 'default', storeState: false, @@ -111,6 +148,7 @@ define( this.toolbar.show(new ToolbarLayout({ right : [ + filterButtons, rightSideButtons ], context: this @@ -125,6 +163,19 @@ define( buttonContext.ui.icon.spinForPromise(promise); } }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.setFilterMode(mode, { reset: false }); + + this.collection.state.currentPage = 1; + var promise = this.collection.fetch({ reset: true }); + + if (buttonContext) { + buttonContext.ui.icon.spinForPromise(promise); + } + }, _commandComplete: function (options) { if (options.command.get('name') === 'clearlog') { From 7a4e05f04a0fb9c776375e5233a81b8d5547f881 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:25 +0100 Subject: [PATCH 31/37] Workaround to ensure the view uses a unique cloned collection for filtering instead of affecting the generic SeriesCollection. --- src/UI/Series/Index/SeriesIndexLayout.js | 70 ++++++++++++------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index ff7eb55c1..c5cc9ca51 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -127,46 +127,46 @@ define( ] }, - sortingOptions: { - type : 'sorting', - storeState : false, - viewCollection: SeriesCollection, - items : - [ - { - title: 'Title', - name : 'title' - }, - { - title: 'Seasons', - name : 'seasonCount' - }, - { - title: 'Quality', - name : 'qualityProfileId' - }, - { - title: 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring', - sortValue : SeriesCollection.nextAiring - }, - { - title: 'Episodes', - name : 'percentOfEpisodes' - } - ] - }, - initialize: function () { - this.seriesCollection = SeriesCollection; + this.seriesCollection = SeriesCollection.clone(); this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView); + this.sortingOptions = { + type : 'sorting', + storeState : false, + viewCollection: this.seriesCollection, + items : + [ + { + title: 'Title', + name : 'title' + }, + { + title: 'Seasons', + name : 'seasonCount' + }, + { + title: 'Quality', + name : 'qualityProfileId' + }, + { + title: 'Network', + name : 'network' + }, + { + title : 'Next Airing', + name : 'nextAiring', + sortValue : SeriesCollection.nextAiring + }, + { + title: 'Episodes', + name : 'percentOfEpisodes' + } + ] + }; + this.filteringOptions = { type : 'radio', storeState : true, From 98903869c32f0b8e02dfb97ef5866127c9eae0bb Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 1 Feb 2014 23:09:30 +0100 Subject: [PATCH 32/37] Quick patch to solve Model.url issue. Should update to backbone 1.1.0 instead. --- src/UI/JsLibraries/backbone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/JsLibraries/backbone.js b/src/UI/JsLibraries/backbone.js index 3512d42fb..70a854d31 100644 --- a/src/UI/JsLibraries/backbone.js +++ b/src/UI/JsLibraries/backbone.js @@ -259,7 +259,7 @@ }; // A list of options to be attached directly to the model, if provided. - var modelOptions = ['url', 'urlRoot', 'collection']; + var modelOptions = ['urlRoot', 'collection']; // Attach all inheritable methods to the Model prototype. _.extend(Model.prototype, Events, { From 01bdec965b496287d22936f0fe271be7a97bedc9 Mon Sep 17 00:00:00 2001 From: markus101 Date: Sat, 1 Feb 2014 14:26:49 -0800 Subject: [PATCH 33/37] Add absolute episode numbers to episodes during refresh --- .../TvTests/RefreshEpisodeServiceFixture.cs | Bin 6639 -> 7498 bytes src/NzbDrone.Core/Tv/RefreshEpisodeService.cs | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index bb89e1a8c84413861c36b49400df8a10c3f511e3..cdf3492bd1172834f547df2faefbfc08a7f5d96d 100644 GIT binary patch delta 437 zcmaEFe9CHrmDuC}UR|Ez)ROqbq~iRX(vsB4%XzgX-)Fxy*4p~O${gvRSXrk zQ!vo9p6nQs@M2n%GbAD-FiH4?vwu0g0iCik#xP1)t88#PzJgn!KlLHGS qklh$oD}YrY*&k9=nmYL-i{fMlA+E{$xFjd{^GgD0k e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) + foreach (var episode in group.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber)) { episode.AirDateUtc = episode.AirDateUtc.Value.AddMinutes(series.Runtime * episodeCount); episodeCount++; } } } + + private static void SetAbsoluteEpisodeNumber(IEnumerable allEpisodes) + { + var episodes = allEpisodes.Where(e => e.SeasonNumber > 0 && e.EpisodeNumber > 0) + .OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber) + .ToList(); + + for (int i = 0; i < episodes.Count(); i++) + { + episodes[i].AbsoluteEpisodeNumber = i + 1; + } + } } } \ No newline at end of file From 298837f96a102c0ce7b5b6cb89275f03c291f948 Mon Sep 17 00:00:00 2001 From: markus101 Date: Sat, 1 Feb 2014 16:32:08 -0800 Subject: [PATCH 34/37] Changed buttons on series/logs views to look less cramped --- src/UI/Content/theme.less | 38 +++++++++++++++---- src/UI/Series/Index/SeriesIndexLayout.js | 26 +++++++++---- .../Index/SeriesIndexLayoutTemplate.html | 5 ++- src/UI/Shared/Toolbar/ToolbarLayout.js | 3 +- src/UI/System/Logs/Files/LogFileLayout.js | 6 +-- src/UI/System/Logs/Table/LogsTableLayout.js | 11 ++++-- 6 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 5f1dd6725..e5916eb0c 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -15,19 +15,41 @@ @import "../Shared/Styles/card"; @import "../Rename/rename"; -.page-toolbar { - margin-top : 10px; - margin-bottom : 30px; +.toolbar { - .toolbar-group { - display: inline-block; + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; } - .sorting-buttons { - .sorting-title { + .page-toolbar { + margin-top : 10px; + margin-bottom : 30px; + + .toolbar-group { display: inline-block; - width: 110px; } + + .sorting-buttons { + .sorting-title { + display: inline-block; + width: 110px; + } + } + } +} + +.toolbars { + margin-top : 5px; + margin-bottom : 30px; + + .page-toolbar { + margin-top : 5px; + margin-bottom : 0px; } } diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index c5cc9ca51..fc42b2ea1 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -41,6 +41,7 @@ define( regions: { seriesRegion : '#x-series', toolbar : '#x-toolbar', + toolbar2 : '#x-toolbar2', footer : '#x-series-footer' }, @@ -237,6 +238,12 @@ define( }; }, + onShow: function () { + this._showToolbar(); + this._renderView(); + this._fetchCollection(); + }, + _showTable: function () { this.currentView = new Backgrid.Grid({ collection: this.seriesCollection, @@ -277,12 +284,6 @@ define( } }, - onShow: function () { - this._showToolbar(); - this._renderView(); - this._fetchCollection(); - }, - _fetchCollection: function () { this.seriesCollection.fetch(); }, @@ -300,8 +301,6 @@ define( } var rightButtons = [ - this.sortingOptions, - this.filteringOptions, this.viewButtons ]; @@ -313,6 +312,17 @@ define( ], context: this })); + + this.toolbar2.show(new ToolbarLayout({ + right : [ + this.filteringOptions + ], + left : + [ + this.sortingOptions + ], + context: this + })); }, _showFooter: function () { diff --git a/src/UI/Series/Index/SeriesIndexLayoutTemplate.html b/src/UI/Series/Index/SeriesIndexLayoutTemplate.html index 40124822f..08353e5c1 100644 --- a/src/UI/Series/Index/SeriesIndexLayoutTemplate.html +++ b/src/UI/Series/Index/SeriesIndexLayoutTemplate.html @@ -1,4 +1,7 @@ -
+
+
+
+
diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index a50828880..b1e2ee148 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -10,7 +10,8 @@ define( 'underscore' ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView, SortingButtonCollectionView, _) { return Marionette.Layout.extend({ - template: 'Shared/Toolbar/ToolbarLayoutTemplate', + template : 'Shared/Toolbar/ToolbarLayoutTemplate', + className: 'toolbar', ui: { left_x : '.x-toolbar-left', diff --git a/src/UI/System/Logs/Files/LogFileLayout.js b/src/UI/System/Logs/Files/LogFileLayout.js index 0b1431aee..8cffc029e 100644 --- a/src/UI/System/Logs/Files/LogFileLayout.js +++ b/src/UI/System/Logs/Files/LogFileLayout.js @@ -72,7 +72,7 @@ define( _showToolbar: function () { - var rightSideButtons = { + var leftSideButtons = { type : 'default', storeState: false, items : @@ -95,9 +95,9 @@ define( }; this.toolbar.show(new ToolbarLayout({ - right : + left : [ - rightSideButtons + leftSideButtons ], context: this })); diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js index 325aa0730..841a5a92b 100644 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ b/src/UI/System/Logs/Table/LogsTableLayout.js @@ -125,7 +125,7 @@ define( ] }; - var rightSideButtons = { + var leftSideButtons = { type : 'default', storeState: false, items : @@ -146,10 +146,13 @@ define( }; this.toolbar.show(new ToolbarLayout({ - right : + left : [ - filterButtons, - rightSideButtons + leftSideButtons + ], + right : + [ + filterButtons ], context: this })); From ee7cb2b80f91be2d9e66622745a5b3459a0bb3c0 Mon Sep 17 00:00:00 2001 From: markus101 Date: Sat, 1 Feb 2014 16:48:00 -0800 Subject: [PATCH 35/37] Hide both series toolbars when there are no series New: Filter series view New Filter logs table --- src/UI/Series/Index/SeriesIndexLayout.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index fc42b2ea1..0287b2818 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -275,6 +275,7 @@ define( if (SeriesCollection.length === 0) { this.seriesRegion.show(new EmptyView()); this.toolbar.close(); + this.toolbar2.close(); } else { this.seriesRegion.show(this.currentView); From 9da0263eb5c80bb9014026508fe3a1e20aec89fe Mon Sep 17 00:00:00 2001 From: markus101 Date: Sun, 2 Feb 2014 21:57:10 -0800 Subject: [PATCH 36/37] Added warning to permissions fixed icon name for form-info --- src/UI/Form/CheckboxTemplate.html | 2 +- src/UI/Form/FormHelpPartial.html | 2 +- src/UI/Form/PasswordTemplate.html | 2 +- src/UI/Form/SelectTemplate.html | 2 +- src/UI/SeasonPass/SeriesLayoutTemplate.html | 2 +- src/UI/Series/Delete/DeleteSeriesTemplate.html | 2 +- src/UI/Series/Edit/EditSeriesViewTemplate.html | 8 ++++---- src/UI/Settings/DownloadClient/SabViewTemplate.html | 2 +- .../Indexers/Options/IndexerOptionsViewTemplate.html | 2 +- .../FileManagement/FileManagementViewTemplate.html | 10 +++++----- .../Permissions/PermissionsViewTemplate.html | 7 ++++--- .../Settings/MediaManagement/Sorting/ViewTemplate.html | 2 +- .../Notifications/NotificationEditViewTemplate.html | 4 ++-- .../Quality/Profile/EditQualityProfileTemplate.html | 2 +- 14 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/UI/Form/CheckboxTemplate.html b/src/UI/Form/CheckboxTemplate.html index 94c7dde4c..68a6fdb26 100644 --- a/src/UI/Form/CheckboxTemplate.html +++ b/src/UI/Form/CheckboxTemplate.html @@ -14,7 +14,7 @@ {{#if helpText}} - + {{/if}}
diff --git a/src/UI/Form/FormHelpPartial.html b/src/UI/Form/FormHelpPartial.html index cdf295852..53a2a495b 100644 --- a/src/UI/Form/FormHelpPartial.html +++ b/src/UI/Form/FormHelpPartial.html @@ -1,6 +1,6 @@ {{#if helpText}} - + {{/if}} diff --git a/src/UI/Form/PasswordTemplate.html b/src/UI/Form/PasswordTemplate.html index 9f8c4a9a0..c03abbaed 100644 --- a/src/UI/Form/PasswordTemplate.html +++ b/src/UI/Form/PasswordTemplate.html @@ -5,7 +5,7 @@ {{#if helpText}} - + {{/if}}
diff --git a/src/UI/Form/SelectTemplate.html b/src/UI/Form/SelectTemplate.html index 62feb61af..89e5065ce 100644 --- a/src/UI/Form/SelectTemplate.html +++ b/src/UI/Form/SelectTemplate.html @@ -9,7 +9,7 @@ {{#if helpText}} - + {{/if}}
diff --git a/src/UI/SeasonPass/SeriesLayoutTemplate.html b/src/UI/SeasonPass/SeriesLayoutTemplate.html index 8601d6453..be084a498 100644 --- a/src/UI/SeasonPass/SeriesLayoutTemplate.html +++ b/src/UI/SeasonPass/SeriesLayoutTemplate.html @@ -22,7 +22,7 @@ - + diff --git a/src/UI/Series/Delete/DeleteSeriesTemplate.html b/src/UI/Series/Delete/DeleteSeriesTemplate.html index ac262f604..e54fc750d 100644 --- a/src/UI/Series/Delete/DeleteSeriesTemplate.html +++ b/src/UI/Series/Delete/DeleteSeriesTemplate.html @@ -27,7 +27,7 @@ - +
diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.html b/src/UI/Series/Edit/EditSeriesViewTemplate.html index 7fe6660b9..4476abad0 100644 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.html +++ b/src/UI/Series/Edit/EditSeriesViewTemplate.html @@ -26,7 +26,7 @@ - +
@@ -46,7 +46,7 @@ - +
@@ -62,7 +62,7 @@ {{/each}} - +
@@ -73,7 +73,7 @@
- +
diff --git a/src/UI/Settings/DownloadClient/SabViewTemplate.html b/src/UI/Settings/DownloadClient/SabViewTemplate.html index 39e194aa0..9b997ae07 100644 --- a/src/UI/Settings/DownloadClient/SabViewTemplate.html +++ b/src/UI/Settings/DownloadClient/SabViewTemplate.html @@ -7,7 +7,7 @@
-
diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html index 616a66f76..3dd1338e1 100644 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html +++ b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html @@ -28,7 +28,7 @@ - + Newline-delimited set of rules diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index ae02dc6a5..a150cee6e 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -16,7 +16,7 @@ - +
@@ -36,7 +36,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -112,7 +112,7 @@ - + diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html index 74cef9c4d..ca0a685a7 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -1,4 +1,4 @@ -{{#if_linux}} +{{#if_linux}}
Permissions @@ -17,7 +17,8 @@ - + + @@ -66,4 +67,4 @@
-{{/if_linux}} \ No newline at end of file +{{/if_linux}} \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html b/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html index dde9a2d84..76f25867a 100644 --- a/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html @@ -17,7 +17,7 @@ - + diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html index 9c1fff961..c785fe9c1 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/NotificationEditViewTemplate.html @@ -31,7 +31,7 @@ - + @@ -51,7 +51,7 @@ - + diff --git a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html index f45c97dda..5f65e44b5 100644 --- a/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html +++ b/src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html @@ -23,7 +23,7 @@ {{/each}} - + From 9a43ab9b94c22c628f1d3410ebe640e22cfedb54 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 3 Feb 2014 13:49:46 -0800 Subject: [PATCH 37/37] Re-organized buttons on series index --- src/UI/Series/Index/SeriesIndexLayout.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 0287b2818..3313bca83 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -301,12 +301,12 @@ define( return; } - var rightButtons = [ - this.viewButtons - ]; - this.toolbar.show(new ToolbarLayout({ - right : rightButtons, + right : + [ + this.sortingOptions, + this.viewButtons + ], left : [ this.leftSideButtons @@ -315,12 +315,9 @@ define( })); this.toolbar2.show(new ToolbarLayout({ - right : [ - this.filteringOptions - ], - left : + right : [ - this.sortingOptions + this.filteringOptions ], context: this }));