diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6bd416a38..50d8f9832 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,4 +2,6 @@ Provide a description of the feature request or bug, the more details the better. -Please use https://forums.sonarr.tv/ for support or other questions. (When in doubt, use the forums) +When possible include a log! + +Please use our [Discord server](https://discord.gg/NWYch8M) for support or longer discussions. diff --git a/.gitignore b/.gitignore index 8413af8f8..2a5850895 100644 --- a/.gitignore +++ b/.gitignore @@ -127,9 +127,18 @@ bin obj output/* +#Packages +Radarr_*/ +Radarr_*.zip +Radarr_*.gz #OS X metadata files ._* +.DS_Store _start _temp_*/**/* + +#AppVeyor +/tools-cake/ +/_artifacts/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..3cea8954a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: csharp +solution: src/NzbDrone.sln +script: # the following commands are just examples, use whatever your build process requires + - ./build.sh + - chmod +x test.sh +# - ./test.sh Linux Unit Takes far too long, maybe even crashes travis :/ +install: + - sudo apt-get install nodejs + - sudo apt-get install npm +after_success: + - chmod +x package.sh + - ./package.sh +notifications: + - webhooks: https://discordapp.com/api/webhooks/266910310219251712/V-QvCcnYkg3O8PMevcAJOJyCgrYkZQoF2pupLDGbaISNUECmYPd6LRwl3avKHsPyfgWP diff --git a/7za.dll b/7za.dll new file mode 100644 index 000000000..f2657b610 Binary files /dev/null and b/7za.dll differ diff --git a/7za.exe b/7za.exe new file mode 100644 index 000000000..dd6cc759b Binary files /dev/null and b/7za.exe differ diff --git a/7zxa.dll b/7zxa.dll new file mode 100644 index 000000000..21ec79dc2 Binary files /dev/null and b/7zxa.dll differ diff --git a/CLA.md b/CLA.md index 40adac7f6..05ce7890d 100644 --- a/CLA.md +++ b/CLA.md @@ -1,6 +1,6 @@ -# Sonarr Individual Contributor License Agreement # +# Radarr Individual Contributor License Agreement # -Thank you for your interest in contributing to Sonarr ("We" or "Us"). +Thank you for your interest in contributing to Radarr ("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 ## diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab945cb0c..3ae50843d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # How to Contribute # -We're always looking for people to help make Sonarr even better, there are a number of ways to contribute. +We're always looking for people to help make Radarr even better, there are a number of ways to contribute. ## Documentation ## Setup guides, FAQ, the more information we have on the wiki the better. @@ -15,7 +15,7 @@ Setup guides, FAQ, the more information we have on the wiki the better. ### Getting started ### -1. Fork Sonarr +1. Fork Radarr 2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)* 3. Run `npm install` 4. Run `npm start` - Used to compile the UI components and copy them. @@ -24,8 +24,8 @@ Setup guides, FAQ, the more information we have on the wiki the better. 5. Compile in Visual Studio ### Contributing Code ### -- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) -- Rebase from Sonarr's develop branch, don't merge +- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Radarr/Radarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first) +- Rebase from Radarr'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 diff --git a/Logo/1024.png b/Logo/1024.png index 9979d55d4..6923c2554 100644 Binary files a/Logo/1024.png and b/Logo/1024.png differ diff --git a/Logo/128.png b/Logo/128.png index ae8cf56c7..5e143b52e 100644 Binary files a/Logo/128.png and b/Logo/128.png differ diff --git a/Logo/16.png b/Logo/16.png index 00078bfdd..0a042f4bb 100644 Binary files a/Logo/16.png and b/Logo/16.png differ diff --git a/Logo/256.png b/Logo/256.png index 815750aa0..c958e1bbf 100644 Binary files a/Logo/256.png and b/Logo/256.png differ diff --git a/Logo/32.png b/Logo/32.png index a079d7afc..f1fe93db5 100644 Binary files a/Logo/32.png and b/Logo/32.png differ diff --git a/Logo/400.png b/Logo/400.png index f445977bf..dac41bfd8 100644 Binary files a/Logo/400.png and b/Logo/400.png differ diff --git a/Logo/48.png b/Logo/48.png index b4a009323..8b9d0fc88 100644 Binary files a/Logo/48.png and b/Logo/48.png differ diff --git a/Logo/512.png b/Logo/512.png index 36e87c0da..d2f56252f 100644 Binary files a/Logo/512.png and b/Logo/512.png differ diff --git a/Logo/64.png b/Logo/64.png index 33387d7f9..80edc7894 100644 Binary files a/Logo/64.png and b/Logo/64.png differ diff --git a/Logo/800.png b/Logo/800.png index a0081ab5c..4a1d25228 100644 Binary files a/Logo/800.png and b/Logo/800.png differ diff --git a/Logo/Radarr.svg b/Logo/Radarr.svg new file mode 100644 index 000000000..a9ce35970 --- /dev/null +++ b/Logo/Radarr.svg @@ -0,0 +1,572 @@ + + + + SVG + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Logo/Sonarr.svg b/Logo/Sonarr.svg deleted file mode 100644 index cc8e1370e..000000000 --- a/Logo/Sonarr.svg +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Logo/Thumbs.db b/Logo/Thumbs.db new file mode 100644 index 000000000..f01582531 Binary files /dev/null and b/Logo/Thumbs.db differ diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..b529317ee --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,48 @@ +version: '0.2.0.{build}' + +branches: + only: + - develop + +assembly_info: + patch: true + file: 'src\NzbDrone.Common\Properties\SharedAssemblyInfo.cs' + assembly_version: '{version}' + assembly_file_version: '{version}' + assembly_informational_version: '{version}-rc1' + +environment: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + +install: + - git submodule update --init --recursive + +build_script: + - ps: ./build-appveyor.ps1 + +test: off +# test: +# assemblies: +# - '_tests\*Test.dll' +# categories: +# except: +# - IntegrationTest +# - AutomationTest + +artifacts: + - path: '_artifacts\*.zip' + - path: '_artifacts\*.tar.gz' + +cache: + - '%USERPROFILE%\.nuget\packages' + - node_modules + +pull_requests: + do_not_increment_build_number: true + +only_commits: + files: + - src/ + - osx/ + - gulp/ + - logo/ diff --git a/build-appveyor.cake b/build-appveyor.cake new file mode 100644 index 000000000..8d806e663 --- /dev/null +++ b/build-appveyor.cake @@ -0,0 +1,303 @@ +#addin "Cake.Npm" +#addin "SharpZipLib" +#addin "Cake.Compression" + +// Build variables +var outputFolder = "./_output"; +var outputFolderMono = outputFolder + "_mono"; +var outputFolderOsx = outputFolder + "_osx"; +var outputFolderOsxApp = outputFolderOsx + "_app"; +var testPackageFolder = "./_tests"; +var testSearchPattern = "*.Test/bin/x86/Release"; +var sourceFolder = "./src"; +var solutionFile = sourceFolder + "/NzbDrone.sln"; +var updateFolder = outputFolder + "/NzbDrone.Update"; +var updateFolderMono = outputFolderMono + "/NzbDrone.Update"; + +// Artifact variables +var artifactsFolder = "./_artifacts"; +var artifactsFolderWindows = artifactsFolder + "/windows"; +var artifactsFolderLinux = artifactsFolder + "/linux"; +var artifactsFolderOsx = artifactsFolder + "/osx"; +var artifactsFolderOsxApp = artifactsFolder + "/osx-app"; + +// Utility methods +public void RemoveEmptyFolders(string startLocation) { + foreach (var directory in System.IO.Directory.GetDirectories(startLocation)) + { + RemoveEmptyFolders(directory); + + if (System.IO.Directory.GetFiles(directory).Length == 0 && + System.IO.Directory.GetDirectories(directory).Length == 0) + { + DeleteDirectory(directory, false); + } + } +} + +public void CleanFolder(string path, bool keepConfigFiles) { + DeleteFiles(path + "/**/*.transform"); + + if (!keepConfigFiles) { + DeleteFiles(path + "/**/*.dll.config"); + } + + DeleteFiles(path + "/**/FluentValidation.resources.dll"); + DeleteFiles(path + "/**/App.config"); + + DeleteFiles(path + "/**/*.less"); + + DeleteFiles(path + "/**/*.vshost.exe"); + + DeleteFiles(path + "/**/*.dylib"); + + RemoveEmptyFolders(path); +} + +public void CreateMdbs(string path) { + foreach (var file in System.IO.Directory.EnumerateFiles(path, "*.pdb", System.IO.SearchOption.AllDirectories)) { + var actualFile = file.Substring(0, file.Length - 4); + + if (FileExists(actualFile + ".exe")) { + StartProcess("./tools/pdb2mdb/pdb2mdb.exe", new ProcessSettings() + .WithArguments(args => args.Append(actualFile + ".exe"))); + } + + if (FileExists(actualFile + ".dll")) { + StartProcess("./tools/pdb2mdb/pdb2mdb.exe", new ProcessSettings() + .WithArguments(args => args.Append(actualFile + ".dll"))); + } + } +} + +// Build Tasks +Task("Compile").Does(() => { + // Build + if (DirectoryExists(outputFolder)) { + DeleteDirectory(outputFolder, true); + } + + MSBuild(solutionFile, config => + config.UseToolVersion(MSBuildToolVersion.VS2015) + .WithTarget("Clean") + .SetVerbosity(Verbosity.Minimal)); + + NuGetRestore(solutionFile); + + MSBuild(solutionFile, config => + config.UseToolVersion(MSBuildToolVersion.VS2015) + .SetPlatformTarget(PlatformTarget.x86) + .SetConfiguration("Release") + .WithProperty("AllowedReferenceRelatedFileExtensions", new string[] { ".pdb" }) + .WithTarget("Build") + .SetVerbosity(Verbosity.Minimal)); + + CleanFolder(outputFolder, false); + + // Add JsonNet + DeleteFiles(outputFolder + "/Newtonsoft.Json.*"); + CopyFiles(sourceFolder + "/packages/Newtonsoft.Json.*/lib/net35/*.dll", outputFolder); + CopyFiles(sourceFolder + "/packages/Newtonsoft.Json.*/lib/net35/*.dll", updateFolder); + + // Remove Mono stuff + DeleteFile(outputFolder + "/Mono.Posix.dll"); +}); + +Task("Gulp").Does(() => { + Npm + .WithLogLevel(NpmLogLevel.Silent) + .FromPath(".") + .Install() + .RunScript("build"); +}); + +Task("PackageMono").Does(() => { + // Start mono package + if (DirectoryExists(outputFolderMono)) { + DeleteDirectory(outputFolderMono, true); + } + + CopyDirectory(outputFolder, outputFolderMono); + + // Create MDBs + CreateMdbs(outputFolderMono); + + // Remove PDBs + DeleteFiles(outputFolderMono + "/**/*.pdb"); + + // Remove service helpers + DeleteFiles(outputFolderMono + "/ServiceUninstall.*"); + DeleteFiles(outputFolderMono + "/ServiceInstall.*"); + + // Remove native windows binaries + DeleteFiles(outputFolderMono + "/sqlite3.*"); + DeleteFiles(outputFolderMono + "/MediaInfo.*"); + + // Adding NzbDrone.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", outputFolderMono + "/NzbDrone.Core.dll.config"); + + // Adding CurlSharp.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", outputFolderMono + "/CurlSharp.dll.config"); + + // Renaming Radarr.Console.exe to Radarr.exe + DeleteFiles(outputFolderMono + "/Radarr.exe*"); + MoveFile(outputFolderMono + "/Radarr.Console.exe", outputFolderMono + "/Radarr.exe"); + MoveFile(outputFolderMono + "/Radarr.Console.exe.config", outputFolderMono + "/Radarr.exe.config"); + MoveFile(outputFolderMono + "/Radarr.Console.exe.mdb", outputFolderMono + "/Radarr.exe.mdb"); + + // Remove NzbDrone.Windows.* + DeleteFiles(outputFolderMono + "/NzbDrone.Windows.*"); + + // Adding NzbDrone.Mono to updatePackage + CopyFiles(outputFolderMono + "/NzbDrone.Mono.*", updateFolderMono); +}); + +Task("PackageOsx").Does(() => { + // Start osx package + if (DirectoryExists(outputFolderOsx)) { + DeleteDirectory(outputFolderOsx, true); + } + + CopyDirectory(outputFolderMono, outputFolderOsx); + + // Adding sqlite dylibs + CopyFiles(sourceFolder + "/Libraries/Sqlite/*.dylib", outputFolderOsx); + + // Adding MediaInfo dylib + CopyFiles(sourceFolder + "/Libraries/MediaInfo/*.dylib", outputFolderOsx); + + // Adding Startup script + CopyFile("./osx/Sonarr", outputFolderOsx + "/Sonarr"); +}); + +Task("PackageOsxApp").Does(() => { + // Start osx app package + if (DirectoryExists(outputFolderOsxApp)) { + DeleteDirectory(outputFolderOsxApp, true); + } + + CreateDirectory(outputFolderOsxApp); + + // Copy osx package files + CopyDirectory("./osx/Radarr.app", outputFolderOsxApp + "/Radarr.app"); + CopyDirectory(outputFolderOsx, outputFolderOsxApp + "/Radarr.app/Contents/MacOS"); +}); + +Task("PackageTests").Does(() => { + // Start tests package + if (DirectoryExists(testPackageFolder)) { + DeleteDirectory(testPackageFolder, true); + } + + CreateDirectory(testPackageFolder); + + // Copy tests + CopyFiles(sourceFolder + "/" + testSearchPattern + "/*", testPackageFolder); + foreach (var directory in System.IO.Directory.GetDirectories(sourceFolder, "*.Test")) { + var releaseDirectory = directory + "/bin/x86/Release"; + if (DirectoryExists(releaseDirectory)) { + foreach (var releaseSubDirectory in System.IO.Directory.GetDirectories(releaseDirectory)) { + Information(System.IO.Path.GetDirectoryName(releaseSubDirectory)); + CopyDirectory(releaseSubDirectory, testPackageFolder + "/" + System.IO.Path.GetFileName(releaseSubDirectory)); + } + } + } + + // Install NUnit.ConsoleRunner + NuGetInstall("NUnit.ConsoleRunner", new NuGetInstallSettings { + Version = "3.2.0", + OutputDirectory = testPackageFolder + }); + + // Copy dlls + CopyFiles(outputFolder + "/*.dll", testPackageFolder); + + // Copy scripts + CopyFiles("./*.sh", testPackageFolder); + + // Create MDBs for tests + CreateMdbs(testPackageFolder); + + // Remove config + DeleteFiles(testPackageFolder + "/*.log.config"); + + // Clean + CleanFolder(testPackageFolder, true); + + // Adding NzbDrone.Core.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Core/NzbDrone.Core.dll.config", testPackageFolder + "/NzbDrone.Core.dll.config"); + + // Adding CurlSharp.dll.config (for dllmap) + CopyFile(sourceFolder + "/NzbDrone.Common/CurlSharp.dll.config", testPackageFolder + "/CurlSharp.dll.config"); + + // Adding CurlSharp libraries + CopyFiles(sourceFolder + "/ExternalModules/CurlSharp/libs/i386/*", testPackageFolder); +}); + +Task("CleanupWindowsPackage").Does(() => { + // Remove mono + DeleteFiles(outputFolder + "/NzbDrone.Mono.*"); + + // Adding NzbDrone.Windows to updatePackage + CopyFiles(outputFolder + "/NzbDrone.Windows.*", updateFolder); +}); + +Task("Build") + .IsDependentOn("Compile") + .IsDependentOn("Gulp") + .IsDependentOn("PackageMono") + .IsDependentOn("PackageOsx") + .IsDependentOn("PackageOsxApp") + .IsDependentOn("PackageTests") + .IsDependentOn("CleanupWindowsPackage"); + +// Build Artifacts +Task("CleanArtifacts").Does(() => { + if (DirectoryExists(artifactsFolder)) { + DeleteDirectory(artifactsFolder, true); + } + + CreateDirectory(artifactsFolder); +}); + +Task("ArtifactsWindows").Does(() => { + CopyDirectory(outputFolder, artifactsFolderWindows + "/Radarr"); +}); + +Task("ArtifactsLinux").Does(() => { + CopyDirectory(outputFolderMono, artifactsFolderLinux + "/Radarr"); +}); + +Task("ArtifactsOsx").Does(() => { + CopyDirectory(outputFolderOsx, artifactsFolderOsx + "/Radarr"); +}); + +Task("ArtifactsOsxApp").Does(() => { + CopyDirectory(outputFolderOsxApp, artifactsFolderOsxApp); +}); + +Task("CompressArtifacts").Does(() => { + var prefix = ""; + + if (AppVeyor.IsRunningOnAppVeyor) { + prefix += AppVeyor.Environment.Repository.Branch + "."; + prefix += AppVeyor.Environment.Build.Version + "."; + } + + Zip(artifactsFolderWindows, artifactsFolder + "/Radarr." + prefix + "windows.zip"); + GZipCompress(artifactsFolderLinux, artifactsFolder + "/Radarr." + prefix + "linux.tar.gz"); + GZipCompress(artifactsFolderOsx, artifactsFolder + "/Radarr." + prefix + "osx.tar.gz"); + Zip(artifactsFolderOsxApp, artifactsFolder + "/Radarr." + prefix + "osx-app.zip"); +}); + +Task("Artifacts") + .IsDependentOn("CleanArtifacts") + .IsDependentOn("ArtifactsWindows") + .IsDependentOn("ArtifactsLinux") + .IsDependentOn("ArtifactsOsx") + .IsDependentOn("ArtifactsOsxApp") + .IsDependentOn("CompressArtifacts"); + +// Run +RunTarget("Build"); +RunTarget("Artifacts"); \ No newline at end of file diff --git a/build-appveyor.ps1 b/build-appveyor.ps1 new file mode 100644 index 000000000..fd3bea746 --- /dev/null +++ b/build-appveyor.ps1 @@ -0,0 +1,184 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. +.LINK +http://cakebuild.net +#> + +[CmdletBinding()] +Param( + [string]$Script = "build-appveyor.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools-cake" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/build.sh b/build.sh index e45c949e9..a3a6eb568 100755 --- a/build.sh +++ b/build.sh @@ -154,8 +154,8 @@ PackageMono() cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderMono echo "Renaming NzbDrone.Console.exe to NzbDrone.exe" - rm $outputFolderMono/NzbDrone.exe* - for file in $outputFolderMono/NzbDrone.Console.exe*; do + rm $outputFolderMono/Radarr.exe* + for file in $outputFolderMono/Radarr.Console.exe*; do mv "$file" "${file//.Console/}" done @@ -192,8 +192,8 @@ PackageOsxApp() rm -rf $outputFolderOsxApp mkdir $outputFolderOsxApp - cp -r ./osx/Sonarr.app $outputFolderOsxApp - cp -r $outputFolderOsx $outputFolderOsxApp/Sonarr.app/Contents/MacOS + cp -r ./osx/Radarr.app $outputFolderOsxApp + cp -r $outputFolderOsx $outputFolderOsxApp/Radarr.app/Contents/MacOS echo "##teamcity[progressFinish 'Creating OS X App Package']" } diff --git a/gulp/less.js b/gulp/less.js index 76e04b8dc..92d50a43e 100644 --- a/gulp/less.js +++ b/gulp/less.js @@ -19,13 +19,16 @@ gulp.task('less', function() { paths.src.root + 'Series/series.less', paths.src.root + 'Activity/activity.less', paths.src.root + 'AddSeries/addSeries.less', + paths.src.root + 'AddMovies/addMovies.less', paths.src.root + 'Calendar/calendar.less', paths.src.root + 'Cells/cells.less', paths.src.root + 'ManualImport/manualimport.less', paths.src.root + 'Settings/settings.less', paths.src.root + 'System/Logs/logs.less', paths.src.root + 'System/Update/update.less', - paths.src.root + 'System/Info/info.less' + paths.src.root + 'System/Info/info.less', + paths.src.root + 'Movies/movies.less', + ]; return gulp.src(src) diff --git a/osx/Sonarr.app/Contents/Info.plist b/osx/Radarr.app/Contents/Info.plist similarity index 97% rename from osx/Sonarr.app/Contents/Info.plist rename to osx/Radarr.app/Contents/Info.plist index eeae50f41..345002166 100644 --- a/osx/Sonarr.app/Contents/Info.plist +++ b/osx/Radarr.app/Contents/Info.plist @@ -13,7 +13,7 @@ CFBundleExecutable Sonarr CFBundleIconFile - sonarr.icns + radarr.icns CFBundleIdentifier com.osx.sonarr.tv CFBundleInfoDictionaryVersion diff --git a/osx/Radarr.app/Contents/Resources/radarr.icns b/osx/Radarr.app/Contents/Resources/radarr.icns new file mode 100644 index 000000000..5284eec97 Binary files /dev/null and b/osx/Radarr.app/Contents/Resources/radarr.icns differ diff --git a/osx/Sonarr.app/Contents/Resources/sonarr.icns b/osx/Radarr.app/Contents/Resources/sonarr.icns similarity index 100% rename from osx/Sonarr.app/Contents/Resources/sonarr.icns rename to osx/Radarr.app/Contents/Resources/sonarr.icns diff --git a/osx/Sonarr b/osx/Sonarr index db2a35399..bb5d9d6bd 100644 --- a/osx/Sonarr +++ b/osx/Sonarr @@ -4,9 +4,9 @@ DIR=$(cd "$(dirname "$0")"; pwd) #change these values to match your app -EXE_PATH="$DIR/NzbDrone.exe" +EXE_PATH="$DIR/Radarr.exe" APPNAME="Sonarr" - + #set up environment if [[ -x '/opt/local/bin/mono' ]]; then export PATH="/opt/local/bin:$PATH" @@ -29,11 +29,11 @@ export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$HOME/lib:/usr/lo #mono version check REQUIRED_MAJOR=3 REQUIRED_MINOR=10 - + VERSION_TITLE="Cannot launch $APPNAME" VERSION_MSG="$APPNAME requires Mono Runtime Environment(MRE) $REQUIRED_MAJOR.$REQUIRED_MINOR or later." DOWNLOAD_URL="http://www.mono-project.com/download/#download-mac" - + MONO_VERSION="$(mono --version | grep 'Mono JIT compiler version ' | cut -f5 -d\ )" # if [[ -o DEBUG ]]; then osascript -e "display dialog \"MONO_VERSION: $MONO_VERSION\""; fi @@ -42,7 +42,7 @@ MONO_VERSION_MAJOR="$(echo $MONO_VERSION | cut -f1 -d.)" MONO_VERSION_MINOR="$(echo $MONO_VERSION | cut -f2 -d.)" if [ -z "$MONO_VERSION" ] \ || [ $MONO_VERSION_MAJOR -lt $REQUIRED_MAJOR ] \ - || [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ] + || [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ] then osascript \ -e "set question to display dialog \"$VERSION_MSG\" with title \"$VERSION_TITLE\" buttons {\"Cancel\", \"Download...\"} default button 2" \ @@ -51,8 +51,8 @@ then echo "$VERSION_MSG" exit 1 fi - + MONO_EXEC="exec mono --debug" - + #run app using mono -$MONO_EXEC "$EXE_PATH" \ No newline at end of file +$MONO_EXEC "$EXE_PATH" diff --git a/package.sh b/package.sh new file mode 100644 index 000000000..679786658 --- /dev/null +++ b/package.sh @@ -0,0 +1,68 @@ +if [ $# -eq 0 ]; then + if [ "$TRAVIS_PULL_REQUEST" != false ]; then + echo "Need to supply version argument" && exit; + fi +fi + +# Use mono or .net depending on OS +case "$(uname -s)" in + CYGWIN*|MINGW32*|MINGW64*|MSYS*) + # on windows, use dotnet + runtime="dotnet" + ;; + *) + # otherwise use mono + runtime="mono" + ;; +esac + +if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + VERSION="`date +%H:%M:%S`" + YEAR="`date +%Y`" + MONTH="`date +%m`" + DAY="`date +%d`" +else + VERSION=$1 +fi +outputFolder='./_output' +outputFolderMono='./_output_mono' +outputFolderOsx='./_output_osx' +outputFolderOsxApp='./_output_osx_app' + +tr -d "\r" < $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr > $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr2 +rm $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr +chmod +x $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr2 +mv $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr2 $outputFolderOsxApp/Radarr.app/Contents/MacOS/Sonarr >& error.log + +cp -r $outputFolder/ Radarr_Windows_$VERSION +cp -r $outputFolderMono/ Radarr_Mono_$VERSION +cp -r $outputFolderOsxApp/ Radarr_OSX_$VERSION + +if [ $runtime = "dotnet" ] ; then + ./7za.exe a Radarr_Windows_$VERSION.zip ./Radarr_Windows_$VERSION/* + ./7za.exe a -ttar -so Radarr_Mono_$VERSION.tar ./Radarr_Mono_$VERSION/* | ./7za.exe a -si Radarr_Mono_$VERSION.tar.gz + ./7za.exe a -ttar -so Radarr_OSX_$VERSION.tar ./_output_osx/* | ./7za.exe a -si Radarr_OSX_$VERSION.tar.gz + ./7za.exe a -ttar -so Radarr_OSX_App_$VERSION.tar ./_output_osx_app/* | ./7za.exe a -si Radarr_OSX_App_$VERSION.tar.gz +else +zip -r Radarr_Windows_$VERSION.zip Radarr_Windows_$VERSION/* >& /dev/null +zip -r Radarr_Mono_$VERSION.zip Radarr_Mono_$VERSION/* >& /dev/null #TODO update for tar.gz +zip -r Radarr_OSX_$VERSION_App.zip Radarr_OSX_$VERSION/* >& /dev/null +fi +ftp -n ftp.leonardogalli.ch << END_SCRIPT +passive +quote USER $FTP_USER +quote PASS $FTP_PASS +mkdir builds +cd builds +mkdir $YEAR +cd $YEAR +mkdir $MONTH +cd $MONTH +mkdir $DAY +cd $DAY +binary +put Radarr_Windows_$VERSION.zip +put Radarr_Mono_$VERSION.zip +put Radarr_OSX_$VERSION.zip +quit +END_SCRIPT diff --git a/readme.md b/readme.md index 495dd4155..86665c6fd 100644 --- a/readme.md +++ b/readme.md @@ -1,27 +1,57 @@ -# Sonarr # +# Radarr +| Service | Master | Develop | +|----------|:---------------------------:|:----------------------------:| +| AppVeyor | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr/master.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr) | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr-usby1/develop.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr-usby1) | +| Travis | [![Travis](https://img.shields.io/travis/galli-leo/Radarr/master.svg?maxAge=60&style=flat-square)](https://travis-ci.org/galli-leo/Radarr) | [![Travis](https://img.shields.io/travis/galli-leo/Radarr/develop.svg?maxAge=60&style=flat-square)](https://travis-ci.org/galli-leo/Radarr) | -Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available. +This fork of Sonarr aims to turn it into something like Couchpotato. + +## Currently working: +* Adding new movies +* Manually searching for releases of movies. +* Automatically searching for releases. +* Automatically importing downloaded movies. +* Recognizing Special Editions, Director's Cut, etc. +* Identifying releases with hardcoded subs. +* Rarbg.to, Torznab and Newznab Indexer. +* QBittorrent and Deluge download client (Other clients are coming) +* New TorrentPotato Indexer (Works well with [Jackett](https://github.com/Jackett/Jackett)) + +## Planned Features: +* Scanning PreDB to know when a new release is available. +* Fixing the other Indexers and download clients. +* Importing of Sonarr config. + +## Download +The latest precompiled binary versions can be found here: https://github.com/galli-leo/Radarr/releases. + +To connect to the UI, fire up your browser and open localhost:7878 or your-ip:7878. + +Docker containers from [linuxserver.io](https://linuxserver.io) can be found here. +* [Radarr (x64)](https://hub.docker.com/r/linuxserver/radarr/) +* [Radarr (armhf)](https://hub.docker.com/r/lsioarmhf/radarr/) +* [Radarr (aarch64)](https://hub.docker.com/r/lsioarmhf/radarr-aarch64/) + +For more up to date versions (but also sometimes broken), daily builds can be found here: +* [OSX](https://leonardogalli.ch/radarr/builds/latest.php?os=osx) +* [Windows](https://leonardogalli.ch/radarr/builds/latest.php?os=windows) +* [Linux](https://leonardogalli.ch/radarr/builds/latest.php?os=mono) ## Major Features Include: ## * Support for major platforms: Windows, Linux, OSX, Raspberry Pi, etc. -* Automatically detects new episodes -* Can scan your existing library and download any missing episodes -* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray* +* Can watch for better quality of the movies you have and do an upgrade. * Automatic failed download handling will try another release if one fails -* Manual search so you can pick any release or to see why a release was not downloaded automatically -* Fully configurable episode renaming -* Full integration with SABNzbd and NzbGet -* Full integration with XBMC, Plex (notification, library update, metadata) -* Full support for specials and multi-episode releases +* Manual search so you can pick any release or to see why a release was not downloaded automatically. +* Full integration with SABNzbd and NzbGet. +* Full integration with XBMC, Plex (notification, library update, metadata). * And a beautiful UI - ## Configuring Development Environment: ## ### Requirements ### -- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) +- Visual Studio 2015 [Free Community Edition](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) or Mono - [Git](http://git-scm.com/downloads) - [NodeJS](http://nodejs.org/download/) @@ -37,7 +67,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee ### Development ### -- Open `NzbDrone.sln` in Visual Studio +- Open `NzbDrone.sln` in Visual Studio or run the build.sh script, if Mono is installed. - Make sure `NzbDrone.Console` is set as the startup project diff --git a/sonarr.icns b/sonarr.icns new file mode 100644 index 000000000..5284eec97 Binary files /dev/null and b/sonarr.icns differ diff --git a/src/Common/CommonVersionInfo.cs b/src/Common/CommonVersionInfo.cs index d674c376f..f7e96bcb8 100644 --- a/src/Common/CommonVersionInfo.cs +++ b/src/Common/CommonVersionInfo.cs @@ -2,4 +2,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.0.0.*")] +[assembly: AssemblyVersion("0.1.0.*")] diff --git a/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs index 4d2901c1a..b45cbd098 100644 --- a/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Api.Test/Properties/AssemblyInfo.cs @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("260b2ff9-d3b7-4d8a-b720-a12c93d045e5")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs index 0e62517f9..685c5cf16 100644 --- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Api.Calendar Get["/NzbDrone.ics"] = options => GetCalendarFeed(); Get["/Sonarr.ics"] = options => GetCalendarFeed(); + Get["/Radarr.ics"] = options => GetCalendarFeed(); } private Response GetCalendarFeed() diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index 0b72e0b0c..94b515473 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -34,11 +34,13 @@ namespace NzbDrone.Api.Config Get["/samples"] = x => GetExamples(this.Bind()); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 5); - SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + /*SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); - SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); + SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();*/ + SharedValidator.RuleFor(c => c.StandardMovieFormat).ValidMovieFormat(); + SharedValidator.RuleFor(c => c.MovieFolderFormat).ValidMovieFolderFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -54,7 +56,13 @@ namespace NzbDrone.Api.Config var nameSpec = _namingConfigService.GetConfig(); var resource = nameSpec.ToResource(); - if (resource.StandardEpisodeFormat.IsNotNullOrWhiteSpace()) + //if (resource.StandardEpisodeFormat.IsNotNullOrWhiteSpace()) + //{ + // var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); + // basicConfig.AddToResource(resource); + //} + + if (resource.StandardMovieFormat.IsNotNullOrWhiteSpace()) { var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); basicConfig.AddToResource(resource); @@ -73,39 +81,50 @@ namespace NzbDrone.Api.Config var nameSpec = config.ToModel(); var sampleResource = new NamingSampleResource(); - var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); - var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); - var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); - var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); - var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); + //var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + //var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + //var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); + //var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); + //var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); - sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null - ? "Invalid format" - : singleEpisodeSampleResult.FileName; + var movieSampleResult = _filenameSampleService.GetMovieSample(nameSpec); + - sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null - ? "Invalid format" - : multiEpisodeSampleResult.FileName; + //sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null + // ? "Invalid format" + // : singleEpisodeSampleResult.FileName; - sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null - ? "Invalid format" - : dailyEpisodeSampleResult.FileName; + //sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null + // ? "Invalid format" + // : multiEpisodeSampleResult.FileName; - sampleResource.AnimeEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult) != null - ? "Invalid format" - : animeEpisodeSampleResult.FileName; + //sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null + // ? "Invalid format" + // : dailyEpisodeSampleResult.FileName; - sampleResource.AnimeMultiEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult) != null - ? "Invalid format" - : animeMultiEpisodeSampleResult.FileName; + //sampleResource.AnimeEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult) != null + // ? "Invalid format" + // : animeEpisodeSampleResult.FileName; - sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace() + //sampleResource.AnimeMultiEpisodeExample = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult) != null + // ? "Invalid format" + // : animeMultiEpisodeSampleResult.FileName; + + sampleResource.MovieExample = nameSpec.StandardMovieFormat.IsNullOrWhiteSpace() + ? "Invalid Format" + : movieSampleResult.FileName; + + //sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace() + // ? "Invalid format" + // : _filenameSampleService.GetSeriesFolderSample(nameSpec); + + //sampleResource.SeasonFolderExample = nameSpec.SeasonFolderFormat.IsNullOrWhiteSpace() + // ? "Invalid format" + // : _filenameSampleService.GetSeasonFolderSample(nameSpec); + + sampleResource.MovieFolderExample = nameSpec.MovieFolderFormat.IsNullOrWhiteSpace() ? "Invalid format" - : _filenameSampleService.GetSeriesFolderSample(nameSpec); - - sampleResource.SeasonFolderExample = nameSpec.SeasonFolderFormat.IsNullOrWhiteSpace() - ? "Invalid format" - : _filenameSampleService.GetSeasonFolderSample(nameSpec); + : _filenameSampleService.GetMovieFolderSample(nameSpec); return sampleResource.AsResponse(); } @@ -118,19 +137,25 @@ namespace NzbDrone.Api.Config var animeEpisodeSampleResult = _filenameSampleService.GetAnimeSample(nameSpec); var animeMultiEpisodeSampleResult = _filenameSampleService.GetAnimeMultiEpisodeSample(nameSpec); + var movieSampleResult = _filenameSampleService.GetMovieSample(nameSpec); + var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); var animeEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeEpisodeSampleResult); var animeMultiEpisodeValidationResult = _filenameValidationService.ValidateAnimeFilename(animeMultiEpisodeSampleResult); + //var standardMovieValidationResult = _filenameValidationService.ValidateMovieFilename(movieSampleResult); For now, let's hope the user is not stupid enough :/ + var validationFailures = new List(); - validationFailures.AddIfNotNull(singleEpisodeValidationResult); - validationFailures.AddIfNotNull(multiEpisodeValidationResult); - validationFailures.AddIfNotNull(dailyEpisodeValidationResult); - validationFailures.AddIfNotNull(animeEpisodeValidationResult); - validationFailures.AddIfNotNull(animeMultiEpisodeValidationResult); + //validationFailures.AddIfNotNull(singleEpisodeValidationResult); + //validationFailures.AddIfNotNull(multiEpisodeValidationResult); + //validationFailures.AddIfNotNull(dailyEpisodeValidationResult); + //validationFailures.AddIfNotNull(animeEpisodeValidationResult); + //validationFailures.AddIfNotNull(animeMultiEpisodeValidationResult); + + //validationFailures.AddIfNotNull(standardMovieValidationResult); if (validationFailures.Any()) { diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index 39147b993..f65d90e48 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -7,6 +7,8 @@ namespace NzbDrone.Api.Config { public bool RenameEpisodes { get; set; } public bool ReplaceIllegalCharacters { get; set; } + public string StandardMovieFormat { get; set; } + public string MovieFolderFormat { get; set; } public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } @@ -36,7 +38,9 @@ namespace NzbDrone.Api.Config DailyEpisodeFormat = model.DailyEpisodeFormat, AnimeEpisodeFormat = model.AnimeEpisodeFormat, SeriesFolderFormat = model.SeriesFolderFormat, - SeasonFolderFormat = model.SeasonFolderFormat + SeasonFolderFormat = model.SeasonFolderFormat, + StandardMovieFormat = model.StandardMovieFormat, + MovieFolderFormat = model.MovieFolderFormat //IncludeSeriesTitle //IncludeEpisodeTitle //IncludeQuality @@ -64,12 +68,14 @@ namespace NzbDrone.Api.Config RenameEpisodes = resource.RenameEpisodes, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, - MultiEpisodeStyle = resource.MultiEpisodeStyle, - StandardEpisodeFormat = resource.StandardEpisodeFormat, - DailyEpisodeFormat = resource.DailyEpisodeFormat, - AnimeEpisodeFormat = resource.AnimeEpisodeFormat, - SeriesFolderFormat = resource.SeriesFolderFormat, - SeasonFolderFormat = resource.SeasonFolderFormat + //MultiEpisodeStyle = resource.MultiEpisodeStyle, + //StandardEpisodeFormat = resource.StandardEpisodeFormat, + //DailyEpisodeFormat = resource.DailyEpisodeFormat, + //AnimeEpisodeFormat = resource.AnimeEpisodeFormat, + //SeriesFolderFormat = resource.SeriesFolderFormat, + //SeasonFolderFormat = resource.SeasonFolderFormat, + StandardMovieFormat = resource.StandardMovieFormat, + MovieFolderFormat = resource.MovieFolderFormat }; } } diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs index 1f9c7f066..3430050e0 100644 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ b/src/NzbDrone.Api/Config/NamingSampleResource.cs @@ -9,5 +9,8 @@ public string AnimeMultiEpisodeExample { get; set; } public string SeriesFolderExample { get; set; } public string SeasonFolderExample { get; set; } + + public string MovieExample { get; set; } + public string MovieFolderExample { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs index 9e4912524..8e8393ef6 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { - return resourceUrl.StartsWith("/backup/") && resourceUrl.ContainsIgnoreCase("nzbdrone_backup_") && resourceUrl.EndsWith(".zip"); + return resourceUrl.StartsWith("/backup/") && resourceUrl.ContainsIgnoreCase("radarr_backup_") && resourceUrl.EndsWith(".zip"); } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index d85cf74d8..a17b9963c 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -3,6 +3,7 @@ using Nancy; using NzbDrone.Api.Episodes; using NzbDrone.Api.Extensions; using NzbDrone.Api.Series; +using NzbDrone.Api.Movie; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; @@ -34,12 +35,18 @@ namespace NzbDrone.Api.History resource.Series = model.Series.ToResource(); resource.Episode = model.Episode.ToResource(); + resource.Movie = model.Movie.ToResource(); if (model.Series != null) { resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Series.Profile.Value, model.Quality); } + if (model.Movie != null) + { + resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Movie.Profile.Value, model.Quality); + } + return resource; } @@ -47,6 +54,8 @@ namespace NzbDrone.Api.History { var episodeId = Request.Query.EpisodeId; + var movieId = Request.Query.MovieId; + var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); if (pagingResource.FilterKey == "eventType") @@ -61,6 +70,12 @@ namespace NzbDrone.Api.History pagingSpec.FilterExpression = h => h.EpisodeId == i; } + if (movieId.HasValue) + { + int i = (int)movieId; + pagingSpec.FilterExpression = h => h.MovieId == i; + } + return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource); } diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index dba4149dd..93ec372c7 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Api.Episodes; using NzbDrone.Api.REST; using NzbDrone.Api.Series; +using NzbDrone.Api.Movie; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; @@ -12,6 +13,7 @@ namespace NzbDrone.Api.History public class HistoryResource : RestResource { public int EpisodeId { get; set; } + public int MovieId { get; set; } public int SeriesId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } @@ -22,7 +24,7 @@ namespace NzbDrone.Api.History public HistoryEventType EventType { get; set; } public Dictionary Data { get; set; } - + public MovieResource Movie { get; set; } public EpisodeResource Episode { get; set; } public SeriesResource Series { get; set; } } @@ -39,6 +41,7 @@ namespace NzbDrone.Api.History EpisodeId = model.EpisodeId, SeriesId = model.SeriesId, + MovieId = model.MovieId, SourceTitle = model.SourceTitle, Quality = model.Quality, //QualityCutoffNotMet diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 5729af932..7f92215fb 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Api.Indexers private readonly Logger _logger; private readonly ICached _remoteEpisodeCache; + private readonly ICached _remoteMovieCache; public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, ISearchForNzb nzbSearchService, @@ -49,6 +50,7 @@ namespace NzbDrone.Api.Indexers PostValidator.RuleFor(s => s.Guid).NotEmpty(); _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); + _remoteMovieCache = cacheManager.GetCache(GetType(), "remoteMovies"); } private Response DownloadRelease(ReleaseResource release) @@ -59,7 +61,26 @@ namespace NzbDrone.Api.Indexers { _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); - return new NotFoundResponse(); + var remoteMovie = _remoteMovieCache.Find(release.Guid); + + if (remoteMovie == null) + { + return new NotFoundResponse(); + } + + try + { + _downloadService.DownloadReport(remoteMovie); + } + catch (ReleaseDownloadException ex) + { + _logger.Error(ex, ex.Message); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); + } + + return release.AsResponse(); + + } try @@ -82,6 +103,11 @@ namespace NzbDrone.Api.Indexers return GetEpisodeReleases(Request.Query.episodeId); } + if (Request.Query.movieId != null) + { + return GetMovieReleases(Request.Query.movieId); + } + return GetRss(); } @@ -102,6 +128,27 @@ namespace NzbDrone.Api.Indexers return new List(); } + private List GetMovieReleases(int movieId) + { + try + { + var decisions = _nzbSearchService.MovieSearch(movieId, true); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisionsForMovies(decisions); + + return MapDecisions(prioritizedDecisions); + } + catch (NotImplementedException ex) + { + _logger.Error(ex, "One or more indexer you selected does not support movie search yet: " + ex.Message); + } + catch (Exception ex) + { + _logger.Error(ex, "Movie search failed: " + ex.Message); + } + + return new List(); + } + private List GetRss() { var reports = _rssFetcherAndParser.Fetch(); @@ -113,7 +160,15 @@ namespace NzbDrone.Api.Indexers protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) { - _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + if (decision.IsForMovie) + { + _remoteMovieCache.Set(decision.RemoteMovie.Release.Guid, decision.RemoteMovie, TimeSpan.FromMinutes(30)); + } + else + { + _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + } + return base.MapDecision(decision, initialWeight); } } diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index b951b0fe0..2be0acbb9 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Api.Indexers public string Indexer { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } + public string Edition { get; set; } public string Title { get; set; } public bool FullSeason { get; set; } public int SeasonNumber { get; set; } @@ -86,6 +87,60 @@ namespace NzbDrone.Api.Indexers var parsedEpisodeInfo = model.RemoteEpisode.ParsedEpisodeInfo; var remoteEpisode = model.RemoteEpisode; var torrentInfo = (model.RemoteEpisode.Release as TorrentInfo) ?? new TorrentInfo(); + var downloadAllowed = model.RemoteEpisode.DownloadAllowed; + if (model.IsForMovie) + { + downloadAllowed = model.RemoteMovie.DownloadAllowed; + var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo; + + return new ReleaseResource + { + Guid = releaseInfo.Guid, + Quality = parsedMovieInfo.Quality, + QualityWeight = parsedMovieInfo.Quality.Quality.Id, //Id kinda hacky for wheight, but what you gonna do? TODO: Fix this shit! + Age = releaseInfo.Age, + AgeHours = releaseInfo.AgeHours, + AgeMinutes = releaseInfo.AgeMinutes, + Size = releaseInfo.Size, + IndexerId = releaseInfo.IndexerId, + Indexer = releaseInfo.Indexer, + ReleaseGroup = parsedMovieInfo.ReleaseGroup, + ReleaseHash = parsedMovieInfo.ReleaseHash, + Title = releaseInfo.Title, + //FullSeason = parsedMovieInfo.FullSeason, + //SeasonNumber = parsedMovieInfo.SeasonNumber, + Language = parsedMovieInfo.Language, + AirDate = "", + SeriesTitle = parsedMovieInfo.MovieTitle, + EpisodeNumbers = new int[0], + AbsoluteEpisodeNumbers = new int[0], + Approved = model.Approved, + TemporarilyRejected = model.TemporarilyRejected, + Rejected = model.Rejected, + TvdbId = releaseInfo.TvdbId, + TvRageId = releaseInfo.TvRageId, + Rejections = model.Rejections.Select(r => r.Reason).ToList(), + PublishDate = releaseInfo.PublishDate, + CommentUrl = releaseInfo.CommentUrl, + DownloadUrl = releaseInfo.DownloadUrl, + InfoUrl = releaseInfo.InfoUrl, + DownloadAllowed = downloadAllowed, + //ReleaseWeight + + MagnetUrl = torrentInfo.MagnetUrl, + InfoHash = torrentInfo.InfoHash, + Seeders = torrentInfo.Seeders, + Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, + Protocol = releaseInfo.DownloadProtocol, + + Edition = parsedMovieInfo.Edition, + + IsDaily = false, + IsAbsoluteNumbering = false, + IsPossibleSpecialEpisode = false, + //Special = parsedMovieInfo.Special, + }; + } // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) return new ReleaseResource @@ -119,7 +174,7 @@ namespace NzbDrone.Api.Indexers CommentUrl = releaseInfo.CommentUrl, DownloadUrl = releaseInfo.DownloadUrl, InfoUrl = releaseInfo.InfoUrl, - DownloadAllowed = remoteEpisode.DownloadAllowed, + DownloadAllowed = downloadAllowed, //ReleaseWeight MagnetUrl = torrentInfo.MagnetUrl, diff --git a/src/NzbDrone.Api/Movies/MovieEditorModule.cs b/src/NzbDrone.Api/Movies/MovieEditorModule.cs new file mode 100644 index 000000000..ca744a099 --- /dev/null +++ b/src/NzbDrone.Api/Movies/MovieEditorModule.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Movie +{ + public class MovieEditorModule : NzbDroneApiModule + { + private readonly IMovieService _movieService; + + public MovieEditorModule(IMovieService movieService) + : base("/movie/editor") + { + _movieService = movieService; + Put["/"] = Movie => SaveAll(); + } + + private Response SaveAll() + { + var resources = Request.Body.FromJson>(); + + var Movie = resources.Select(MovieResource => MovieResource.ToModel(_movieService.GetMovie(MovieResource.Id))).ToList(); + + return _movieService.UpdateMovie(Movie) + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/NzbDrone.Api/Movies/MovieFileModule.cs b/src/NzbDrone.Api/Movies/MovieFileModule.cs new file mode 100644 index 000000000..a45fbefad --- /dev/null +++ b/src/NzbDrone.Api/Movies/MovieFileModule.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.IO; +using NLog; +using NzbDrone.Api.REST; +using NzbDrone.Api.Movie; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.EpisodeFiles +{ + public class MovieFileModule : NzbDroneRestModuleWithSignalR + //IHandle + { + private readonly IMediaFileService _mediaFileService; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly IMovieService _seriesService; + private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly Logger _logger; + + public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IRecycleBinProvider recycleBinProvider, + IMovieService seriesService, + IQualityUpgradableSpecification qualityUpgradableSpecification, + Logger logger) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _recycleBinProvider = recycleBinProvider; + _seriesService = seriesService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + _logger = logger; + /*GetResourceById = GetEpisodeFile; + GetResourceAll = GetEpisodeFiles; + UpdateResource = SetQuality;*/ + DeleteResource = DeleteEpisodeFile; + } + + /*private EpisodeFileResource GetEpisodeFile(int id) + { + var episodeFile = _mediaFileService.Get(id); + var series = _seriesService.GetSeries(episodeFile.SeriesId); + + return episodeFile.ToResource(series, _qualityUpgradableSpecification); + } + + private List GetEpisodeFiles() + { + if (!Request.Query.SeriesId.HasValue) + { + throw new BadRequestException("seriesId is missing"); + } + + var seriesId = (int)Request.Query.SeriesId; + + var series = _seriesService.GetSeries(seriesId); + + return _mediaFileService.GetFilesBySeries(seriesId).ConvertAll(f => f.ToResource(series, _qualityUpgradableSpecification)); + } + + private void SetQuality(EpisodeFileResource episodeFileResource) + { + var episodeFile = _mediaFileService.Get(episodeFileResource.Id); + episodeFile.Quality = episodeFileResource.Quality; + _mediaFileService.Update(episodeFile); + }*/ + + private void DeleteEpisodeFile(int id) + { + var episodeFile = _mediaFileService.GetMovie(id); + var series = _seriesService.GetMovie(episodeFile.MovieId); + var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + + _logger.Info("Deleting episode file: {0}", fullPath); + _recycleBinProvider.DeleteFile(fullPath); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.Manual); + } + + public void Handle(EpisodeFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Movies/MovieModule.cs b/src/NzbDrone.Api/Movies/MovieModule.cs new file mode 100644 index 000000000..2a4d405fc --- /dev/null +++ b/src/NzbDrone.Api/Movies/MovieModule.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.Movies +{ + class MovieModule + { + } +} diff --git a/src/NzbDrone.Api/Movies/RenameMovieModule.cs b/src/NzbDrone.Api/Movies/RenameMovieModule.cs new file mode 100644 index 000000000..8736cccb0 --- /dev/null +++ b/src/NzbDrone.Api/Movies/RenameMovieModule.cs @@ -0,0 +1,35 @@ +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaFiles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.Movies +{ + public class RenameMovieModule : NzbDroneRestModule + { + private readonly IRenameMovieFileService _renameMovieFileService; + + public RenameMovieModule(IRenameMovieFileService renameMovieFileService) + : base("rename") + { + _renameMovieFileService = renameMovieFileService; + + GetResourceAll = GetMovies; //TODO: GetResourceSingle? + } + + private List GetMovies() + { + if(!Request.Query.MovieId.HasValue) + { + throw new BadRequestException("movieId is missing"); + } + + var movieId = (int)Request.Query.MovieId; + + return _renameMovieFileService.GetRenamePreviews(movieId).ToResource(); + } + + } +} diff --git a/src/NzbDrone.Api/Movies/RenameMovieResource.cs b/src/NzbDrone.Api/Movies/RenameMovieResource.cs new file mode 100644 index 000000000..d71f1bbcf --- /dev/null +++ b/src/NzbDrone.Api/Movies/RenameMovieResource.cs @@ -0,0 +1,35 @@ +using NzbDrone.Api.REST; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Api.Movies +{ + public class RenameMovieResource : RestResource + { + public int MovieId { get; set; } + public int MovieFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } + + public static class RenameMovieResourceMapper + { + public static RenameMovieResource ToResource(this Core.MediaFiles.RenameMovieFilePreview model) + { + if (model == null) return null; + + return new RenameMovieResource + { + MovieId = model.MovieId, + MovieFileId = model.MovieFileId, + ExistingPath = model.ExistingPath, + NewPath = model.NewPath + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 4ade4bcdf..6a61d84fc 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -116,6 +116,11 @@ + + + + + @@ -228,11 +233,15 @@ + + + + diff --git a/src/NzbDrone.Api/Properties/AssemblyInfo.cs b/src/NzbDrone.Api/Properties/AssemblyInfo.cs index 6149a06c4..300ee6fc1 100644 --- a/src/NzbDrone.Api/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Api/Properties/AssemblyInfo.cs @@ -6,6 +6,5 @@ using System.Runtime.InteropServices; [assembly: Guid("4c0922d7-979e-4ff7-b44b-b8ac2100eeb5")] -[assembly: AssemblyVersion("10.0.0.*")] [assembly: InternalsVisibleTo("NzbDrone.Core")] diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index cf1356c49..e90a9bace 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Api.Episodes; +using NzbDrone.Api.Movie; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Indexers; using System.Linq; @@ -14,6 +15,7 @@ namespace NzbDrone.Api.Queue { public SeriesResource Series { get; set; } public EpisodeResource Episode { get; set; } + public MovieResource Movie { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } @@ -49,7 +51,8 @@ namespace NzbDrone.Api.Queue TrackedDownloadStatus = model.TrackedDownloadStatus, StatusMessages = model.StatusMessages, DownloadId = model.DownloadId, - Protocol = model.Protocol + Protocol = model.Protocol, + Movie = model.Movie.ToResource() }; } diff --git a/src/NzbDrone.Api/Series/MovieFileResource.cs b/src/NzbDrone.Api/Series/MovieFileResource.cs new file mode 100644 index 000000000..848d31ab4 --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieFileResource.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; +using NzbDrone.Api.Series; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Api.Movie +{ + public class MovieFileResource : RestResource + { + public MovieFileResource() + { + + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + public int MovieId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public MovieResource Movie { get; set; } + public string Edition { get; set; } + public Core.MediaFiles.MediaInfo.MediaInfoModel MediaInfo { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + } + + public static class MovieFileResourceMapper + { + public static MovieFileResource ToResource(this MovieFile model) + { + if (model == null) return null; + + MovieResource movie = null; + + if (model.Movie != null) + { + model.Movie.LazyLoad(); + if (model.Movie.Value != null) + { + //movie = model.Movie.Value.ToResource(); + } + } + + return new MovieFileResource + { + Id = model.Id, + RelativePath = model.RelativePath, + Path = model.Path, + Size = model.Size, + DateAdded = model.DateAdded, + ReleaseGroup = model.ReleaseGroup, + Quality = model.Quality, + Movie = movie, + MediaInfo = model.MediaInfo, + Edition = model.Edition + }; + } + + public static MovieFile ToModel(this MovieFileResource resource) + { + if (resource == null) return null; + + return new MovieFile + { + + }; + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/Series/MovieLookupModule.cs b/src/NzbDrone.Api/Series/MovieLookupModule.cs new file mode 100644 index 000000000..1120b3046 --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieLookupModule.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using System.Linq; + +namespace NzbDrone.Api.Movie +{ + public class MovieLookupModule : NzbDroneRestModule + { + private readonly ISearchForNewMovie _searchProxy; + + public MovieLookupModule(ISearchForNewMovie searchProxy) + : base("/movies/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var imdbResults = _searchProxy.SearchForNewMovie((string)Request.Query.term); + return MapToResource(imdbResults).AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable movies) + { + foreach (var currentSeries in movies) + { + var resource = currentSeries.ToResource(); + var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Series/MovieModule.cs b/src/NzbDrone.Api/Series/MovieModule.cs new file mode 100644 index 000000000..a40695a1c --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieModule.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MovieStats; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Validation; +using NzbDrone.SignalR; + +namespace NzbDrone.Api.Movie +{ + public class MovieModule : NzbDroneRestModuleWithSignalR, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle, + IHandle + + { + private readonly IMovieService _moviesService; + private readonly IMovieStatisticsService _moviesStatisticsService; + private readonly IMapCoversToLocal _coverMapper; + + public MovieModule(IBroadcastSignalRMessage signalRBroadcaster, + IMovieService moviesService, + IMovieStatisticsService moviesStatisticsService, + ISceneMappingService sceneMappingService, + IMapCoversToLocal coverMapper, + RootFolderValidator rootFolderValidator, + MoviePathValidator moviesPathValidator, + MovieExistsValidator moviesExistsValidator, + DroneFactoryValidator droneFactoryValidator, + MovieAncestorValidator moviesAncestorValidator, + ProfileExistsValidator profileExistsValidator + ) + : base(signalRBroadcaster) + { + _moviesService = moviesService; + _moviesStatisticsService = moviesStatisticsService; + + _coverMapper = coverMapper; + + GetResourceAll = AllMovie; + GetResourceById = GetMovie; + CreateResource = AddMovie; + UpdateResource = UpdateMovie; + DeleteResource = DeleteMovie; + + Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(moviesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(moviesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.Title).NotEmpty(); + PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private MovieResource GetMovie(int id) + { + var movies = _moviesService.GetMovie(id); + return MapToResource(movies); + } + + private MovieResource MapToResource(Core.Tv.Movie movies) + { + if (movies == null) return null; + + var resource = movies.ToResource(); + MapCoversToLocal(resource); + FetchAndLinkMovieStatistics(resource); + PopulateAlternateTitles(resource); + + return resource; + } + + private List AllMovie() + { + var moviesStats = _moviesStatisticsService.MovieStatistics(); + var moviesResources = _moviesService.GetAllMovies().ToResource(); + + MapCoversToLocal(moviesResources.ToArray()); + LinkMovieStatistics(moviesResources, moviesStats); + PopulateAlternateTitles(moviesResources); + + return moviesResources; + } + + private int AddMovie(MovieResource moviesResource) + { + var model = moviesResource.ToModel(); + + return _moviesService.AddMovie(model).Id; + } + + private void UpdateMovie(MovieResource moviesResource) + { + var model = moviesResource.ToModel(_moviesService.GetMovie(moviesResource.Id)); + + _moviesService.UpdateMovie(model); + + BroadcastResourceChange(ModelAction.Updated, moviesResource); + } + + private void DeleteMovie(int id) + { + var deleteFiles = false; + var deleteFilesQuery = Request.Query.deleteFiles; + + if (deleteFilesQuery.HasValue) + { + deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); + } + + _moviesService.DeleteMovie(id, deleteFiles); + } + + private void MapCoversToLocal(params MovieResource[] movies) + { + foreach (var moviesResource in movies) + { + _coverMapper.ConvertToLocalUrls(moviesResource.Id, moviesResource.Images); + } + } + + private void FetchAndLinkMovieStatistics(MovieResource resource) + { + LinkMovieStatistics(resource, _moviesStatisticsService.MovieStatistics(resource.Id)); + } + + private void LinkMovieStatistics(List resources, List moviesStatistics) + { + var dictMovieStats = moviesStatistics.ToDictionary(v => v.MovieId); + + foreach (var movies in resources) + { + var stats = dictMovieStats.GetValueOrDefault(movies.Id); + if (stats == null) continue; + + LinkMovieStatistics(movies, stats); + } + } + + private void LinkMovieStatistics(MovieResource resource, MovieStatistics moviesStatistics) + { + resource.SizeOnDisk = moviesStatistics.SizeOnDisk; + } + + private void PopulateAlternateTitles(List resources) + { + foreach (var resource in resources) + { + PopulateAlternateTitles(resource); + } + } + + private void PopulateAlternateTitles(MovieResource resource) + { + //var mappings = null;//_sceneMappingService.FindByTvdbId(resource.TvdbId); + + //if (mappings == null) return; + + //resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + } + + public void Handle(EpisodeImportedEvent message) + { + //BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.MovieId); + } + + public void Handle(EpisodeFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + //BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.MovieId); + } + + public void Handle(MovieUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MovieEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MovieDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Movie.ToResource()); + } + + public void Handle(MovieRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + + public void Handle(MediaCoversUpdatedEvent message) + { + //BroadcastResourceChange(ModelAction.Updated, message.Movie.Id); + } + } +} diff --git a/src/NzbDrone.Api/Series/MovieResource.cs b/src/NzbDrone.Api/Series/MovieResource.cs new file mode 100644 index 000000000..c0cc8d623 --- /dev/null +++ b/src/NzbDrone.Api/Series/MovieResource.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Api.REST; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Tv; +using NzbDrone.Api.Series; + +namespace NzbDrone.Api.Movie +{ + public class MovieResource : RestResource + { + public MovieResource() + { + Monitored = true; + } + + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + //View Only + public string Title { get; set; } + public List AlternateTitles { get; set; } + public string SortTitle { get; set; } + public long? SizeOnDisk { get; set; } + public MovieStatusType Status { get; set; } + public string Overview { get; set; } + public DateTime? InCinemas { get; set; } + public DateTime? PhysicalRelease { get; set; } + public List Images { get; set; } + public string Website { get; set; } + public bool Downloaded { get; set; } + public string RemotePoster { get; set; } + public int Year { get; set; } + public bool HasFile { get; set; } + + //View & Edit + public string Path { get; set; } + public int ProfileId { get; set; } + + //Editing Only + public bool Monitored { get; set; } + public int Runtime { get; set; } + public DateTime? LastInfoSync { get; set; } + public string CleanTitle { get; set; } + public string ImdbId { get; set; } + public int TmdbId { get; set; } + public string TitleSlug { get; set; } + public string RootFolderPath { get; set; } + public string Certification { get; set; } + public List Genres { get; set; } + public HashSet Tags { get; set; } + public DateTime Added { get; set; } + public AddMovieOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + public List AlternativeTitles { get; set; } + public MovieFileResource MovieFile { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + + //Used to support legacy consumers + public int QualityProfileId + { + get + { + return ProfileId; + } + set + { + if (value > 0 && ProfileId == 0) + { + ProfileId = value; + } + } + } + } + + public static class MovieResourceMapper + { + public static MovieResource ToResource(this Core.Tv.Movie model) + { + if (model == null) return null; + + + long size = 0; + bool downloaded = false; + MovieFileResource movieFile = null; + + + if(model.MovieFile != null) + { + model.MovieFile.LazyLoad(); + } + + if (model.MovieFile != null && model.MovieFile.IsLoaded && model.MovieFile.Value != null) + { + size = model.MovieFile.Value.Size; + downloaded = true; + movieFile = model.MovieFile.Value.ToResource(); + } + + return new MovieResource + { + Id = model.Id, + TmdbId = model.TmdbId, + Title = model.Title, + //AlternateTitles + SortTitle = model.SortTitle, + InCinemas = model.InCinemas, + PhysicalRelease = model.PhysicalRelease, + HasFile = model.HasFile, + Downloaded = downloaded, + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = model.Status, + Overview = model.Overview, + //NextAiring + //PreviousAiring + Images = model.Images, + + Year = model.Year, + + Path = model.Path, + ProfileId = model.ProfileId, + + Monitored = model.Monitored, + + SizeOnDisk = size, + + Runtime = model.Runtime, + LastInfoSync = model.LastInfoSync, + CleanTitle = model.CleanTitle, + ImdbId = model.ImdbId, + TitleSlug = model.TitleSlug, + RootFolderPath = model.RootFolderPath, + Certification = model.Certification, + Website = model.Website, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + AlternativeTitles = model.AlternativeTitles, + Ratings = model.Ratings, + MovieFile = movieFile + }; + } + + public static Core.Tv.Movie ToModel(this MovieResource resource) + { + if (resource == null) return null; + + return new Core.Tv.Movie + { + Id = resource.Id, + TmdbId = resource.TmdbId, + + Title = resource.Title, + //AlternateTitles + SortTitle = resource.SortTitle, + InCinemas = resource.InCinemas, + PhysicalRelease = resource.PhysicalRelease, + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Overview = resource.Overview, + //NextAiring + //PreviousAiring + Images = resource.Images, + + Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.ProfileId, + + Monitored = resource.Monitored, + + Runtime = resource.Runtime, + LastInfoSync = resource.LastInfoSync, + CleanTitle = resource.CleanTitle, + ImdbId = resource.ImdbId, + TitleSlug = resource.TitleSlug, + RootFolderPath = resource.RootFolderPath, + Certification = resource.Certification, + Website = resource.Website, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions, + AlternativeTitles = resource.AlternativeTitles, + Ratings = resource.Ratings + }; + } + + public static Core.Tv.Movie ToModel(this MovieResource resource, Core.Tv.Movie movie) + { + movie.ImdbId = resource.ImdbId; + movie.TmdbId = resource.TmdbId; + + movie.Path = resource.Path; + movie.ProfileId = resource.ProfileId; + + movie.Monitored = resource.Monitored; + + movie.RootFolderPath = resource.RootFolderPath; + movie.Tags = resource.Tags; + movie.AddOptions = resource.AddOptions; + + return movie; + } + + public static List ToResource(this IEnumerable movies) + { + return movies.Select(ToResource).ToList(); + } + } +} diff --git a/src/NzbDrone.Api/System/Tasks/TaskResource.cs b/src/NzbDrone.Api/System/Tasks/TaskResource.cs index fda392cae..d4b583aa5 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskResource.cs +++ b/src/NzbDrone.Api/System/Tasks/TaskResource.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Api.System.Tasks { public string Name { get; set; } public string TaskName { get; set; } - public int Interval { get; set; } + public double Interval { get; set; } public DateTime LastExecution { get; set; } public DateTime NextExecution { get; set; } } diff --git a/src/NzbDrone.App.Test/ContainerFixture.cs b/src/NzbDrone.App.Test/ContainerFixture.cs index 1064d1c5b..e0c01be2f 100644 --- a/src/NzbDrone.App.Test/ContainerFixture.cs +++ b/src/NzbDrone.App.Test/ContainerFixture.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Jobs; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Host; +using Radarr.Host; using NzbDrone.Test.Common; using FluentAssertions; using System.Linq; diff --git a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs index 1ee1ee522..dc8eda638 100644 --- a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs +++ b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs @@ -3,8 +3,9 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Model; using NzbDrone.Common.Processes; -using NzbDrone.Host; +using Radarr.Host; using NzbDrone.Test.Common; +using Radarr.Host; namespace NzbDrone.App.Test { diff --git a/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs index 86a324eef..0d82bf1bf 100644 --- a/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.App.Test/Properties/AssemblyInfo.cs @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b47d34ef-05e8-4826-8a57-9dd05106c964")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.App.Test/RouterTest.cs b/src/NzbDrone.App.Test/RouterTest.cs index 0cf7b6c3d..1805875f0 100644 --- a/src/NzbDrone.App.Test/RouterTest.cs +++ b/src/NzbDrone.App.Test/RouterTest.cs @@ -3,7 +3,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Host; +using Radarr.Host; using NzbDrone.Test.Common; namespace NzbDrone.App.Test diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index 9f493d824..0e0fea564 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Automation.Test _runner.KillAll(); _runner.Start(); - driver.Url = "http://localhost:8989"; + driver.Url = "http://localhost:7878"; var page = new PageBase(driver); page.WaitForNoSpinner(); diff --git a/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs index a5d255084..8cba7bd2e 100644 --- a/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Automation.Test/Properties/AssemblyInfo.cs @@ -20,5 +20,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("6b8945f5-f5b5-4729-865d-f958fbd673d9")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index 92df06ded..7d0e0442f 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Common.Test public void GetValue_Success() { const string key = "Port"; - const string value = "8989"; + const string value = "7878"; var result = Subject.GetValue(key, value); @@ -60,7 +60,7 @@ namespace NzbDrone.Common.Test public void GetInt_Success() { const string key = "Port"; - const int value = 8989; + const int value = 7878; var result = Subject.GetValueInt(key, value); @@ -95,7 +95,7 @@ namespace NzbDrone.Common.Test [Test] public void GetPort_Success() { - const int value = 8989; + const int value = 7878; var result = Subject.Port; diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index e3e7fb34a..95e4aebe7 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -263,7 +263,7 @@ namespace NzbDrone.Common.Test [Test] public void GetUpdateClientExePath() { - GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\NzbDrone.Update.exe".AsOsAgnostic()); + GetIAppDirectoryInfo().GetUpdateClientExePath().Should().BeEquivalentTo(@"C:\Temp\nzbdrone_update\Radarr.Update.exe".AsOsAgnostic()); } [Test] diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 95b5027ff..d8c5d26a4 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -5,7 +5,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Host; +using Radarr.Host; using NzbDrone.Test.Common; namespace NzbDrone.Common.Test diff --git a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs index 5c3712d85..8e6b55e11 100644 --- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs +++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs @@ -6,22 +6,33 @@ namespace NzbDrone.Common.Cloud { IHttpRequestBuilderFactory Services { get; } IHttpRequestBuilderFactory SkyHookTvdb { get; } + IHttpRequestBuilderFactory TMDB { get; } + IHttpRequestBuilderFactory TMDBSingle { get; } } public class SonarrCloudRequestBuilder : ISonarrCloudRequestBuilder { public SonarrCloudRequestBuilder() { - Services = new HttpRequestBuilder("http://services.sonarr.tv/v1/") + Services = new HttpRequestBuilder("http://radarr.aeonlucid.com/v1/") .CreateFactory(); SkyHookTvdb = new HttpRequestBuilder("http://skyhook.sonarr.tv/v1/tvdb/{route}/{language}/") .SetSegment("language", "en") .CreateFactory(); + + TMDB = new HttpRequestBuilder("https://api.themoviedb.org/3/{route}/{id}{secondaryRoute}") + .AddQueryParam("api_key", "1a7373301961d03f97f853a876dd1212") + .CreateFactory(); + + TMDBSingle = new HttpRequestBuilder("https://api.themoviedb.org/3/{route}") + .AddQueryParam("api_key", "1a7373301961d03f97f853a876dd1212") + .CreateFactory(); } public IHttpRequestBuilderFactory Services { get; private set; } - public IHttpRequestBuilderFactory SkyHookTvdb { get; private set; } + public IHttpRequestBuilderFactory TMDB { get; private set; } + public IHttpRequestBuilderFactory TMDBSingle { get; private set; } } } diff --git a/src/NzbDrone.Common/ConsoleService.cs b/src/NzbDrone.Common/ConsoleService.cs index 321831277..8a16c352b 100644 --- a/src/NzbDrone.Common/ConsoleService.cs +++ b/src/NzbDrone.Common/ConsoleService.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Common Console.WriteLine(" Commands:"); Console.WriteLine(" /{0} Install the application as a Windows Service ({1}).", StartupContext.INSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME); Console.WriteLine(" /{0} Uninstall already installed Windows Service ({1}).", StartupContext.UNINSTALL_SERVICE, ServiceProvider.NZBDRONE_SERVICE_NAME); - Console.WriteLine(" /{0} Don't open Sonarr in a browser", StartupContext.NO_BROWSER); + Console.WriteLine(" /{0} Don't open Radarr in a browser", StartupContext.NO_BROWSER); Console.WriteLine(" Run application in console mode."); } diff --git a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs index 75b75093e..0d35aed70 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/AppFolderInfo.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Common.EnvironmentInfo } else { - AppDataFolder = Path.Combine(Environment.GetFolderPath(DATA_SPECIAL_FOLDER, Environment.SpecialFolderOption.None), "NzbDrone"); + AppDataFolder = Path.Combine(Environment.GetFolderPath(DATA_SPECIAL_FOLDER, Environment.SpecialFolderOption.None), "Radarr"); } StartUpFolder = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 7e77f9d7e..63dc57884 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -13,13 +13,13 @@ namespace NzbDrone.Common.Extensions private const string NZBDRONE_DB = "nzbdrone.db"; private const string NZBDRONE_LOG_DB = "logs.db"; private const string NLOG_CONFIG_FILE = "nlog.config"; - private const string UPDATE_CLIENT_EXE = "NzbDrone.Update.exe"; + private const string UPDATE_CLIENT_EXE = "Radarr.Update.exe"; private const string BACKUP_FOLDER = "Backups"; - private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "nzbdrone_update" + Path.DirectorySeparatorChar; - private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "NzbDrone" + Path.DirectorySeparatorChar; - private static readonly string UPDATE_BACKUP_FOLDER_NAME = "nzbdrone_backup" + Path.DirectorySeparatorChar; - private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "nzbdrone_appdata_backup" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_SANDBOX_FOLDER_NAME = "radarr_update" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_PACKAGE_FOLDER_NAME = "Radarr" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_BACKUP_FOLDER_NAME = "radarr_backup" + Path.DirectorySeparatorChar; + private static readonly string UPDATE_BACKUP_APPDATA_FOLDER_NAME = "radarr_appdata_backup" + Path.DirectorySeparatorChar; private static readonly string UPDATE_CLIENT_FOLDER_NAME = "NzbDrone.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; diff --git a/src/NzbDrone.Common/Http/UserAgentBuilder.cs b/src/NzbDrone.Common/Http/UserAgentBuilder.cs index fa99d03f4..f0cff30e9 100644 --- a/src/NzbDrone.Common/Http/UserAgentBuilder.cs +++ b/src/NzbDrone.Common/Http/UserAgentBuilder.cs @@ -9,12 +9,12 @@ namespace NzbDrone.Common.Http static UserAgentBuilder() { - UserAgent = string.Format("Sonarr/{0} ({1} {2})", + UserAgent = string.Format("Radarr/{0} ({1} {2})", BuildInfo.Version, OsInfo.Os, OsInfo.Version.ToString(2)); - UserAgentSimplified = string.Format("Sonarr/{0}", + UserAgentSimplified = string.Format("Radarr/{0}", BuildInfo.Version.ToString(2)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 7c672f725..39febd26c 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -103,9 +103,9 @@ namespace NzbDrone.Common.Instrumentation private static void RegisterAppFile(IAppFolderInfo appFolderInfo) { - RegisterAppFile(appFolderInfo, "appFileInfo", "sonarr.txt", 5, LogLevel.Info); - RegisterAppFile(appFolderInfo, "appFileDebug", "sonarr.debug.txt", 50, LogLevel.Off); - RegisterAppFile(appFolderInfo, "appFileTrace", "sonarr.trace.txt", 50, LogLevel.Off); + RegisterAppFile(appFolderInfo, "appFileInfo", "radarr.txt", 5, LogLevel.Info); + RegisterAppFile(appFolderInfo, "appFileDebug", "radarr.debug.txt", 50, LogLevel.Off); + RegisterAppFile(appFolderInfo, "appFileTrace", "radarr.trace.txt", 50, LogLevel.Off); } private static LoggingRule RegisterAppFile(IAppFolderInfo appFolderInfo, string name, string fileName, int maxArchiveFiles, LogLevel minLogLevel) diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 57068c840..50cc89a84 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -35,8 +35,8 @@ namespace NzbDrone.Common.Processes { private readonly Logger _logger; - public const string NZB_DRONE_PROCESS_NAME = "NzbDrone"; - public const string NZB_DRONE_CONSOLE_PROCESS_NAME = "NzbDrone.Console"; + public const string NZB_DRONE_PROCESS_NAME = "Radarr"; + public const string NZB_DRONE_CONSOLE_PROCESS_NAME = "Radarr.Console"; public ProcessProvider(Logger logger) { diff --git a/src/NzbDrone.Common/Properties/AssemblyInfo.cs b/src/NzbDrone.Common/Properties/AssemblyInfo.cs index e8cdf90c1..7ab20e84b 100644 --- a/src/NzbDrone.Common/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Common/Properties/AssemblyInfo.cs @@ -9,4 +9,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b6eaa144-e13b-42e5-a738-c60d89c0f728")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs b/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs index 9c8e66406..09379201a 100644 --- a/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs +++ b/src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs @@ -2,8 +2,9 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("sonarr.tv")] +[assembly: AssemblyCompany("radarr.tv")] [assembly: AssemblyProduct("NzbDrone")] +[assembly: AssemblyVersion("0.1.0.*")] [assembly: AssemblyCopyright("GNU General Public v3")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/NzbDrone.Common/ServiceProvider.cs b/src/NzbDrone.Common/ServiceProvider.cs index b494381c3..8387e6f7c 100644 --- a/src/NzbDrone.Common/ServiceProvider.cs +++ b/src/NzbDrone.Common/ServiceProvider.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Common public class ServiceProvider : IServiceProvider { - public const string NZBDRONE_SERVICE_NAME = "NzbDrone"; + public const string NZBDRONE_SERVICE_NAME = "Radarr"; private readonly IProcessProvider _processProvider; private readonly Logger _logger; @@ -78,7 +78,7 @@ namespace NzbDrone.Common serviceInstaller.Context = context; serviceInstaller.DisplayName = serviceName; serviceInstaller.ServiceName = serviceName; - serviceInstaller.Description = "NzbDrone Application Server"; + serviceInstaller.Description = "Radarr Application Server"; serviceInstaller.StartType = ServiceStartMode.Automatic; serviceInstaller.ServicesDependedOn = new[] { "EventLog", "Tcpip", "http" }; diff --git a/src/NzbDrone.Console/ConsoleAlerts.cs b/src/NzbDrone.Console/ConsoleAlerts.cs index 4d623fc8e..8533c46f2 100644 --- a/src/NzbDrone.Console/ConsoleAlerts.cs +++ b/src/NzbDrone.Console/ConsoleAlerts.cs @@ -1,4 +1,4 @@ -using NzbDrone.Host; +using Radarr.Host; namespace NzbDrone.Console { diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 6f935887f..eb75bddc7 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -3,7 +3,7 @@ using System.Net.Sockets; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; -using NzbDrone.Host; +using Radarr.Host; namespace NzbDrone.Console { @@ -23,7 +23,7 @@ namespace NzbDrone.Console { System.Console.WriteLine(""); System.Console.WriteLine(""); - Logger.Fatal(exception.Message + ". This can happen if another instance of Sonarr is already running another application is using the same port (default: 8989) or the user has insufficient permissions"); + Logger.Fatal(exception.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions"); System.Console.WriteLine("Press enter to exit..."); System.Console.ReadLine(); Environment.Exit(1); diff --git a/src/NzbDrone.Console/NzbDrone.Console.csproj b/src/NzbDrone.Console/NzbDrone.Console.csproj index 61cc3190f..6c35d7672 100644 --- a/src/NzbDrone.Console/NzbDrone.Console.csproj +++ b/src/NzbDrone.Console/NzbDrone.Console.csproj @@ -9,7 +9,7 @@ Exe Properties NzbDrone.Console - NzbDrone.Console + Radarr.Console v4.0 512 @@ -54,7 +54,7 @@ 4 - ..\NzbDrone.Host\NzbDrone.ico + Radarr.ico NzbDrone.Console.ConsoleApp @@ -139,6 +139,9 @@ + + + diff --git a/src/NzbDrone.Console/Properties/AssemblyInfo.cs b/src/NzbDrone.Console/Properties/AssemblyInfo.cs index ed519f028..172df372a 100644 --- a/src/NzbDrone.Console/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Console/Properties/AssemblyInfo.cs @@ -7,5 +7,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("NzbDrone.Host")] [assembly: Guid("67AADCD9-89AA-4D95-8281-3193740E70E5")] - -[assembly: AssemblyVersion("10.0.0.*")] \ No newline at end of file diff --git a/src/NzbDrone.Console/Radarr.ico b/src/NzbDrone.Console/Radarr.ico new file mode 100644 index 000000000..6f0a8b50e Binary files /dev/null and b/src/NzbDrone.Console/Radarr.ico differ diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 76d22d669..356ad1e7e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -178,8 +178,9 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests public void should_return_an_empty_list_when_none_are_appproved() { var decisions = new List(); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); - decisions.Add(new DownloadDecision(null, new Rejection("Failure!"))); + RemoteEpisode ep = null; + decisions.Add(new DownloadDecision(ep, new Rejection("Failure!"))); + decisions.Add(new DownloadDecision(ep, new Rejection("Failure!"))); Subject.GetQualifiedReports(decisions).Should().BeEmpty(); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs index af24f2797..b70021190 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DelugeTests/DelugeFixture.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DelugeTests Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new DelugeSettings() { - TvCategory = null + MovieCategory = null }; _queued = new DelugeTorrent diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 3ceece6f6..2283f7add 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Port = 2222, Username = "admin", Password = "pass", - TvCategory = "tv" + MovieCategory = "movies-radarr" }; Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs index f657a7884..a3d7eebf1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.RTorrentTests Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new RTorrentSettings() { - TvCategory = null + MovieCategory = null }; _downloading = new RTorrentTorrent diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs index d46f9a30e..5fd4136b6 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs @@ -112,12 +112,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests protected void GivenTvCategory() { - _settings.TvCategory = "sonarr"; + _settings.MovieCategory = "radarr"; } protected void GivenTvDirectory() { - _settings.TvDirectory = @"C:/Downloads/Finished/sonarr"; + _settings.MovieDirectory = @"C:/Downloads/Finished/radarr"; } protected void GivenFailedDownload() diff --git a/src/NzbDrone.Core.Test/Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml b/src/NzbDrone.Core.Test/Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml index d11fe2a1b..93dc7ce13 100644 --- a/src/NzbDrone.Core.Test/Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml +++ b/src/NzbDrone.Core.Test/Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml @@ -1,1212 +1,1210 @@ - omgwtfnzbs.org rss feeds generator + omgwtfnzbs.me rss feeds generator en-us - Search NZB Download Feed - http://rss.omgwtfnzbs.org - auto-dl feed for omgwtfnzbs.org - 2010 - 2012 omgwtfnzbs - - Mon, 17 Dec 2012 23:30:16 +0000 - - - Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV - Mon, 17 Dec 2012 23:30:13 +0000 - http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 225.85 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:30:04
Added to usenet: 17/12/2012 23:30:13
Weblink: http://www.tvrage.com/shows/id-33431
View NZB: http://omgwtfnzbs.org/details.php?id=OAl4g]]>
- TV: STD - tv.sd - 19 - -
- - - Never.Mind.The.Buzzcocks.UK.S26E12.720p.HDTV.x264-FTP - Mon, 17 Dec 2012 23:27:23 +0000 - http://api.omgwtfnzbs.org/sn.php?id=3whQL&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=3whQL&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 660.51 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:26:53
Added to usenet: 17/12/2012 23:27:23
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=3whQL]]>
- TV: HD - tv.hd - 20 - -
- - - Bad.Santas.S01E01.HDTV.x264-W4F - Mon, 17 Dec 2012 23:23:02 +0000 - http://api.omgwtfnzbs.org/sn.php?id=YXPhW&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=YXPhW&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 437.29 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:22:39
Added to usenet: 17/12/2012 23:23:02
Weblink: http://thetvdb.com/?tab=series&id=264930&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=YXPhW]]>
- TV: STD - tv.sd - 19 - -
- - - Chainsaw.Gang.S01E06.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 23:10:55 +0000 - http://api.omgwtfnzbs.org/sn.php?id=387yh&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=387yh&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 218.8 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 23:16:58
Added to usenet: 17/12/2012 23:10:55
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=387yh]]>
- TV: STD - tv.sd - 19 - -
- - - NFL.2012.12.16.Buccaneers.vs.Saints.720p.HDTV.x264-BAJSKORV - Mon, 17 Dec 2012 23:10:23 +0000 - http://api.omgwtfnzbs.org/sn.php?id=oUgMb&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=oUgMb&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 6.76 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:07:16
Added to usenet: 17/12/2012 23:10:23
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=oUgMb]]>
- TV: HD - tv.hd - 20 - -
- - - Never.Mind.The.Buzzcocks.UK.S26E12.HDTV.x264-FTP - Mon, 17 Dec 2012 23:10:22 +0000 - http://api.omgwtfnzbs.org/sn.php?id=CAxYY&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=CAxYY&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 220.87 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:09:59
Added to usenet: 17/12/2012 23:10:22
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=CAxYY]]>
- TV: STD - tv.sd - 19 - -
- - - Sloth.Bear.HDTV.x264-TERRA - Mon, 17 Dec 2012 23:02:13 +0000 - http://api.omgwtfnzbs.org/sn.php?id=LDn8P&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=LDn8P&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 452 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 23:02:00
Added to usenet: 17/12/2012 23:02:13
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=LDn8P]]>
- TV: STD - tv.sd - 19 - -
- - - Panorama.S60E49.HDTV.x264-BARGE - Mon, 17 Dec 2012 22:55:30 +0000 - http://api.omgwtfnzbs.org/sn.php?id=6aLWJ&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=6aLWJ&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 250.86 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:55:18
Added to usenet: 17/12/2012 22:55:30
Weblink: http://thetvdb.com/?tab=series&id=80748&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=6aLWJ]]>
- TV: STD - tv.sd - 19 - -
- - - Chainsaw.Gang.S01E05.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 22:51:26 +0000 - http://api.omgwtfnzbs.org/sn.php?id=FdB6A&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=FdB6A&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 165.75 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 22:57:31
Added to usenet: 17/12/2012 22:51:26
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=FdB6A]]>
- TV: STD - tv.sd - 19 - + omgwtfnzbs.me - Latest NZB Download Feed + https://rss.omgwtfnzbs.me/ + omgwtfnzbs.me - NZB Download Feed (false) + 2010 - 2014 omgwtfnzbs + + + Un.Petit.Boulot.2016.FRENCH.720p.BluRay.DTS.x264-LOST + Mon, 09 Jan 2017 02:16:54 +0200 + https://api.omgwtfnzbs.me/nzb/?id=8a2Bw&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=8a2Bw&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 4.99 GB
Group: alt.binaries.boneless
Added to index: 01/01/2017 22:49:30
Added to usenet: 09/01/2017 02:16:54
View NZB: https://omgwtfnzbs.me/details.php?id=8a2Bw]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Death.Race.2050.2017.1080p.BluRay.x264-ROVERS + Mon, 09 Jan 2017 01:40:12 +0200 + https://api.omgwtfnzbs.me/nzb/?id=2Aqi3&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=2Aqi3&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 8.8 GB
Group: alt.binaries.moovee
Added to index: 09/01/2017 00:37:45
Added to usenet: 09/01/2017 01:40:12
View NZB: https://omgwtfnzbs.me/details.php?id=2Aqi3]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Death.Race.2050.2017.BDRip.x264-ROVERS + Mon, 09 Jan 2017 01:17:52 +0200 + https://api.omgwtfnzbs.me/nzb/?id=dg04S&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=dg04S&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 1.1 GB
Group: alt.binaries.moovee
Added to index: 09/01/2017 00:14:52
Added to usenet: 09/01/2017 01:17:52
View NZB: https://omgwtfnzbs.me/details.php?id=dg04S]]>
+ Movies: STD + movies.sd + 15 + +
+ + + Floored.2009.1080p.BluRay.x264-THUGLiNE + Sun, 08 Jan 2017 23:34:46 +0200 + https://api.omgwtfnzbs.me/nzb/?id=c2rBA&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=c2rBA&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.33 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 22:31:47
Added to usenet: 08/01/2017 23:34:46
View NZB: https://omgwtfnzbs.me/details.php?id=c2rBA]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Floored.2009.720p.BluRay.x264-THUGLiNE + Sun, 08 Jan 2017 23:31:06 +0200 + https://api.omgwtfnzbs.me/nzb/?id=fV4im&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=fV4im&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.73 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 22:27:10
Added to usenet: 08/01/2017 23:31:06
View NZB: https://omgwtfnzbs.me/details.php?id=fV4im]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Live.Flesh.1997.1080p.BluRay.FLAC2.0.x264-DON + Sun, 08 Jan 2017 20:19:06 +0200 + https://api.omgwtfnzbs.me/nzb/?id=BnTZ0&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=BnTZ0&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 16.85 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 19:16:44
Added to usenet: 08/01/2017 20:19:06
View NZB: https://omgwtfnzbs.me/details.php?id=BnTZ0]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Sex.By.Advertisement.1968.DVDRip.x264-FiCO + Sun, 08 Jan 2017 18:02:53 +0200 + https://api.omgwtfnzbs.me/nzb/?id=yMSuc&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=yMSuc&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 862.74 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 17:00:47
Added to usenet: 08/01/2017 18:02:53
View NZB: https://omgwtfnzbs.me/details.php?id=yMSuc]]>
+ Movies: STD + movies.sd + 15 + +
+ + + Super.Rhino.2009.1080p.BluRay.x264-FLAME + Sun, 08 Jan 2017 13:17:06 +0200 + https://api.omgwtfnzbs.me/nzb/?id=b43Ej&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=b43Ej&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 502.14 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 12:15:19
Added to usenet: 08/01/2017 13:17:06
View NZB: https://omgwtfnzbs.me/details.php?id=b43Ej]]>
+ Movies: HD + movies.hd + 16 + +
+ + + Super.Rhino.2009.720p.BluRay.x264-FLAME + Sun, 08 Jan 2017 13:15:26 +0200 + https://api.omgwtfnzbs.me/nzb/?id=k6soa&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=k6soa&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 266.62 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 12:13:35
Added to usenet: 08/01/2017 13:15:26
View NZB: https://omgwtfnzbs.me/details.php?id=k6soa]]>
+ Movies: HD + movies.hd + 16 +
- Inside.Claridges.S01E03.HDTV.x264-FTP - Mon, 17 Dec 2012 22:48:24 +0000 - http://api.omgwtfnzbs.org/sn.php?id=0zjU4&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=0zjU4&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 448.38 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:48:04
Added to usenet: 17/12/2012 22:48:24
Weblink: http://thetvdb.com/?tab=series&id=264600&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=0zjU4]]>
- TV: STD - tv.sd - 19 - + San.Andreas.2015.TRUEFRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 13:03:23 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Yvek6&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Yvek6&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.05 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 06:25:10
Added to usenet: 08/01/2017 13:03:23
View NZB: https://omgwtfnzbs.me/details.php?id=Yvek6]]>
+ Movies: HD + movies.hd + 16 +
- Royal.Pains.S04E15.Off-Season.Greetings.Pt.1.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 22:47:52 +0000 - http://api.omgwtfnzbs.org/sn.php?id=mMHry&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=mMHry&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 277.66 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 22:53:52
Added to usenet: 17/12/2012 22:47:52
Weblink: http://thetvdb.com/?tab=series&id=92411&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=mMHry]]>
- TV: STD - tv.sd - 19 - + San.Andreas.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 13:02:45 +0200 + https://api.omgwtfnzbs.me/nzb/?id=wbvw3&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=wbvw3&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.felfelida
Added to index: 11/12/2016 01:52:51
Added to usenet: 08/01/2017 13:02:45
View NZB: https://omgwtfnzbs.me/details.php?id=wbvw3]]>
+ Movies: HD + movies.hd + 16 +
- The.Poison.Tree.S01E02.720p.HDTV.x264-TLA - Mon, 17 Dec 2012 22:44:57 +0000 - http://api.omgwtfnzbs.org/sn.php?id=XiqFs&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=XiqFs&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 868.42 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:44:21
Added to usenet: 17/12/2012 22:44:57
Weblink: http://thetvdb.com/?tab=series&id=264796&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=XiqFs]]>
- TV: HD - tv.hd - 20 - + Saints.And.Soldiers.2003.STV.FRENCH.720p.BluRay.x264-MUxHD + Sun, 08 Jan 2017 13:02:12 +0200 + https://api.omgwtfnzbs.me/nzb/?id=KID80&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=KID80&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.12 GB
Group: alt.binaries.movies.french
Added to index: 01/12/2016 22:58:21
Added to usenet: 08/01/2017 13:02:12
View NZB: https://omgwtfnzbs.me/details.php?id=KID80]]>
+ Movies: HD + movies.hd + 16 +
- The.Poison.Tree.S01E02.HDTV.x264-RiVER - Mon, 17 Dec 2012 22:37:44 +0000 - http://api.omgwtfnzbs.org/sn.php?id=S8EDd&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=S8EDd&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 331.56 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:37:28
Added to usenet: 17/12/2012 22:37:44
Weblink: http://thetvdb.com/?tab=series&id=264796&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=S8EDd]]>
- TV: STD - tv.sd - 19 - + Risen.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 13:01:49 +0200 + https://api.omgwtfnzbs.me/nzb/?id=hyUJx&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=hyUJx&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.4 GB
Group: alt.binaries.felfelida
Added to index: 29/11/2016 23:02:11
Added to usenet: 08/01/2017 13:01:49
View NZB: https://omgwtfnzbs.me/details.php?id=hyUJx]]>
+ Movies: HD + movies.hd + 16 +
- Inside.Claridges.S01E03.720p.HDTV.x264-FTP - Mon, 17 Dec 2012 22:29:51 +0000 - http://api.omgwtfnzbs.org/sn.php?id=edHL6&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=edHL6&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.18 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:29:43
Added to usenet: 17/12/2012 22:29:51
Weblink: http://thetvdb.com/?tab=series&id=264600&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=edHL6]]>
- TV: HD - tv.hd - 20 - + Ran.1985.REMASTERED.FRENCH.720p.BluRay.x264-ULSHD + Sun, 08 Jan 2017 13:01:24 +0200 + https://api.omgwtfnzbs.me/nzb/?id=5elVu&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=5elVu&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 10.18 GB
Group: alt.binaries.movies.french
Added to index: 03/12/2016 22:56:51
Added to usenet: 08/01/2017 13:01:24
View NZB: https://omgwtfnzbs.me/details.php?id=5elVu]]>
+ Movies: HD + movies.hd + 16 +
- Tron.Uprising.S01E13.The.Stranger.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 22:28:47 +0000 - http://api.omgwtfnzbs.org/sn.php?id=BMO6u&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=BMO6u&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 163.45 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 22:32:56
Added to usenet: 17/12/2012 22:28:47
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=BMO6u]]>
- TV: STD - tv.sd - 19 - + Pride.and.Prejudice.and.Zombies.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 13:00:41 +0200 + https://api.omgwtfnzbs.me/nzb/?id=TL2hr&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=TL2hr&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.06 GB
Group: alt.binaries.felfelida
Added to index: 02/12/2016 05:19:32
Added to usenet: 08/01/2017 13:00:41
View NZB: https://omgwtfnzbs.me/details.php?id=TL2hr]]>
+ Movies: HD + movies.hd + 16 +
- NFL.2012.12.16.Vikings.vs.Rams.720p.HDTV.x264-BAJSKORV - Mon, 17 Dec 2012 22:26:54 +0000 - http://api.omgwtfnzbs.org/sn.php?id=Z2fIr&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=Z2fIr&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 6.24 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:20:07
Added to usenet: 17/12/2012 22:26:54
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=Z2fIr]]>
- TV: HD - tv.hd - 20 - + Precious.Cargo.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 13:00:21 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Ehb5l&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Ehb5l&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.84 GB
Group: alt.binaries.felfelida
Added to index: 15/12/2016 06:20:31
Added to usenet: 08/01/2017 13:00:21
View NZB: https://omgwtfnzbs.me/details.php?id=Ehb5l]]>
+ Movies: HD + movies.hd + 16 +
- The.Gadget.Show.World.Tour.S02E07.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 22:23:04 +0000 - http://api.omgwtfnzbs.org/sn.php?id=NrREN&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=NrREN&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 341.1 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 22:29:02
Added to usenet: 17/12/2012 22:23:04
Weblink: http://thetvdb.com/?tab=series&id=258440&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=NrREN]]>
- TV: STD - tv.sd - 19 - + Pina.2011.DOC.PROPER.FRENCH.1080p.BluRay.x264-FiDELiO + Sun, 08 Jan 2017 13:00:05 +0200 + https://api.omgwtfnzbs.me/nzb/?id=cNsHi&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=cNsHi&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 8.98 GB
Group: alt.binaries.documentaries.french
Added to index: 01/12/2016 23:03:07
Added to usenet: 08/01/2017 13:00:05
View NZB: https://omgwtfnzbs.me/details.php?id=cNsHi]]>
+ Movies: HD + movies.hd + 16 +
- Redneck.Island.S02E06.HDTV.x264-YesTV - Mon, 17 Dec 2012 22:16:37 +0000 - http://api.omgwtfnzbs.org/sn.php?id=75b7e&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=75b7e&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 440.67 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:16:12
Added to usenet: 17/12/2012 22:16:37
Weblink: http://thetvdb.com/?tab=series&id=259570&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=75b7e]]>
- TV: STD - tv.sd - 19 - + Paranormal.Activity.The.Ghost.Dimension.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:59:36 +0200 + https://api.omgwtfnzbs.me/nzb/?id=uTXRQ&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=uTXRQ&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.05 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 06:39:37
Added to usenet: 08/01/2017 12:59:36
View NZB: https://omgwtfnzbs.me/details.php?id=uTXRQ]]>
+ Movies: HD + movies.hd + 16 +
- Chainsaw.Gang.S01E06.HDTV.x264-YesTV - Mon, 17 Dec 2012 22:15:52 +0000 - http://api.omgwtfnzbs.org/sn.php?id=G8QhV&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=G8QhV&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 259.24 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:15:38
Added to usenet: 17/12/2012 22:15:52
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=G8QhV]]>
- TV: STD - tv.sd - 19 - + Paper.Towns.2015.TRUEFRENCH.720p.BluRay.x264-MELBA + Sun, 08 Jan 2017 12:59:10 +0200 + https://api.omgwtfnzbs.me/nzb/?id=fq5pK&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=fq5pK&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:25:16
Added to usenet: 08/01/2017 12:59:10
View NZB: https://omgwtfnzbs.me/details.php?id=fq5pK]]>
+ Movies: HD + movies.hd + 16 +
- VH1.Divas.2012.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 22:15:23 +0000 - http://api.omgwtfnzbs.org/sn.php?id=dohtS&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=dohtS&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 1.16 GB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 22:20:56
Added to usenet: 17/12/2012 22:15:23
Weblink: http://lookpic.com/O/i2/227/bkl5VFGu.jpeg[/IMG
View NZB: http://omgwtfnzbs.org/details.php?id=dohtS]]>
- TV: STD - tv.sd - 19 - + Papa.Ou.Maman.2015.RERIP.FRENCH.DVDRip.x264-Ryotox + Sun, 08 Jan 2017 12:58:10 +0200 + https://api.omgwtfnzbs.me/nzb/?id=3WN1H&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=3WN1H&user=nzbdrone&api=nzbdrone + Category: Movies: DVD
Size: 585.4 MB
Group: alt.binaries.movies.french
Added to index: 12/12/2016 00:21:27
Added to usenet: 08/01/2017 12:58:10
View NZB: https://omgwtfnzbs.me/details.php?id=3WN1H]]>
+ Movies: DVD + movies.dvd + 17 +
- Chainsaw.Gang.S01E05.HDTV.x264-YesTV - Mon, 17 Dec 2012 22:14:57 +0000 - http://api.omgwtfnzbs.org/sn.php?id=PBGHM&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=PBGHM&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 196.5 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:14:43
Added to usenet: 17/12/2012 22:14:57
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=PBGHM]]>
- TV: STD - tv.sd - 19 - + Pan.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:57:29 +0200 + https://api.omgwtfnzbs.me/nzb/?id=RCeDE&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=RCeDE&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.08 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:27:53
Added to usenet: 08/01/2017 12:57:29
View NZB: https://omgwtfnzbs.me/details.php?id=RCeDE]]>
+ Movies: HD + movies.hd + 16 +
- Bad.Girls.S03E16.DVDRiP.XViD-PiX - Mon, 17 Dec 2012 22:10:30 +0000 - http://api.omgwtfnzbs.org/sn.php?id=JaeF7&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=JaeF7&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 397.05 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:10:16
Added to usenet: 17/12/2012 22:10:30
Weblink: http://thetvdb.com/?tab=series&id=75328&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=JaeF7]]>
- TV: STD - tv.sd - 19 - + Now.You.See.Me.2.2016.TRUEFRENCH.720p.BluRay.x264-PKPTRS + Sun, 08 Jan 2017 12:56:08 +0200 + https://api.omgwtfnzbs.me/nzb/?id=aFTn5&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=aFTn5&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.36 GB
Group: alt.binaries.felfelida
Added to index: 10/12/2016 10:15:44
Added to usenet: 08/01/2017 12:56:08
View NZB: https://omgwtfnzbs.me/details.php?id=aFTn5]]>
+ Movies: HD + movies.hd + 16 +
- Royal.Pains.S04E16.720p.WEB-DL.DD5.1.H.264-NTb - Mon, 17 Dec 2012 22:06:00 +0000 - http://api.omgwtfnzbs.org/sn.php?id=tlyYX&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=tlyYX&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.61 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 22:05:19
Added to usenet: 17/12/2012 22:06:00
Weblink: http://thetvdb.com/?tab=series&id=92411&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=tlyYX]]>
- TV: HD - tv.hd - 20 - + Now.You.See.Me.2.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:55:45 +0200 + https://api.omgwtfnzbs.me/nzb/?id=xvkz2&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=xvkz2&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.11 GB
Group: alt.binaries.felfelida
Added to index: 03/12/2016 09:20:25
Added to usenet: 08/01/2017 12:55:45
View NZB: https://omgwtfnzbs.me/details.php?id=xvkz2]]>
+ Movies: HD + movies.hd + 16 +
- Royal.Pains.S04E15.720p.WEB-DL.DD5.1.H.264-NTb - Mon, 17 Dec 2012 21:57:55 +0000 - http://api.omgwtfnzbs.org/sn.php?id=FJrFr&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=FJrFr&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.59 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:57:32
Added to usenet: 17/12/2012 21:57:55
Weblink: http://thetvdb.com/?tab=series&id=92411&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=FJrFr]]>
- TV: HD - tv.hd - 20 - + Morgan.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:55:10 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Lc2Az&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Lc2Az&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.83 GB
Group: alt.binaries.felfelida
Added to index: 10/12/2016 10:18:39
Added to usenet: 08/01/2017 12:55:10
View NZB: https://omgwtfnzbs.me/details.php?id=Lc2Az]]>
+ Movies: HD + movies.hd + 16 +
- Chainsaw.Gang.S01E06.720p.HDTV.x264-YesTV - Mon, 17 Dec 2012 21:54:29 +0000 - http://api.omgwtfnzbs.org/sn.php?id=OMgpi&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=OMgpi&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 874.25 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:54:04
Added to usenet: 17/12/2012 21:54:29
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=OMgpi]]>
- TV: HD - tv.hd - 20 - + Moms.Night.Out.2014.MULTi.1080p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:54:52 +0200 + https://api.omgwtfnzbs.me/nzb/?id=jIJMw&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=jIJMw&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 9.28 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 06:30:43
Added to usenet: 08/01/2017 12:54:52
View NZB: https://omgwtfnzbs.me/details.php?id=jIJMw]]>
+ Movies: HD + movies.hd + 16 +
- Chainsaw.Gang.S01E05.720p.HDTV.x264-YesTV - Mon, 17 Dec 2012 21:53:07 +0000 - http://api.omgwtfnzbs.org/sn.php?id=9jFDc&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=9jFDc&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 646.35 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:52:35
Added to usenet: 17/12/2012 21:53:07
Weblink: http://thetvdb.com/?tab=series&id=263322&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=9jFDc]]>
- TV: HD - tv.hd - 20 - + Career.Bed.1969.DVDRip.x264-FiCO + Sun, 08 Jan 2017 12:54:28 +0200 + https://api.omgwtfnzbs.me/nzb/?id=6yaYt&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=6yaYt&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 902.56 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 11:52:16
Added to usenet: 08/01/2017 12:54:28
View NZB: https://omgwtfnzbs.me/details.php?id=6yaYt]]>
+ Movies: STD + movies.sd + 15 +
- Redneck.Island.S02E06.720p.HDTV.x264-YesTV - Mon, 17 Dec 2012 21:51:58 +0000 - http://api.omgwtfnzbs.org/sn.php?id=48dBN&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=48dBN&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.38 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:50:34
Added to usenet: 17/12/2012 21:51:58
Weblink: http://thetvdb.com/?tab=series&id=259570&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=48dBN]]>
- TV: HD - tv.hd - 20 - + Mission.Impossible.Rogue.Nation.2015.TrueHD.Atmos.AC3.MULTISUBS.1080p.BluRay.x264.HQ-TUSAHD + Sun, 08 Jan 2017 12:54:06 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Uerkq&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Uerkq&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 16.69 GB
Group: alt.binaries.felfelida
Added to index: 03/12/2016 04:44:07
Added to usenet: 08/01/2017 12:54:06
View NZB: https://omgwtfnzbs.me/details.php?id=Uerkq]]>
+ Movies: HD + movies.hd + 16 +
- Drugs.Inc.S03E07.Hollywood.High.720p.HDTV.x264-YesTV - Mon, 17 Dec 2012 21:49:53 +0000 - http://api.omgwtfnzbs.org/sn.php?id=yY198&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=yY198&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.46 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:47:58
Added to usenet: 17/12/2012 21:49:53
Weblink: http://thetvdb.com/?tab=series&id=174501&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=yY198]]>
- TV: HD - tv.hd - 20 - + Miss.Peregrines.Home.for.Peculiar.Children.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:53:12 +0200 + https://api.omgwtfnzbs.me/nzb/?id=sL8wn&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=sL8wn&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.34 GB
Group: alt.binaries.felfelida
Added to index: 10/12/2016 10:09:56
Added to usenet: 08/01/2017 12:53:12
View NZB: https://omgwtfnzbs.me/details.php?id=sL8wn]]>
+ Movies: HD + movies.hd + 16 +
- Drugs.Inc.S03E07.Hollywood.High.HDTV.x264-YesTV - Mon, 17 Dec 2012 21:45:17 +0000 - http://api.omgwtfnzbs.org/sn.php?id=VH7uw&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=VH7uw&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 476.18 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:44:51
Added to usenet: 17/12/2012 21:45:17
Weblink: http://thetvdb.com/?tab=series&id=174501&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=VH7uw]]>
- TV: STD - tv.sd - 19 - + Miracles.from.Heaven.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:52:40 +0200 + https://api.omgwtfnzbs.me/nzb/?id=G1Q8k&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=G1Q8k&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.felfelida
Added to index: 02/12/2016 05:21:58
Added to usenet: 08/01/2017 12:52:40
View NZB: https://omgwtfnzbs.me/details.php?id=G1Q8k]]>
+ Movies: HD + movies.hd + 16 +
- Tron.Uprising.S01E13.720p.WEB-DL.H.264.DD5.1-iT00NZ - Mon, 17 Dec 2012 21:44:59 +0000 - http://api.omgwtfnzbs.org/sn.php?id=eJxUn&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=eJxUn&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 807.76 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:44:13
Added to usenet: 17/12/2012 21:44:59
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=eJxUn]]>
- TV: HD - tv.hd - 20 - + Miles.Ahead.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:51:15 +0200 + https://api.omgwtfnzbs.me/nzb/?id=vnMUP&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=vnMUP&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.06 GB
Group: alt.binaries.felfelida
Added to index: 02/12/2016 05:17:15
Added to usenet: 08/01/2017 12:51:15
View NZB: https://omgwtfnzbs.me/details.php?id=vnMUP]]>
+ Movies: HD + movies.hd + 16 +
- The.Gadget.Show.World.Tour.S02E07.720p.HDTV.x264-FTP - Mon, 17 Dec 2012 21:27:34 +0000 - http://api.omgwtfnzbs.org/sn.php?id=Wawxv&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=Wawxv&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.28 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 21:27:23
Added to usenet: 17/12/2012 21:27:34
Weblink: http://thetvdb.com/?tab=series&id=258440&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=Wawxv]]>
- TV: HD - tv.hd - 20 - + Mike.and.Dave.Need.Wedding.Dates.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:50:54 +0200 + https://api.omgwtfnzbs.me/nzb/?id=JTdgM&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=JTdgM&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 04/12/2016 06:25:11
Added to usenet: 08/01/2017 12:50:54
View NZB: https://omgwtfnzbs.me/details.php?id=JTdgM]]>
+ Movies: HD + movies.hd + 16 +
- VH1.Divas.2012.720p.HDTV.x264-2HD - Mon, 17 Dec 2012 20:57:15 +0000 - http://api.omgwtfnzbs.org/sn.php?id=Tdz1e&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=Tdz1e&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 3.3 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 20:56:27
Added to usenet: 17/12/2012 20:57:15
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=Tdz1e]]>
- TV: HD - tv.hd - 20 - + Mia.Madre.2015.FRENCH.720p.BluRay.x264-DEAL + Sun, 08 Jan 2017 12:50:30 +0200 + https://api.omgwtfnzbs.me/nzb/?id=h9usJ&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=h9usJ&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.08 GB
Group: alt.binaries.felfelida
Added to index: 15/12/2016 05:54:50
Added to usenet: 08/01/2017 12:50:30
View NZB: https://omgwtfnzbs.me/details.php?id=h9usJ]]>
+ Movies: HD + movies.hd + 16 +
- Bamazon.S01E02.Heart.of.Darkness.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 20:25:17 +0000 - http://api.omgwtfnzbs.org/sn.php?id=hSrC6&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=hSrC6&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 599.56 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 20:31:05
Added to usenet: 17/12/2012 20:25:17
Weblink: http://thetvdb.com/?tab=series&id=263659&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=hSrC6]]>
- TV: STD - tv.sd - 19 - + Mechanic.Resurrection.2016.RERiP.FRENCH.720p.BluRay.x264-ZEST + Sun, 08 Jan 2017 12:50:08 +0200 + https://api.omgwtfnzbs.me/nzb/?id=4qC4G&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=4qC4G&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.82 GB
Group: alt.binaries.felfelida
Added to index: 10/12/2016 07:00:11
Added to usenet: 08/01/2017 12:50:08
View NZB: https://omgwtfnzbs.me/details.php?id=4qC4G]]>
+ Movies: HD + movies.hd + 16 +
- Bamazon.S01E02.Heart.of.Darkness.720p.HDTV.x264-DHD - Mon, 17 Dec 2012 19:35:19 +0000 - http://api.omgwtfnzbs.org/sn.php?id=Iy0YV&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=Iy0YV&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.93 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:33:31
Added to usenet: 17/12/2012 19:35:19
Weblink: http://thetvdb.com/?tab=series&id=263659&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=Iy0YV]]>
- TV: HD - tv.hd - 20 - + Maze.Runner.The.Scorch.Trials.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:49:48 +0200 + https://api.omgwtfnzbs.me/nzb/?id=SWTGD&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=SWTGD&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.36 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:29:53
Added to usenet: 08/01/2017 12:49:48
View NZB: https://omgwtfnzbs.me/details.php?id=SWTGD]]>
+ Movies: HD + movies.hd + 16 +
- Tron.Uprising.S01E13.1080p.WEB-DL.H.264.DD5.1-iT00NZ - Mon, 17 Dec 2012 19:34:42 +0000 - http://api.omgwtfnzbs.org/sn.php?id=S0q8M&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=S0q8M&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.02 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:33:44
Added to usenet: 17/12/2012 19:34:42
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=S0q8M]]>
- TV: HD - tv.hd - 20 - + Marauders.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:49:26 +0200 + https://api.omgwtfnzbs.me/nzb/?id=qCkSA&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=qCkSA&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 03/12/2016 06:33:57
Added to usenet: 08/01/2017 12:49:26
View NZB: https://omgwtfnzbs.me/details.php?id=qCkSA]]>
+ Movies: HD + movies.hd + 16 +
- Robot.Chicken.S06E13.1080p.WEB-DL.H.264.AAC2.0-iT00NZ - Mon, 17 Dec 2012 19:31:14 +0000 - http://api.omgwtfnzbs.org/sn.php?id=MQE67&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=MQE67&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 485.75 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:31:08
Added to usenet: 17/12/2012 19:31:14
Weblink: http://thetvdb.com/?tab=series&id=75734&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=MQE67]]>
- TV: HD - tv.hd - 20 - + Maggie.2015.TRUEFRENCH.720p.BluRay.x264-Ryotox + Sun, 08 Jan 2017 12:48:44 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Dt1e7&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Dt1e7&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.felfelida
Added to index: 10/12/2016 22:13:04
Added to usenet: 08/01/2017 12:48:44
View NZB: https://omgwtfnzbs.me/details.php?id=Dt1e7]]>
+ Movies: HD + movies.hd + 16 +
- Teenage.Mutant.Ninja.Turtles.2012.S01E12.It.Came.From.the.Depths.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 19:31:12 +0000 - http://api.omgwtfnzbs.org/sn.php?id=k6VI4&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=k6VI4&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 141.12 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 19:37:18
Added to usenet: 17/12/2012 19:31:12
Weblink: http://thetvdb.com/?tab=series&id=261451&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=k6VI4]]>
- TV: STD - tv.sd - 19 - + Mad.Max.Fury.Road.2015.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:46:53 +0200 + https://api.omgwtfnzbs.me/nzb/?id=zFa21&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=zFa21&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.12 GB
Group: alt.binaries.felfelida
Added to index: 11/12/2016 01:26:53
Added to usenet: 08/01/2017 12:46:53
View NZB: https://omgwtfnzbs.me/details.php?id=zFa21]]>
+ Movies: HD + movies.hd + 16 +
- Teenage.Mutant.Ninja.Turtles.2012.S01E12.1080p.WEB-DL.H.264.AAC2.0-iT00NZ - Mon, 17 Dec 2012 19:30:55 +0000 - http://api.omgwtfnzbs.org/sn.php?id=7mmU1&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=7mmU1&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 980.75 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:30:42
Added to usenet: 17/12/2012 19:30:55
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=7mmU1]]>
- TV: HD - tv.hd - 20 - + London.Has.Fallen.2016.TRUEFRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 12:46:22 +0200 + https://api.omgwtfnzbs.me/nzb/?id=MvrDy&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=MvrDy&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.12 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 23:06:45
Added to usenet: 08/01/2017 12:46:22
View NZB: https://omgwtfnzbs.me/details.php?id=MvrDy]]>
+ Movies: HD + movies.hd + 16 +
- Bamazon.S01E02.HDTV.x264-KILLERS - Mon, 17 Dec 2012 19:29:18 +0000 - http://api.omgwtfnzbs.org/sn.php?id=eVtFp&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=eVtFp&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 666.34 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:29:11
Added to usenet: 17/12/2012 19:29:18
Weblink: http://thetvdb.com/?tab=series&id=263659&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=eVtFp]]>
- TV: STD - tv.sd - 19 - + Life.On.The.Line.2015.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:45:59 +0200 + https://api.omgwtfnzbs.me/nzb/?id=kc8Pv&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=kc8Pv&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.12 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 06:33:01
Added to usenet: 08/01/2017 12:45:59
View NZB: https://omgwtfnzbs.me/details.php?id=kc8Pv]]>
+ Movies: HD + movies.hd + 16 +
- Cross.Country.Skiing.World.Cup.2012.12.16.Canmore.Womens.Skiathlon.720p.HDTV.x264-SKIS - Mon, 17 Dec 2012 19:28:43 +0000 - http://api.omgwtfnzbs.org/sn.php?id=PsRdj&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=PsRdj&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.84 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 19:28:15
Added to usenet: 17/12/2012 19:28:43
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=PsRdj]]>
- TV: HD - tv.hd - 20 - + Le.Corps.De.Mon.Ennemi.1976.FRENCH.720p.BluRay.x264-DuSS + Sun, 08 Jan 2017 12:45:38 +0200 + https://api.omgwtfnzbs.me/nzb/?id=8IPbs&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=8IPbs&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.38 GB
Group: alt.binaries.movies.french
Added to index: 10/12/2016 08:00:35
Added to usenet: 08/01/2017 12:45:38
View NZB: https://omgwtfnzbs.me/details.php?id=8IPbs]]>
+ Movies: HD + movies.hd + 16 +
- Robot.Chicken.S06E13.Robot.Chickens.Atm.Christmas.Special.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 19:17:22 +0000 - http://api.omgwtfnzbs.org/sn.php?id=cNUlC&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=cNUlC&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 109.35 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 19:23:28
Added to usenet: 17/12/2012 19:17:22
Weblink: http://thetvdb.com/?tab=series&id=75734&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=cNUlC]]>
- TV: STD - tv.sd - 19 - + Last.Knights.2015.TRUEFRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 12:45:11 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Vygnp&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Vygnp&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 27/12/2016 10:06:24
Added to usenet: 08/01/2017 12:45:11
View NZB: https://omgwtfnzbs.me/details.php?id=Vygnp]]>
+ Movies: HD + movies.hd + 16 +
- Bobs.Burgers.S03E09.God.Rest.Ye.Merry.Gentle-Mannequins.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 19:04:10 +0000 - http://api.omgwtfnzbs.org/sn.php?id=LbeeT&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=LbeeT&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 101.75 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 19:10:14
Added to usenet: 17/12/2012 19:04:10
Weblink: http://thetvdb.com/?tab=series&id=194031&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=LbeeT]]>
- TV: STD - tv.sd - 19 - + La.Vallee.1972.FRENCH.720p.BluRay.x264-FiDELiO + Sun, 08 Jan 2017 12:44:43 +0200 + https://api.omgwtfnzbs.me/nzb/?id=texzm&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=texzm&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.11 GB
Group: alt.binaries.movies.french
Added to index: 11/12/2016 23:55:30
Added to usenet: 08/01/2017 12:44:43
View NZB: https://omgwtfnzbs.me/details.php?id=texzm]]>
+ Movies: HD + movies.hd + 16 +
- Ax.Men.S06E02.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 19:01:22 +0000 - http://api.omgwtfnzbs.org/sn.php?id=UeUEK&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=UeUEK&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 730.17 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 19:07:05
Added to usenet: 17/12/2012 19:01:22
Weblink: http://thetvdb.com/?tab=series&id=81578&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=UeUEK]]>
- TV: STD - tv.sd - 19 - + Kind.Hearts.and.Coronets.1949.FRENCH.720p.BluRay.x264-FiDELiO + Sun, 08 Jan 2017 12:44:04 +0200 + https://api.omgwtfnzbs.me/nzb/?id=HLFBj&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=HLFBj&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.movies.french
Added to index: 04/12/2016 02:58:43
Added to usenet: 08/01/2017 12:44:04
View NZB: https://omgwtfnzbs.me/details.php?id=HLFBj]]>
+ Movies: HD + movies.hd + 16 +
- Teenage.Mutant.Ninja.Turtles.2012.S01E12.720p.WEB-DL.H.264.AAC2.0-iT00NZ - Mon, 17 Dec 2012 17:06:46 +0000 - http://api.omgwtfnzbs.org/sn.php?id=BkFcy&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=BkFcy&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 802.96 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 17:06:05
Added to usenet: 17/12/2012 17:06:46
Weblink: http://thetvdb.com/?tab=series&id=261451&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=BkFcy]]>
- TV: HD - tv.hd - 20 - + Kill.Command.2016.FRENCH.720p.BluRay.x264-ZEST + Sun, 08 Jan 2017 12:43:12 +0200 + https://api.omgwtfnzbs.me/nzb/?id=f1WNg&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=f1WNg&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 29/11/2016 07:31:47
Added to usenet: 08/01/2017 12:43:12
View NZB: https://omgwtfnzbs.me/details.php?id=f1WNg]]>
+ Movies: HD + movies.hd + 16 +
- Robot.Chicken.S06E13.720p.WEB-DL.H.264.AAC2.0-iT00NZ - Mon, 17 Dec 2012 16:41:36 +0000 - http://api.omgwtfnzbs.org/sn.php?id=69daj&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=69daj&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 382.44 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 16:41:30
Added to usenet: 17/12/2012 16:41:36
Weblink: http://thetvdb.com/?tab=series&id=75734&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=69daj]]>
- TV: HD - tv.hd - 20 - + Joy.2015.FRENCH.720p.BluRay.x264-PiNKPANTERS + Sun, 08 Jan 2017 12:42:45 +0200 + https://api.omgwtfnzbs.me/nzb/?id=2hnZd&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=2hnZd&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.14 GB
Group: alt.binaries.felfelida
Added to index: 04/12/2016 06:40:26
Added to usenet: 08/01/2017 12:42:45
View NZB: https://omgwtfnzbs.me/details.php?id=2hnZd]]>
+ Movies: HD + movies.hd + 16 +
- Last.Man.Standing.2011.S02E07.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 16:39:12 +0000 - http://api.omgwtfnzbs.org/sn.php?id=rWByd&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=rWByd&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 129.91 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 16:45:18
Added to usenet: 17/12/2012 16:39:12
Weblink: http://thetvdb.com/?tab=series&id=248834&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=rWByd]]>
- TV: STD - tv.sd - 19 - + Jean.de.Florette.1985.720p.BluRay.x264-PFa + Sun, 08 Jan 2017 12:42:23 +0200 + https://api.omgwtfnzbs.me/nzb/?id=QO4la&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=QO4la&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.32 GB
Group: alt.binaries.movies.french
Added to index: 06/12/2016 09:19:17
Added to usenet: 08/01/2017 12:42:23
View NZB: https://omgwtfnzbs.me/details.php?id=QO4la]]>
+ Movies: HD + movies.hd + 16 +
- WWE.Tables.Ladders.and.Chairs.2012.PPV.HDTV.x264-KYR - Mon, 17 Dec 2012 16:28:06 +0000 - http://api.omgwtfnzbs.org/sn.php?id=cskMX&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=cskMX&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 2.34 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 16:25:20
Added to usenet: 17/12/2012 16:28:06
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=cskMX]]>
- TV: STD - tv.sd - 19 - + Jaws.3.1983.FRENCH.720p.BluRay.x264-ULSHD + Sun, 08 Jan 2017 12:41:58 +0200 + https://api.omgwtfnzbs.me/nzb/?id=o4LxX&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=o4LxX&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.movies.french
Added to index: 08/12/2016 23:03:02
Added to usenet: 08/01/2017 12:41:58
View NZB: https://omgwtfnzbs.me/details.php?id=o4LxX]]>
+ Movies: HD + movies.hd + 16 +
- Damages.S05E03.Failure.Is.Failure.720p.WEB-DL.DD5.1.H.264-BS - Mon, 17 Dec 2012 16:19:22 +0000 - http://api.omgwtfnzbs.org/sn.php?id=OFIkR&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=OFIkR&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.05 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 16:17:32
Added to usenet: 17/12/2012 16:19:22
Weblink: http://thetvdb.com/?tab=series&id=80367&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=OFIkR]]>
- TV: HD - tv.hd - 20 - + Jaws.2.1978.FRENCH.720p.BluRay.x264-ULSHD + Sun, 08 Jan 2017 12:41:29 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Bkd9U&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Bkd9U&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.movies.french
Added to index: 10/12/2016 22:34:02
Added to usenet: 08/01/2017 12:41:29
View NZB: https://omgwtfnzbs.me/details.php?id=Bkd9U]]>
+ Movies: HD + movies.hd + 16 +
- CSI.S13E10.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 16:17:48 +0000 - http://api.omgwtfnzbs.org/sn.php?id=XI8KI&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=XI8KI&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 274.77 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 16:23:46
Added to usenet: 17/12/2012 16:17:48
Weblink: http://thetvdb.com/?tab=series&id=233851&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=XI8KI]]>
- TV: STD - tv.sd - 19 - + In.the.Heart.of.the.Sea.2015.TRUEFRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 12:40:42 +0200 + https://api.omgwtfnzbs.me/nzb/?id=ZQuLR&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=ZQuLR&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.08 GB
Group: alt.binaries.felfelida
Added to index: 29/11/2016 23:05:34
Added to usenet: 08/01/2017 12:40:42
View NZB: https://omgwtfnzbs.me/details.php?id=ZQuLR]]>
+ Movies: HD + movies.hd + 16 +
- Damages.S05E02.Have.You.Met.the.Eel.Yet.720p.WEB-DL.DD5.1.H.264-BS - Mon, 17 Dec 2012 16:17:23 +0000 - http://api.omgwtfnzbs.org/sn.php?id=uyPWF&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=uyPWF&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.63 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 16:16:43
Added to usenet: 17/12/2012 16:17:23
Weblink: http://thetvdb.com/?tab=series&id=80367&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=uyPWF]]>
- TV: HD - tv.hd - 20 - + Ils.Sont.Partout.2016.FRENCH.1080p.WEB.H264-SiGeRiS + Sun, 08 Jan 2017 12:39:57 +0200 + https://api.omgwtfnzbs.me/nzb/?id=FCqh6&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=FCqh6&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 4.22 GB
Group: alt.binaries.movies.french
Added to index: 29/11/2016 22:56:32
Added to usenet: 08/01/2017 12:39:57
View NZB: https://omgwtfnzbs.me/details.php?id=FCqh6]]>
+ Movies: HD + movies.hd + 16 +
- Damages.S05E01.You.Want.to.End.This.Once.and.for.All.720p.WEB-DL.DD5.1.H.264-BS - Mon, 17 Dec 2012 16:14:47 +0000 - http://api.omgwtfnzbs.org/sn.php?id=41F66&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=41F66&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.75 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 16:13:48
Added to usenet: 17/12/2012 16:14:47
Weblink: http://thetvdb.com/?tab=series&id=80367&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=41F66]]>
- TV: HD - tv.hd - 20 - + I.Saw.the.Light.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:38:44 +0200 + https://api.omgwtfnzbs.me/nzb/?id=ct7t3&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=ct7t3&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.36 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 06:28:23
Added to usenet: 08/01/2017 12:38:44
View NZB: https://omgwtfnzbs.me/details.php?id=ct7t3]]>
+ Movies: HD + movies.hd + 16 +
- Damages.S05.720p.WEB-DL.DD5.1.H.264 - Mon, 17 Dec 2012 15:44:20 +0000 - http://api.omgwtfnzbs.org/sn.php?id=1sqbT&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=1sqbT&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 18.76 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:26:02
Added to usenet: 17/12/2012 15:44:20
Weblink: http://thetvdb.com/?tab=series&id=80367&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=1sqbT]]>
- TV: HD - tv.hd - 20 - + Hoosiers.1986.FRENCH.720p.BluRay.x264-DuSS + Sun, 08 Jan 2017 12:37:43 +0200 + https://api.omgwtfnzbs.me/nzb/?id=OFgGx&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=OFgGx&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.movies.french
Added to index: 08/12/2016 23:07:15
Added to usenet: 08/01/2017 12:37:43
View NZB: https://omgwtfnzbs.me/details.php?id=OFgGx]]>
+ Movies: HD + movies.hd + 16 +
- Criminal.Minds.S08E11.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 15:21:48 +0000 - http://api.omgwtfnzbs.org/sn.php?id=Hq7GY&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=Hq7GY&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 243.65 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 15:27:51
Added to usenet: 17/12/2012 15:21:48
Weblink: http://thetvdb.com/?tab=series&id=75710&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=Hq7GY]]>
- TV: STD - tv.sd - 19 - + Home.On.The.Range.2004.FRENCH.720p.BluRay.x264.DTS-MUxHD + Sun, 08 Jan 2017 12:37:22 +0200 + https://api.omgwtfnzbs.me/nzb/?id=9cEer&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=9cEer&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 2.56 GB
Group: alt.binaries.movies.french
Added to index: 04/12/2016 02:59:32
Added to usenet: 08/01/2017 12:37:22
View NZB: https://omgwtfnzbs.me/details.php?id=9cEer]]>
+ Movies: HD + movies.hd + 16 +
- Ax.Men.S06E02.720p.HDTV.x264-KILLERS - Mon, 17 Dec 2012 15:21:40 +0000 - http://api.omgwtfnzbs.org/sn.php?id=fWOSV&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=fWOSV&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.93 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:19:52
Added to usenet: 17/12/2012 15:21:40
Weblink: http://thetvdb.com/?tab=series&id=81578&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=fWOSV]]>
- TV: HD - tv.hd - 20 - + Hitman.Agent.47.2015.TRUEFRENCH.720p.BluRay.x264-MELBA + Sun, 08 Jan 2017 12:37:07 +0200 + https://api.omgwtfnzbs.me/nzb/?id=XIVqo&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=XIVqo&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.82 GB
Group: alt.binaries.felfelida
Added to index: 27/12/2016 12:26:04
Added to usenet: 08/01/2017 12:37:07
View NZB: https://omgwtfnzbs.me/details.php?id=XIVqo]]>
+ Movies: HD + movies.hd + 16 +
- Last.Man.Standing.2011.S02E07.720p.WEB-DL.DD5.1.h.264-pcsyndicate - Mon, 17 Dec 2012 15:09:28 +0000 - http://api.omgwtfnzbs.org/sn.php?id=dNzxs&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=dNzxs&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 778.83 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:09:10
Added to usenet: 17/12/2012 15:09:28
Weblink: http://thetvdb.com/?tab=series&id=248834&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=dNzxs]]>
- TV: HD - tv.hd - 20 - + Hitman.Agent.47.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:36:51 +0200 + https://api.omgwtfnzbs.me/nzb/?id=If4Ei&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=If4Ei&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.08 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:35:11
Added to usenet: 08/01/2017 12:36:51
View NZB: https://omgwtfnzbs.me/details.php?id=If4Ei]]>
+ Movies: HD + movies.hd + 16 +
- Last.Man.Standing.2011.S02E06.720p.WEB-DL.DD5.1.h.264-pcsyndicate - Mon, 17 Dec 2012 15:08:45 +0000 - http://api.omgwtfnzbs.org/sn.php?id=mQpXj&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=mQpXj&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 788.79 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:08:33
Added to usenet: 17/12/2012 15:08:45
Weblink: http://thetvdb.com/?tab=series&id=248834&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=mQpXj]]>
- TV: HD - tv.hd - 20 - + Hibou.2016.FRENCH.1080p.WEB.h264-TiMELiNE + Sun, 08 Jan 2017 12:36:24 +0200 + https://api.omgwtfnzbs.me/nzb/?id=gLLQf&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=gLLQf&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.58 GB
Group: alt.binaries.movies.french
Added to index: 10/12/2016 07:42:56
Added to usenet: 08/01/2017 12:36:24
View NZB: https://omgwtfnzbs.me/details.php?id=gLLQf]]>
+ Movies: HD + movies.hd + 16 +
- Last.Man.Standing.2011.S02E05.720p.WEB-DL.DD5.1.h.264-pcsyndicate - Mon, 17 Dec 2012 15:08:03 +0000 - http://api.omgwtfnzbs.org/sn.php?id=XnNvd&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=XnNvd&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 791.55 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:07:40
Added to usenet: 17/12/2012 15:08:03
Weblink: http://thetvdb.com/?tab=series&id=248834&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=XnNvd]]>
- TV: HD - tv.hd - 20 - + Hello.My.Name.Is.Doris.2015.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 12:35:49 +0200 + https://api.omgwtfnzbs.me/nzb/?id=D4SCT&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=D4SCT&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 06:56:54
Added to usenet: 08/01/2017 12:35:49
View NZB: https://omgwtfnzbs.me/details.php?id=D4SCT]]>
+ Movies: HD + movies.hd + 16 +
- Last.Man.Standing.2011.S02E04.720p.WEB-DL.DD5.1.h.264-pcsyndicate - Mon, 17 Dec 2012 15:07:27 +0000 - http://api.omgwtfnzbs.org/sn.php?id=J9wJX&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=J9wJX&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 782.39 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:07:19
Added to usenet: 17/12/2012 15:07:27
Weblink: http://thetvdb.com/?tab=series&id=248834&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=J9wJX]]>
- TV: HD - tv.hd - 20 - + Hell.or.High.Water.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:34:49 +0200 + https://api.omgwtfnzbs.me/nzb/?id=akjOQ&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=akjOQ&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 01/12/2016 06:16:27
Added to usenet: 08/01/2017 12:34:49
View NZB: https://omgwtfnzbs.me/details.php?id=akjOQ]]>
+ Movies: HD + movies.hd + 16 +
- CSI.S13E10.1080p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 15:03:32 +0000 - http://api.omgwtfnzbs.org/sn.php?id=pt35L&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=pt35L&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.88 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 15:02:42
Added to usenet: 17/12/2012 15:03:32
Weblink: http://thetvdb.com/?tab=series&id=233851&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=pt35L]]>
- TV: HD - tv.hd - 20 - + Heist.2015.LIMITED.FRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 12:34:29 +0200 + https://api.omgwtfnzbs.me/nzb/?id=yR0aN&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=yR0aN&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:42:58
Added to usenet: 08/01/2017 12:34:29
View NZB: https://omgwtfnzbs.me/details.php?id=yR0aN]]>
+ Movies: HD + movies.hd + 16 +
- CSI.S13E10.720p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 14:55:24 +0000 - http://api.omgwtfnzbs.org/sn.php?id=2O6Co&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=2O6Co&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.49 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:54:47
Added to usenet: 17/12/2012 14:55:24
Weblink: http://thetvdb.com/?tab=series&id=233851&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=2O6Co]]>
- TV: HD - tv.hd - 20 - + Hail.Caesar.2016.FRENCH.720p.BluRay.x264-MELBA + Sun, 08 Jan 2017 12:33:57 +0200 + https://api.omgwtfnzbs.me/nzb/?id=jnYyH&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=jnYyH&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.felfelida
Added to index: 15/12/2016 05:51:34
Added to usenet: 08/01/2017 12:33:57
View NZB: https://omgwtfnzbs.me/details.php?id=jnYyH]]>
+ Movies: HD + movies.hd + 16 +
- Criminal.Minds.S08E11.1080p.WEB-DL.DD5.1.H264-NFHD - Mon, 17 Dec 2012 14:54:32 +0000 - http://api.omgwtfnzbs.org/sn.php?id=nkeai&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=nkeai&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.84 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:54:24
Added to usenet: 17/12/2012 14:54:32
Weblink: http://thetvdb.com/?tab=series&id=75710&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=nkeai]]>
- TV: HD - tv.hd - 20 - + Ghostbusters.2016.EXTENDED.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 12:33:15 +0200 + https://api.omgwtfnzbs.me/nzb/?id=VA7MB&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=VA7MB&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.08 GB
Group: alt.binaries.felfelida
Added to index: 03/12/2016 23:02:06
Added to usenet: 08/01/2017 12:33:15
View NZB: https://omgwtfnzbs.me/details.php?id=VA7MB]]>
+ Movies: HD + movies.hd + 16 +
- Criminal.Minds.S08E11.720p.WEB-DL.DD5.1.H264-NFHD - Mon, 17 Dec 2012 14:53:29 +0000 - http://api.omgwtfnzbs.org/sn.php?id=wnUAZ&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=wnUAZ&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.44 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:52:56
Added to usenet: 17/12/2012 14:53:29
Weblink: http://thetvdb.com/?tab=series&id=75710&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=wnUAZ]]>
- TV: HD - tv.hd - 20 - + 13.Minutes.2015.SUBFRENCH.720p.BluRay.x264-DuSS + Sun, 08 Jan 2017 12:32:22 +0200 + https://api.omgwtfnzbs.me/nzb/?id=1tE7z&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=1tE7z&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.38 GB
Group: alt.binaries.movies.french
Added to index: 04/12/2016 06:09:35
Added to usenet: 08/01/2017 12:32:22
View NZB: https://omgwtfnzbs.me/details.php?id=1tE7z]]>
+ Movies: HD + movies.hd + 16 +
- Ax.Men.S06E02.HDTV.x264-KILLERS - Mon, 17 Dec 2012 14:45:18 +0000 - http://api.omgwtfnzbs.org/sn.php?id=rCs8K&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=rCs8K&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 908.3 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:45:11
Added to usenet: 17/12/2012 14:45:18
Weblink: http://thetvdb.com/?tab=series&id=81578&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=rCs8K]]>
- TV: STD - tv.sd - 19 - + George.Harrison.Living.in.the.Material.World.2011.Part1.SUBFRENCH.720p.BluRay.x264-FiDELiO + Sun, 08 Jan 2017 12:30:49 +0200 + https://api.omgwtfnzbs.me/nzb/?id=hfATe&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=hfATe&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.2 GB
Group: alt.binaries.movies.french
Added to index: 01/12/2016 06:58:06
Added to usenet: 08/01/2017 12:30:49
View NZB: https://omgwtfnzbs.me/details.php?id=hfATe]]>
+ Movies: HD + movies.hd + 16 +
- Two.and.a.Half.Men.S10E11.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 14:37:12 +0000 - http://api.omgwtfnzbs.org/sn.php?id=cZQWE&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=cZQWE&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 150.91 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 14:43:18
Added to usenet: 17/12/2012 14:37:12
Weblink: http://thetvdb.com/?tab=series&id=72227&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=cZQWE]]>
- TV: STD - tv.sd - 19 - + Fantastic.Four.2015.TRUEFRENCH.720p.BluRay.x264-Goatlove + Sun, 08 Jan 2017 12:29:06 +0200 + https://api.omgwtfnzbs.me/nzb/?id=5LRfb&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=5LRfb&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.07 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 05:47:13
Added to usenet: 08/01/2017 12:29:06
View NZB: https://omgwtfnzbs.me/details.php?id=5LRfb]]>
+ Movies: HD + movies.hd + 16 +
- Two.and.a.Half.Men.S10E11.1080p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 14:28:06 +0000 - http://api.omgwtfnzbs.org/sn.php?id=lcG65&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=lcG65&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 997 MB
Group: alt.binaries.etc
Added to index: 17/12/2012 14:27:39
Added to usenet: 17/12/2012 14:28:06
Weblink: http://thetvdb.com/?tab=series&id=72227&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=lcG65]]>
- TV: HD - tv.hd - 20 - + En.Busca.De.Marsupilami.2012.SPANiSH.MULTi.1080p.BluRay.x264-TORO + Sun, 08 Jan 2017 11:57:24 +0200 + https://api.omgwtfnzbs.me/nzb/?id=A7UGC&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=A7UGC&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 10.27 GB
Group: alt.binaries.movies.french
Added to index: 16/12/2016 22:56:11
Added to usenet: 08/01/2017 11:57:24
View NZB: https://omgwtfnzbs.me/details.php?id=A7UGC]]>
+ Movies: HD + movies.hd + 16 +
- EPL.2012.12.15.Queens.Park.Rangers.Vs.Fulham.720p.HDTV.x264-W4F - Mon, 17 Dec 2012 14:26:25 +0000 - http://api.omgwtfnzbs.org/sn.php?id=WyoTz&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=WyoTz&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.76 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:26:04
Added to usenet: 17/12/2012 14:26:25
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=WyoTz]]>
- TV: HD - tv.hd - 20 - + En.Busca.De.Marsupilami.2012.SPANiSH.MULTi.720p.BluRay.x264-TORO + Sun, 08 Jan 2017 11:56:17 +0200 + https://api.omgwtfnzbs.me/nzb/?id=JAJq3&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=JAJq3&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.43 GB
Group: alt.binaries.movies.french
Added to index: 14/12/2016 23:24:51
Added to usenet: 08/01/2017 11:56:17
View NZB: https://omgwtfnzbs.me/details.php?id=JAJq3]]>
+ Movies: HD + movies.hd + 16 +
- Two.and.a.Half.Men.S10E11.720p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 14:24:01 +0000 - http://api.omgwtfnzbs.org/sn.php?id=ILMrt&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=ILMrt&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 814.36 MB
Group: alt.binaries.etc
Added to index: 17/12/2012 14:23:31
Added to usenet: 17/12/2012 14:24:01
Weblink: http://thetvdb.com/?tab=series&id=72227&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=ILMrt]]>
- TV: HD - tv.hd - 20 - + Eddie.the.Eagle.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 11:51:32 +0200 + https://api.omgwtfnzbs.me/nzb/?id=bFyzl&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=bFyzl&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 15/12/2016 05:49:10
Added to usenet: 08/01/2017 11:51:32
View NZB: https://omgwtfnzbs.me/details.php?id=bFyzl]]>
+ Movies: HD + movies.hd + 16 +
- Dont.Trust.The.B----.In.Apartment.23.S02E06.1080p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 14:15:40 +0000 - http://api.omgwtfnzbs.org/sn.php?id=ROCRk&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=ROCRk&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 948.82 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:15:24
Added to usenet: 17/12/2012 14:15:40
Weblink: http://thetvdb.com/?tab=series&id=248812&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=ROCRk]]>
- TV: HD - tv.hd - 20 - + Dont.Breathe.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 11:51:00 +0200 + https://api.omgwtfnzbs.me/nzb/?id=ywGBi&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=ywGBi&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.82 GB
Group: alt.binaries.felfelida
Added to index: 04/12/2016 01:49:16
Added to usenet: 08/01/2017 11:51:00
View NZB: https://omgwtfnzbs.me/details.php?id=ywGBi]]>
+ Movies: HD + movies.hd + 16 +
- Dont.Trust.The.B----.In.Apartment.23.S02E06.720p.WEB-DL.DD5.1.H.264-NFHD - Mon, 17 Dec 2012 14:11:29 +0000 - http://api.omgwtfnzbs.org/sn.php?id=y7JDY&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=y7JDY&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 800.34 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 14:10:37
Added to usenet: 17/12/2012 14:11:29
Weblink: http://thetvdb.com/?tab=series&id=248812&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=y7JDY]]>
- TV: HD - tv.hd - 20 - + Dirty.Grandpa.2016.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 11:50:04 +0200 + https://api.omgwtfnzbs.me/nzb/?id=eiCXN&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=eiCXN&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.11 GB
Group: alt.binaries.felfelida
Added to index: 29/11/2016 08:48:32
Added to usenet: 08/01/2017 11:50:04
View NZB: https://omgwtfnzbs.me/details.php?id=eiCXN]]>
+ Movies: HD + movies.hd + 16 +
- WWE.Tables.Ladders.and.Chairs.2012.PPV.720p.HDTV.x264-KYR - Mon, 17 Dec 2012 13:42:12 +0000 - http://api.omgwtfnzbs.org/sn.php?id=1ZWlA&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=1ZWlA&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 6.51 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 13:40:11
Added to usenet: 17/12/2012 13:42:12
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=1ZWlA]]>
- TV: HD - tv.hd - 20 - + Debarquement.Immediat.2016.FRENCH.1080p.WEB.h264-TiMELiNE + Sun, 08 Jan 2017 11:47:55 +0200 + https://api.omgwtfnzbs.me/nzb/?id=nl27E&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=nl27E&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.92 GB
Group: alt.binaries.movies.french
Added to index: 10/12/2016 06:18:33
Added to usenet: 08/01/2017 11:47:55
View NZB: https://omgwtfnzbs.me/details.php?id=nl27E]]>
+ Movies: HD + movies.hd + 16 +
- Ben.10.Omniverse.S01E14.Blukic.and.Driba.Go.To.Mr.Smooth.480p.WEB-DL.x264-mSD - Mon, 17 Dec 2012 13:29:42 +0000 - http://api.omgwtfnzbs.org/sn.php?id=51kIm&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=51kIm&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 115.21 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 13:35:47
Added to usenet: 17/12/2012 13:29:42
Weblink: http://thetvdb.com/?tab=series&id=260995&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=51kIm]]>
- TV: STD - tv.sd - 19 - + Daddys.Home.2015.TRUEFRENCH.720p.BluRay.x264-PiNKPANTERS + Sun, 08 Jan 2017 11:47:24 +0200 + https://api.omgwtfnzbs.me/nzb/?id=BRJJB&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=BRJJB&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.82 GB
Group: alt.binaries.felfelida
Added to index: 29/11/2016 23:20:30
Added to usenet: 08/01/2017 11:47:24
View NZB: https://omgwtfnzbs.me/details.php?id=BRJJB]]>
+ Movies: HD + movies.hd + 16 +
- The.Horses.of.McBride.2012.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 13:07:12 +0000 - http://api.omgwtfnzbs.org/sn.php?id=tqWEI&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=tqWEI&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 517.24 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 13:13:05
Added to usenet: 17/12/2012 13:07:12
Weblink: http://lookpic.com/O/i2/395/Vl9n9DEY.jpeg[/IMG
View NZB: http://omgwtfnzbs.org/details.php?id=tqWEI]]>
- TV: STD - tv.sd - 19 - + Confirmation.2016.FRENCH.720p.BluRay.x264-VENUE + Sun, 08 Jan 2017 11:47:03 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Z7aV8&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Z7aV8&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.32 GB
Group: alt.binaries.felfelida
Added to index: 02/12/2016 05:13:09
Added to usenet: 08/01/2017 11:47:03
View NZB: https://omgwtfnzbs.me/details.php?id=Z7aV8]]>
+ Movies: HD + movies.hd + 16 +
- Royal.Pains.S04E15E16.PROPER.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 12:38:32 +0000 - http://api.omgwtfnzbs.org/sn.php?id=OREVT&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=OREVT&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 449.31 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 12:44:26
Added to usenet: 17/12/2012 12:38:32
Weblink: http://thetvdb.com/?tab=series&id=92411&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=OREVT]]>
- TV: STD - tv.sd - 19 - + Ben.Hur.2016.TRUEFRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 11:43:55 +0200 + https://api.omgwtfnzbs.me/nzb/?id=wnrh5&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=wnrh5&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.12 GB
Group: alt.binaries.felfelida
Added to index: 04/12/2016 01:47:16
Added to usenet: 08/01/2017 11:43:55
View NZB: https://omgwtfnzbs.me/details.php?id=wnrh5]]>
+ Movies: HD + movies.hd + 16 +
- The.Bachelorette.Special.Ashley.and.J.Ps.Wedding.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 12:03:56 +0000 - http://api.omgwtfnzbs.org/sn.php?id=MIq0q&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=MIq0q&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 660.6 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 12:09:44
Added to usenet: 17/12/2012 12:03:56
Weblink: http://lookpic.com/O/i2/871/MNxJjwjR.jpeg[/IMG
View NZB: http://omgwtfnzbs.org/details.php?id=MIq0q]]>
- TV: STD - tv.sd - 19 - + Bastille.Day.2016.FRENCH.720p.BluRay.x264-AiRLiNE + Sun, 08 Jan 2017 11:43:05 +0200 + https://api.omgwtfnzbs.me/nzb/?id=5qhGw&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=5qhGw&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.1 GB
Group: alt.binaries.felfelida
Added to index: 24/10/2016 08:48:11
Added to usenet: 08/01/2017 11:43:05
View NZB: https://omgwtfnzbs.me/details.php?id=5qhGw]]>
+ Movies: HD + movies.hd + 16 +
- Finding.Bigfoot.S03E06.Bigfoot.and.Wolverines.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:43:23 +0000 - http://api.omgwtfnzbs.org/sn.php?id=3ObPU&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=3ObPU&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 329.13 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:49:20
Added to usenet: 17/12/2012 11:43:23
Weblink: http://thetvdb.com/?tab=series&id=249235&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=3ObPU]]>
- TV: STD - tv.sd - 19 - + American.Ultra.2015.FRENCH.720p.BluRay.x264-LOST + Sun, 08 Jan 2017 11:42:18 +0200 + https://api.omgwtfnzbs.me/nzb/?id=0G5Eh&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=0G5Eh&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.78 GB
Group: alt.binaries.felfelida
Added to index: 06/12/2016 09:18:28
Added to usenet: 08/01/2017 11:42:18
View NZB: https://omgwtfnzbs.me/details.php?id=0G5Eh]]>
+ Movies: HD + movies.hd + 16 +
- Oliver.Stones.Untold.History.Of.The.United.States.S01E04.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:35:52 +0000 - http://api.omgwtfnzbs.org/sn.php?id=CRQzL&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=CRQzL&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 466.2 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:41:44
Added to usenet: 17/12/2012 11:35:52
Weblink: http://thetvdb.com/?tab=series&id=263532&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=CRQzL]]>
- TV: STD - tv.sd - 19 - + Alvin.and.the.Chipmunks.The.Road.Chip.2015.TRUEFRENCH.720p.BluRay.x264-PiNKPANTERS + Sun, 08 Jan 2017 11:41:16 +0200 + https://api.omgwtfnzbs.me/nzb/?id=NwMQe&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=NwMQe&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.83 GB
Group: alt.binaries.felfelida
Added to index: 15/12/2016 05:42:04
Added to usenet: 08/01/2017 11:41:16
View NZB: https://omgwtfnzbs.me/details.php?id=NwMQe]]>
+ Movies: HD + movies.hd + 16 +
- The.Fith.Estate.2012.11.16.Left.For.Dead.720p.HDTV.x264-TWG - Mon, 17 Dec 2012 11:32:54 +0000 - http://api.omgwtfnzbs.org/sn.php?id=LUGZC&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=LUGZC&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.04 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 11:29:30
Added to usenet: 17/12/2012 11:32:54
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=LUGZC]]>
- TV: HD - tv.hd - 20 - + Destroy.All.Monsters.1968.1080p.BluRay.x264-SADPANDA + Sun, 08 Jan 2017 08:19:34 +0200 + https://api.omgwtfnzbs.me/nzb/?id=0xGb4&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=0xGb4&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 8.7 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 07:12:46
Added to usenet: 08/01/2017 08:19:34
View NZB: https://omgwtfnzbs.me/details.php?id=0xGb4]]>
+ Movies: HD + movies.hd + 16 +
- The.Fith.Estate.2012.11.23.Lance.Armstrong.Master.Of.Spin.720p.HDTV.x264-TWG - Mon, 17 Dec 2012 11:32:13 +0000 - http://api.omgwtfnzbs.org/sn.php?id=jAXl9&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=jAXl9&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.27 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 11:31:22
Added to usenet: 17/12/2012 11:32:13
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=jAXl9]]>
- TV: HD - tv.hd - 20 - + Distance.Between.Dreams.2016.iNTERNAL.1080p.WEBRip.x264-13 + Sun, 08 Jan 2017 07:27:03 +0200 + https://api.omgwtfnzbs.me/nzb/?id=UjPQb&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=UjPQb&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.84 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 06:24:20
Added to usenet: 08/01/2017 07:27:03
View NZB: https://omgwtfnzbs.me/details.php?id=UjPQb]]>
+ Movies: HD + movies.hd + 16 +
- Curiosity.S02E10.What.Destroyed.the.Hindenburg.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:24:42 +0000 - http://api.omgwtfnzbs.org/sn.php?id=dZviu&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=dZviu&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 315.97 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:26:21
Added to usenet: 17/12/2012 11:24:42
Weblink: http://thetvdb.com/?tab=series&id=250572&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=dZviu]]>
- TV: STD - tv.sd - 19 - + Distance.Between.Dreams.2016.iNTERNAL.720p.WEBRip.x264-13 + Sun, 08 Jan 2017 07:13:18 +0200 + https://api.omgwtfnzbs.me/nzb/?id=Sb0vy&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=Sb0vy&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.22 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 06:10:17
Added to usenet: 08/01/2017 07:13:18
View NZB: https://omgwtfnzbs.me/details.php?id=Sb0vy]]>
+ Movies: HD + movies.hd + 16 +
- The.Horses.of.McBride.2012.720p.HDTV.x264-2HD - Mon, 17 Dec 2012 11:24:42 +0000 - http://api.omgwtfnzbs.org/sn.php?id=1GCur&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=1GCur&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.59 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 11:23:11
Added to usenet: 17/12/2012 11:24:42
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=1GCur]]>
- TV: HD - tv.hd - 20 - + Distance.Between.Dreams.2016.iNTERNAL.WEBRip.x264-13 + Sun, 08 Jan 2017 06:59:47 +0200 + https://api.omgwtfnzbs.me/nzb/?id=QScRL&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=QScRL&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 1.05 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 05:57:24
Added to usenet: 08/01/2017 06:59:47
View NZB: https://omgwtfnzbs.me/details.php?id=QScRL]]>
+ Movies: STD + movies.sd + 15 +
- Peep.Show.S08E04.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:16:50 +0000 - http://api.omgwtfnzbs.org/sn.php?id=VqvC2&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=VqvC2&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 133.76 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:22:54
Added to usenet: 17/12/2012 11:16:50
Weblink: http://thetvdb.com/?tab=series&id=71656&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=VqvC2]]>
- TV: STD - tv.sd - 19 - + Destroy.All.Monsters.1968.REAL.READNFO.BDRip.x264-VoMiT + Sun, 08 Jan 2017 06:32:49 +0200 + https://api.omgwtfnzbs.me/nzb/?id=apH4B&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=apH4B&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 885.99 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 05:30:51
Added to usenet: 08/01/2017 06:32:49
View NZB: https://omgwtfnzbs.me/details.php?id=apH4B]]>
+ Movies: STD + movies.sd + 15 +
- Royal.Pains.S04E15E16.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:13:38 +0000 - http://api.omgwtfnzbs.org/sn.php?id=BcrXh&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=BcrXh&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 474.45 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:19:31
Added to usenet: 17/12/2012 11:13:38
Weblink: http://thetvdb.com/?tab=series&id=92411&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=BcrXh]]>
- TV: STD - tv.sd - 19 - + Destroy.All.Monsters.1968.720p.BluRay.x264-SADPANDA + Sun, 08 Jan 2017 06:27:17 +0200 + https://api.omgwtfnzbs.me/nzb/?id=yVYG8&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=yVYG8&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 3.72 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 05:23:58
Added to usenet: 08/01/2017 06:27:17
View NZB: https://omgwtfnzbs.me/details.php?id=yVYG8]]>
+ Movies: HD + movies.hd + 16 +
- Strictly.Come.Dancing.S10E22.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 11:02:01 +0000 - http://api.omgwtfnzbs.org/sn.php?id=TiWhP&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=TiWhP&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 1 GB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 11:07:22
Added to usenet: 17/12/2012 11:02:01
Weblink: http://thetvdb.com/?tab=series&id=83127&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=TiWhP]]>
- TV: STD - tv.sd - 19 - + The.Brain.That.Wouldnt.Die.1962.OAR.BDRip.x264-VoMiT + Sun, 08 Jan 2017 06:18:27 +0200 + https://api.omgwtfnzbs.me/nzb/?id=YgsZY&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=YgsZY&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 993.5 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 05:16:32
Added to usenet: 08/01/2017 06:18:27
View NZB: https://omgwtfnzbs.me/details.php?id=YgsZY]]>
+ Movies: STD + movies.sd + 15 +
- Survivor.S25E15.Reunion.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 10:38:13 +0000 - http://api.omgwtfnzbs.org/sn.php?id=hXOrr&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=hXOrr&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 216.49 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 10:44:14
Added to usenet: 17/12/2012 10:38:13
Weblink: http://thetvdb.com/?tab=series&id=76733&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=hXOrr]]>
- TV: STD - tv.sd - 19 - + Coin.Heist.2017.WEBRip.X264-DEFLATE + Sun, 08 Jan 2017 06:04:01 +0200 + https://api.omgwtfnzbs.me/nzb/?id=NVeV4&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=NVeV4&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 779.85 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 05:01:51
Added to usenet: 08/01/2017 06:04:01
View NZB: https://omgwtfnzbs.me/details.php?id=NVeV4]]>
+ Movies: STD + movies.sd + 15 +
- Strictly.Come.Dancing.S10E23.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 10:32:15 +0000 - http://api.omgwtfnzbs.org/sn.php?id=bwmpc&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=bwmpc&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 413.76 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 10:38:10
Added to usenet: 17/12/2012 10:32:15
Weblink: http://thetvdb.com/?tab=series&id=83127&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=bwmpc]]>
- TV: STD - tv.sd - 19 - + Keeping.Up.With.The.Joneses.2016.REAL.REPACK.1080p.BluRay.x264-DRONES + Sun, 08 Jan 2017 04:33:31 +0200 + https://api.omgwtfnzbs.me/nzb/?id=aVxDs&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=aVxDs&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 8.82 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 03:30:47
Added to usenet: 08/01/2017 04:33:31
View NZB: https://omgwtfnzbs.me/details.php?id=aVxDs]]>
+ Movies: HD + movies.hd + 16 +
- Tron.Uprising.S01E13.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 10:22:39 +0000 - http://api.omgwtfnzbs.org/sn.php?id=u2RzK&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=u2RzK&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 152.83 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 10:28:43
Added to usenet: 17/12/2012 10:22:39
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=u2RzK]]>
- TV: STD - tv.sd - 19 - + Keeping.Up.With.The.Joneses.2016.REAL.REPACK.720p.BluRay.x264-DRONES + Sun, 08 Jan 2017 03:51:11 +0200 + https://api.omgwtfnzbs.me/nzb/?id=4I63P&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=4I63P&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.29 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 02:48:06
Added to usenet: 08/01/2017 03:51:11
View NZB: https://omgwtfnzbs.me/details.php?id=4I63P]]>
+ Movies: HD + movies.hd + 16 +
- The.Rolling.Stones.Live.One.More.Shot.PPV.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 10:18:56 +0000 - http://api.omgwtfnzbs.org/sn.php?id=oRpx5&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=oRpx5&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 1.88 GB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 10:13:24
Added to usenet: 17/12/2012 10:18:56
Weblink: http://lookpic.com/O/i2/1911/mc9I2FoX.jpeg[/IMG
View NZB: http://omgwtfnzbs.org/details.php?id=oRpx5]]>
- TV: STD - tv.sd - 19 - + Keeping.Up.With.The.Joneses.2016.REAL.REPACK.BDRip.x264-DRONES + Sun, 08 Jan 2017 03:39:16 +0200 + https://api.omgwtfnzbs.me/nzb/?id=YsoKa&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=YsoKa&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 798.64 MB
Group: alt.binaries.moovee
Added to index: 08/01/2017 02:37:32
Added to usenet: 08/01/2017 03:39:16
View NZB: https://omgwtfnzbs.me/details.php?id=YsoKa]]>
+ Movies: STD + movies.sd + 15 +
- The.Bachelorette.Special.Ashley.and.J.Ps.Wedding.720p.HDTV.x264-2HD - Mon, 17 Dec 2012 10:16:15 +0000 - http://api.omgwtfnzbs.org/sn.php?id=6XV6n&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=6XV6n&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 3.09 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 10:14:43
Added to usenet: 17/12/2012 10:16:15
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=6XV6n]]>
- TV: HD - tv.hd - 20 - + USS.Indianapolis.Men.of.Courage.2016.1080p.BluRay.x264-EiDER + Sun, 08 Jan 2017 03:00:29 +0200 + https://api.omgwtfnzbs.me/nzb/?id=kZI0P&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=kZI0P&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 10.04 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 01:56:21
Added to usenet: 08/01/2017 03:00:29
View NZB: https://omgwtfnzbs.me/details.php?id=kZI0P]]>
+ Movies: HD + movies.hd + 16 +
- Finding.Bigfoot.S03E06.Bigfoot.and.Wolverines.720p.HDTV.x264-DHD - Mon, 17 Dec 2012 10:01:45 +0000 - http://api.omgwtfnzbs.org/sn.php?id=OdAGV&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=OdAGV&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.27 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 09:59:38
Added to usenet: 17/12/2012 10:01:45
Weblink: http://thetvdb.com/?tab=series&id=249235&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=OdAGV]]>
- TV: HD - tv.hd - 20 - + The.Front.Page.1931.1080p.BluRay.x264-CiNEFiLE + Sun, 08 Jan 2017 02:56:25 +0200 + https://api.omgwtfnzbs.me/nzb/?id=8FZCM&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=8FZCM&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 9.2 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 01:53:40
Added to usenet: 08/01/2017 02:56:25
View NZB: https://omgwtfnzbs.me/details.php?id=8FZCM]]>
+ Movies: HD + movies.hd + 16 +
- VH1.Divas.2012.HDTV.x264-2HD - Mon, 17 Dec 2012 09:39:00 +0000 - http://api.omgwtfnzbs.org/sn.php?id=S5No7&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=S5No7&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 1.21 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 09:37:21
Added to usenet: 17/12/2012 09:39:00
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=S5No7]]>
- TV: STD - tv.sd - 19 - + The.Front.Page.1931.720p.BluRay.x264-CiNEFiLE + Sun, 08 Jan 2017 02:48:44 +0200 + https://api.omgwtfnzbs.me/nzb/?id=o1VY1&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=o1VY1&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 5.09 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 01:46:38
Added to usenet: 08/01/2017 02:48:44
View NZB: https://omgwtfnzbs.me/details.php?id=o1VY1]]>
+ Movies: HD + movies.hd + 16 +
- Match.Of.The.Day.Two.2012.12.16.480p.HDTV.x264-mSD - Mon, 17 Dec 2012 09:36:29 +0000 - http://api.omgwtfnzbs.org/sn.php?id=DRwC1&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=DRwC1&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 610.31 MB
Group: alt.binaries.multimedia
Added to index: 17/12/2012 09:42:34
Added to usenet: 17/12/2012 09:36:29
Weblink: http://lookpic.com/O/i2/1718/8EOt9D9x.jpeg[/IMG
View NZB: http://omgwtfnzbs.org/details.php?id=DRwC1]]>
- TV: STD - tv.sd - 19 - + Battleground.1949.1080p.BluRay.x264-SiNNERS + Sun, 08 Jan 2017 02:32:20 +0200 + https://api.omgwtfnzbs.me/nzb/?id=TnRtg&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=TnRtg&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 13.82 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 01:26:16
Added to usenet: 08/01/2017 02:32:20
View NZB: https://omgwtfnzbs.me/details.php?id=TnRtg]]>
+ Movies: HD + movies.hd + 16 +
- The.Horses.of.McBride.2012.HDTV.x264-2HD - Mon, 17 Dec 2012 09:13:27 +0000 - http://api.omgwtfnzbs.org/sn.php?id=b8DNy&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=b8DNy&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 913.02 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 09:11:30
Added to usenet: 17/12/2012 09:13:27
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=b8DNy]]>
- TV: STD - tv.sd - 19 - -
+ Battleground.1949.720p.BluRay.x264-SiNNERS + Sun, 08 Jan 2017 02:24:46 +0200 + https://api.omgwtfnzbs.me/nzb/?id=9ZOFL&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=9ZOFL&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 7.55 GB
Group: alt.binaries.moovee
Added to index: 08/01/2017 01:21:45
Added to usenet: 08/01/2017 02:24:46
View NZB: https://omgwtfnzbs.me/details.php?id=9ZOFL]]>
+ Movies: HD + movies.hd + 16 + +
- Homeland.S02E12.720p.WEB-DL.DD5.1.H.264-DON - Mon, 17 Dec 2012 08:33:56 +0000 - http://api.omgwtfnzbs.org/sn.php?id=kB2xp&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=kB2xp&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.3 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 08:33:22
Added to usenet: 17/12/2012 08:33:56
Weblink: http://thetvdb.com/?tab=series&id=247897&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=kB2xp]]>
- TV: HD - tv.hd - 20 - -
+ Children.of.Men.2006.iNTERNAL.1080p.BluRay.x264-LiBRARiANS + Sun, 08 Jan 2017 00:28:33 +0200 + https://api.omgwtfnzbs.me/nzb/?id=4zHPm&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=4zHPm&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 12.88 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 23:21:55
Added to usenet: 08/01/2017 00:28:33
View NZB: https://omgwtfnzbs.me/details.php?id=4zHPm]]>
+ Movies: HD + movies.hd + 16 + + - Tron.Uprising.S01E13.720p.HDTV.x264-2HD - Mon, 17 Dec 2012 08:25:53 +0000 - http://api.omgwtfnzbs.org/sn.php?id=YzotL&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=YzotL&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 975.2 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 08:25:16
Added to usenet: 17/12/2012 08:25:53
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=YzotL]]>
- TV: HD - tv.hd - 20 - -
+ Children.of.Men.2006.iNTERNAL.BDRip.x264-LiBRARiANS + Sun, 08 Jan 2017 00:17:59 +0200 + https://api.omgwtfnzbs.me/nzb/?id=klDlR&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=klDlR&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 1.37 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 23:15:28
Added to usenet: 08/01/2017 00:17:59
View NZB: https://omgwtfnzbs.me/details.php?id=klDlR]]>
+ Movies: STD + movies.sd + 15 + + - Tron.Uprising.S01E13.HDTV.x264-2HD - Mon, 17 Dec 2012 08:24:55 +0000 - http://api.omgwtfnzbs.org/sn.php?id=i2eTC&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=i2eTC&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 269.56 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 08:24:23
Added to usenet: 17/12/2012 08:24:55
Weblink: http://thetvdb.com/?tab=series&id=258480&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=i2eTC]]>
- TV: STD - tv.sd - 19 - -
+ Children.of.Men.2006.PROPER.720p.BluRay.x264-SADPANDA + Sat, 07 Jan 2017 22:20:51 +0200 + https://api.omgwtfnzbs.me/nzb/?id=sAuS0&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=sAuS0&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.29 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 21:18:21
Added to usenet: 07/01/2017 22:20:51
View NZB: https://omgwtfnzbs.me/details.php?id=sAuS0]]>
+ Movies: HD + movies.hd + 16 + + - Homeland.S02E12.The.Choice.720p.WEB-DL.DD5.1.H.264-DON - Mon, 17 Dec 2012 08:03:24 +0000 - http://api.omgwtfnzbs.org/sn.php?id=WrQOi&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=WrQOi&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 2.41 GB
Group: alt.binaries.tv
Added to index: 17/12/2012 07:11:03
Added to usenet: 17/12/2012 08:03:24
Weblink: http://thetvdb.com/?tab=series&id=247897&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=WrQOi]]>
- TV: HD - tv.hd - 20 - -
+ Camino.2008.1080p.BluRay.DD5.1.x264-DON + Sat, 07 Jan 2017 21:09:01 +0200 + https://api.omgwtfnzbs.me/nzb/?id=PPGp9&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=PPGp9&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 18.69 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 20:03:14
Added to usenet: 07/01/2017 21:09:01
View NZB: https://omgwtfnzbs.me/details.php?id=PPGp9]]>
+ Movies: HD + movies.hd + 16 + + - T.I.and.Tiny.The.Family.Hustle.S02E16.HDTV.x264-CRiMSON - Mon, 17 Dec 2012 07:38:05 +0000 - http://api.omgwtfnzbs.org/sn.php?id=aJdwK&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=aJdwK&user=nzbdrone&api=nzbdrone - Category: TV: STD
Size: 222.29 MB
Group: alt.binaries.teevee
Added to index: 17/12/2012 07:37:25
Added to usenet: 17/12/2012 07:38:05
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=aJdwK]]>
- TV: STD - tv.sd - 19 - -
+ Charlie.St.Cloud.2010.1080p.BluRay.DD5.1.x264-DON + Sat, 07 Jan 2017 20:38:12 +0200 + https://api.omgwtfnzbs.me/nzb/?id=fMXUE&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=fMXUE&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 13.13 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 19:33:26
Added to usenet: 07/01/2017 20:38:12
View NZB: https://omgwtfnzbs.me/details.php?id=fMXUE]]>
+ Movies: HD + movies.hd + 16 + + - Curiosity.S02E10.What.Destroyed.the.Hindenburg.720p.HDTV.x264-DHD - Mon, 17 Dec 2012 07:38:05 +0000 - http://api.omgwtfnzbs.org/sn.php?id=CdMkN&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=CdMkN&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 1.18 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 07:36:22
Added to usenet: 17/12/2012 07:38:05
Weblink: http://thetvdb.com/?tab=series&id=250572&lid=7
View NZB: http://omgwtfnzbs.org/details.php?id=CdMkN]]>
- TV: HD - tv.hd - 20 - -
+ USS.Indianapolis.Men.of.Courage.2016.720p.BluRay.x264-EiDER + Sat, 07 Jan 2017 19:03:29 +0200 + https://api.omgwtfnzbs.me/nzb/?id=fYTFG&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=fYTFG&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 6.26 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 18:00:32
Added to usenet: 07/01/2017 19:03:29
View NZB: https://omgwtfnzbs.me/details.php?id=fYTFG]]>
+ Movies: HD + movies.hd + 16 + + - EPL.2012.12.16.West.Bromwich.Albion.Vs.West.Ham.United.720p.HDTV.x264-FAIRPLAY - Mon, 17 Dec 2012 07:29:04 +0000 - http://api.omgwtfnzbs.org/sn.php?id=LgCKE&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=LgCKE&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 3.62 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 07:28:35
Added to usenet: 17/12/2012 07:29:04
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=LgCKE]]>
- TV: HD - tv.hd - 20 - -
+ USS.Indianapolis.Men.of.Courage.2016.BDRip.x264-EiDER + Sat, 07 Jan 2017 18:52:00 +0200 + https://api.omgwtfnzbs.me/nzb/?id=6gxyp&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=6gxyp&user=nzbdrone&api=nzbdrone + Category: Movies: STD
Size: 1.27 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 17:50:00
Added to usenet: 07/01/2017 18:52:00
View NZB: https://omgwtfnzbs.me/details.php?id=6gxyp]]>
+ Movies: STD + movies.sd + 15 + + - NFL.2012.12.16.Broncos.vs.Ravens.720p.HDTV.x264-BAJSKORV - Mon, 17 Dec 2012 07:26:56 +0000 - http://api.omgwtfnzbs.org/sn.php?id=jMTWB&user=nzbdrone&api=nzbdrone - http://api.omgwtfnzbs.org/sn.php?id=jMTWB&user=nzbdrone&api=nzbdrone - Category: TV: HD
Size: 5.93 GB
Group: alt.binaries.teevee
Added to index: 17/12/2012 07:25:16
Added to usenet: 17/12/2012 07:26:56
Weblink: N/A
View NZB: http://omgwtfnzbs.org/details.php?id=jMTWB]]>
- TV: HD - tv.hd - 20 - + End.of.a.Gun.2016.1080p.BluRay.DD5.1.x264-TayTO + Sat, 07 Jan 2017 18:24:21 +0200 + https://api.omgwtfnzbs.me/nzb/?id=tNLPi&user=nzbdrone&api=nzbdrone + https://api.omgwtfnzbs.me/nzb/?id=tNLPi&user=nzbdrone&api=nzbdrone + Category: Movies: HD
Size: 10.96 GB
Group: alt.binaries.moovee
Added to index: 07/01/2017 17:20:34
Added to usenet: 07/01/2017 18:24:21
View NZB: https://omgwtfnzbs.me/details.php?id=tNLPi]]>
+ Movies: HD + movies.hd + 16 +
diff --git a/src/NzbDrone.Core.Test/IndexerTests/BitMeTvTests/BitMeTvFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BitMeTvTests/BitMeTvFixture.cs deleted file mode 100644 index d49d940a4..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/BitMeTvTests/BitMeTvFixture.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.BitMeTv; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using System; -using System.Linq; -using FluentAssertions; - -namespace NzbDrone.Core.Test.IndexerTests.BitMeTvTests -{ - [TestFixture] - public class BitMeTvFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "BitMeTV", - Settings = new BitMeTvSettings() { Cookie = "uid=123" } - }; - } - - [Test] - public void should_parse_recent_feed_from_BitMeTv() - { - var recentFeed = ReadAllText(@"Files/Indexers/BitMeTv/BitMeTv.xml"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(5); - releases.First().Should().BeOfType(); - - var torrentInfo = releases.First() as TorrentInfo; - - torrentInfo.Title.Should().Be("Total.Divas.S02E08.HDTV.x264-CRiMSON"); - torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - torrentInfo.DownloadUrl.Should().Be("http://www.bitmetv.org/download.php/12/Total.Divas.S02E08.HDTV.x264-CRiMSON.torrent"); - torrentInfo.InfoUrl.Should().BeNullOrEmpty(); - torrentInfo.CommentUrl.Should().BeNullOrEmpty(); - torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/13 17:04:29")); - torrentInfo.Size.Should().Be(395009065); - torrentInfo.InfoHash.Should().Be(null); - torrentInfo.MagnetUrl.Should().Be(null); - torrentInfo.Peers.Should().Be(null); - torrentInfo.Seeders.Should().Be(null); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetFixture.cs deleted file mode 100644 index a22ba44e3..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/BroadcastheNetTests/BroadcastheNetFixture.cs +++ /dev/null @@ -1,161 +0,0 @@ -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.BroadcastheNet; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; -using System; -using System.Linq; -using FluentAssertions; - -namespace NzbDrone.Core.Test.IndexerTests.BroadcastheNetTests -{ - [TestFixture] - public class BroadcastheNetFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "BroadcastheNet", - Settings = new BroadcastheNetSettings() { ApiKey = "abc", BaseUrl = "https://api.btnapps.net/" } - }; - } - - [Test] - public void should_parse_recent_feed_from_BroadcastheNet() - { - var recentFeed = ReadAllText(@"Files/Indexers/BroadcastheNet/RecentFeed.json"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(2); - releases.First().Should().BeOfType(); - - var torrentInfo = releases.First() as TorrentInfo; - - torrentInfo.Guid.Should().Be("BTN-123"); - torrentInfo.Title.Should().Be("Jimmy.Kimmel.2014.09.15.Jane.Fonda.HDTV.x264-aAF"); - torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - torrentInfo.DownloadUrl.Should().Be("https://broadcasthe.net/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"); - torrentInfo.InfoUrl.Should().Be("https://broadcasthe.net/torrents.php?id=237457&torrentid=123"); - torrentInfo.CommentUrl.Should().BeNullOrEmpty(); - torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/09/16 21:15:33")); - torrentInfo.Size.Should().Be(505099926); - torrentInfo.InfoHash.Should().Be("123"); - torrentInfo.TvdbId.Should().Be(71998); - torrentInfo.TvRageId.Should().Be(4055); - torrentInfo.MagnetUrl.Should().BeNullOrEmpty(); - torrentInfo.Peers.Should().Be(40+9); - torrentInfo.Seeders.Should().Be(40); - - torrentInfo.Origin.Should().Be("Scene"); - torrentInfo.Source.Should().Be("HDTV"); - torrentInfo.Container.Should().Be("MP4"); - torrentInfo.Codec.Should().Be("x264"); - torrentInfo.Resolution.Should().Be("SD"); - } - - private void VerifyBackOff() - { - Mocker.GetMock() - .Verify(v => v.RecordFailure(It.IsAny(), It.IsAny()), Times.Once()); - } - - [Test] - public void should_back_off_on_bad_request() - { - Mocker.GetMock() - .Setup(v => v.Execute(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.BadRequest)); - - var results = Subject.FetchRecent(); - - results.Should().BeEmpty(); - - VerifyBackOff(); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_back_off_and_report_api_key_invalid() - { - Mocker.GetMock() - .Setup(v => v.Execute(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.Unauthorized)); - - var results = Subject.FetchRecent(); - - results.Should().BeEmpty(); - - VerifyBackOff(); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_back_off_on_unknown_method() - { - Mocker.GetMock() - .Setup(v => v.Execute(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.NotFound)); - - var results = Subject.FetchRecent(); - - results.Should().BeEmpty(); - - VerifyBackOff(); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_back_off_api_limit_reached() - { - Mocker.GetMock() - .Setup(v => v.Execute(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0], System.Net.HttpStatusCode.ServiceUnavailable)); - - var results = Subject.FetchRecent(); - - results.Should().BeEmpty(); - - VerifyBackOff(); - - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void should_replace_https_http_as_needed() - { - var recentFeed = ReadAllText(@"Files/Indexers/BroadcastheNet/RecentFeed.json"); - - (Subject.Definition.Settings as BroadcastheNetSettings).BaseUrl = "http://api.btnapps.net/"; - - recentFeed = recentFeed.Replace("http:", "https:"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.POST))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(2); - releases.First().Should().BeOfType(); - - var torrentInfo = releases.First() as TorrentInfo; - - torrentInfo.DownloadUrl.Should().Be("http://broadcasthe.net/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"); - torrentInfo.InfoUrl.Should().Be("http://broadcasthe.net/torrents.php?id=237457&torrentid=123"); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubFixture.cs deleted file mode 100644 index ed8587e38..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/FanzubTests/FanzubFixture.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Fanzub; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.IndexerTests.FanzubTests -{ - [TestFixture] - public class FanzubFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "Fanzub", - Settings = new FanzubSettings() - }; - } - - [Test] - public void should_parse_recent_feed_from_fanzub() - { - var recentFeed = ReadAllText(@"Files/Indexers/Fanzub/fanzub.xml"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(3); - - var releaseInfo = releases.First(); - - releaseInfo.Title.Should().Be("[Vivid] Hanayamata - 10 [A33D6606]"); - releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); - releaseInfo.DownloadUrl.Should().Be("http://fanzub.com/nzb/296464/Vivid%20Hanayamata%20-%2010.nzb"); - releaseInfo.InfoUrl.Should().BeNullOrEmpty(); - releaseInfo.CommentUrl.Should().BeNullOrEmpty(); - releaseInfo.Indexer.Should().Be(Subject.Definition.Name); - releaseInfo.PublishDate.Should().Be(DateTime.Parse("2014/09/13 12:56:53")); - releaseInfo.Size.Should().Be(556246858); - } - } -} diff --git a/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs index c5542b943..d79a61236 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/OmgwtfnzbsTests/OmgwtfnzbsFixture.cs @@ -42,14 +42,14 @@ namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests var releaseInfo = releases.First(); - releaseInfo.Title.Should().Be("Stephen.Fry.Gadget.Man.S01E05.HDTV.x264-C4TV"); + releaseInfo.Title.Should().Be("Un.Petit.Boulot.2016.FRENCH.720p.BluRay.DTS.x264-LOST"); releaseInfo.DownloadProtocol.Should().Be(DownloadProtocol.Usenet); - releaseInfo.DownloadUrl.Should().Be("http://api.omgwtfnzbs.org/sn.php?id=OAl4g&user=nzbdrone&api=nzbdrone"); - releaseInfo.InfoUrl.Should().Be("http://omgwtfnzbs.org/details.php?id=OAl4g"); + releaseInfo.DownloadUrl.Should().Be("https://api.omgwtfnzbs.me/nzb/?id=8a2Bw&user=nzbdrone&api=nzbdrone"); + releaseInfo.InfoUrl.Should().Be("https://omgwtfnzbs.me/details.php?id=8a2Bw"); releaseInfo.CommentUrl.Should().BeNullOrEmpty(); releaseInfo.Indexer.Should().Be(Subject.Definition.Name); - releaseInfo.PublishDate.Should().Be(DateTime.Parse("2012/12/17 23:30:13")); - releaseInfo.Size.Should().Be(236822906); + releaseInfo.PublishDate.Should().Be(DateTime.Parse("2017/01/09 00:16:54")); + releaseInfo.Size.Should().Be(5354909355); } } } diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs deleted file mode 100644 index 8ecb58144..000000000 --- a/src/NzbDrone.Core.Test/IndexerTests/TorrentleechTests/TorrentleechFixture.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Torrentleech; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using System; -using System.Linq; -using FluentAssertions; - -namespace NzbDrone.Core.Test.IndexerTests.TorrentleechTests -{ - [TestFixture] - public class TorrentleechFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new IndexerDefinition() - { - Name = "Torrentleech", - Settings = new TorrentleechSettings() - }; - } - - [Test] - public void should_parse_recent_feed_from_Torrentleech() - { - var recentFeed = ReadAllText(@"Files/Indexers/Torrentleech/Torrentleech.xml"); - - Mocker.GetMock() - .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.GET))) - .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); - - var releases = Subject.FetchRecent(); - - releases.Should().HaveCount(5); - releases.First().Should().BeOfType(); - - var torrentInfo = releases.First() as TorrentInfo; - - torrentInfo.Title.Should().Be("Classic Car Rescue S02E04 720p HDTV x264-C4TV"); - torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); - torrentInfo.DownloadUrl.Should().Be("http://www.torrentleech.org/rss/download/513575/1234/Classic.Car.Rescue.S02E04.720p.HDTV.x264-C4TV.torrent"); - torrentInfo.InfoUrl.Should().Be("http://www.torrentleech.org/torrent/513575"); - torrentInfo.CommentUrl.Should().Be("http://www.torrentleech.org/torrent/513575#comments"); - torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2014/05/12 19:15:28")); - torrentInfo.Size.Should().Be(0); - torrentInfo.InfoHash.Should().Be(null); - torrentInfo.MagnetUrl.Should().Be(null); - torrentInfo.Peers.Should().Be(7+1); - torrentInfo.Seeders.Should().Be(1); - } - } -} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 77b36ba5e..70e548681 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -237,8 +237,6 @@ - - @@ -251,7 +249,6 @@ - @@ -259,7 +256,6 @@ - diff --git a/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs index fca9cdaa2..867cfc620 100644 --- a/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Core.Test/Properties/AssemblyInfo.cs @@ -25,6 +25,4 @@ using System.Runtime.InteropServices; [assembly: Guid("699aed1b-015e-4f0d-9c81-d5557b05d260")] -[assembly: AssemblyVersion("10.0.0.*")] - [assembly: InternalsVisibleTo("NzbDrone.Core")] \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index ef29fe797..5a7bf569b 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.UpdateTests Mocker.GetMock().Setup(c => c.Verify(It.IsAny(), It.IsAny())).Returns(true); Mocker.GetMock().Setup(c => c.GetCurrentProcess()).Returns(new ProcessInfo { Id = 12 }); - Mocker.GetMock().Setup(c => c.ExecutingApplication).Returns(@"C:\Test\NzbDrone.exe"); + Mocker.GetMock().Setup(c => c.ExecutingApplication).Returns(@"C:\Test\Radarr.exe"); Mocker.GetMock() .SetupGet(s => s.UpdateAutomatically) diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 8cc89d87b..39130703f 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Backup private string _backupTempFolder; - private static readonly Regex BackupFileRegex = new Regex(@"nzbdrone_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex BackupFileRegex = new Regex(@"radarr_backup_[._0-9]+\.zip", RegexOptions.Compiled | RegexOptions.IgnoreCase); public BackupService(IMainDatabase maindDb, IDiskTransferService diskTransferService, @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Backup _archiveService = archiveService; _logger = logger; - _backupTempFolder = Path.Combine(_appFolderInfo.TempFolder, "nzbdrone_backup"); + _backupTempFolder = Path.Combine(_appFolderInfo.TempFolder, "radarr_backup"); } public void Backup(BackupType backupType) @@ -59,7 +59,7 @@ namespace NzbDrone.Core.Backup _diskProvider.EnsureFolder(_backupTempFolder); _diskProvider.EnsureFolder(GetBackupFolder(backupType)); - var backupFilename = string.Format("nzbdrone_backup_{0:yyyy.MM.dd_HH.mm.ss}.zip", DateTime.Now); + var backupFilename = string.Format("radarr_backup_{0:yyyy.MM.dd_HH.mm.ss}.zip", DateTime.Now); var backupPath = Path.Combine(GetBackupFolder(backupType), backupFilename); Cleanup(); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index fa6d8a914..4b7543d5f 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -133,7 +133,7 @@ namespace NzbDrone.Core.Configuration } } - public int Port => GetValueInt("Port", 8989); + public int Port => GetValueInt("Port", 7878); public int SslPort => GetValueInt("SslPort", 9898); @@ -161,7 +161,8 @@ namespace NzbDrone.Core.Configuration public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false); - public string Branch => GetValue("Branch", "master").ToLowerInvariant(); + // TODO: Change back to "master" for the first stable release. + public string Branch => GetValue("Branch", "develop").ToLowerInvariant(); public string LogLevel => GetValue("LogLevel", "Info"); @@ -303,12 +304,12 @@ namespace NzbDrone.Core.Configuration if (contents.IsNullOrWhiteSpace()) { - throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Sonarr will recreate it."); + throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Radarr will recreate it."); } if (contents.All(char.IsControl)) { - throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it."); + throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Radarr will recreate it."); } return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); @@ -323,7 +324,7 @@ namespace NzbDrone.Core.Configuration catch (XmlException ex) { - throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Sonarr will recreate it.", ex); + throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Radarr will recreate it.", ex); } } diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index b2792fe56..0042de064 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -41,6 +41,32 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("FirstAired").AsDateTime().Nullable() .WithColumn("NextAiring").AsDateTime().Nullable(); + Create.TableForModel("Movies") + .WithColumn("ImdbId").AsString().Unique() + .WithColumn("Title").AsString() + .WithColumn("TitleSlug").AsString().Unique() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("CleanTitle").AsString() + .WithColumn("Status").AsInt32() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString() + .WithColumn("Monitored").AsBoolean() + .WithColumn("ProfileId").AsInt32() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("Runtime").AsInt32() + .WithColumn("InCinemas").AsDateTime().Nullable() + .WithColumn("Year").AsInt32().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("Actors").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Certification").AsString().Nullable() + .WithColumn("AddOptions").AsString().Nullable(); + + Create.TableForModel("Seasons") .WithColumn("SeriesId").AsInt32() .WithColumn("SeasonNumber").AsInt32() @@ -79,7 +105,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Quality").AsString() .WithColumn("Indexer").AsString() .WithColumn("NzbInfoUrl").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable(); + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("MovieId").AsInt32().WithDefaultValue(0); Create.TableForModel("Notifications") .WithColumn("Name").AsString() diff --git a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs index e665c14a4..5535a1bd9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs +++ b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs @@ -21,11 +21,12 @@ namespace NzbDrone.Core.Datastore.Migration //Add HeldReleases Create.TableForModel("PendingReleases") - .WithColumn("SeriesId").AsInt32() + .WithColumn("SeriesId").AsInt32().WithDefaultValue(0) .WithColumn("Title").AsString() .WithColumn("Added").AsDateTime() .WithColumn("ParsedEpisodeInfo").AsString() - .WithColumn("Release").AsString(); + .WithColumn("Release").AsString() + .WithColumn("MovieId").AsInt32().WithDefaultValue(0); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs b/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs new file mode 100644 index 000000000..34a455683 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs @@ -0,0 +1,31 @@ +using FluentMigrator; +using Marr.Data.Mapping; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Datastore.Extensions; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(104)] + public class add_moviefiles_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("MovieFiles") + .WithColumn("MovieId").AsInt32() + .WithColumn("Path").AsString().Unique() + .WithColumn("Quality").AsString() + .WithColumn("Size").AsInt64() + .WithColumn("DateAdded").AsDateTime() + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("RelativePath").AsString().Nullable(); + + Alter.Table("Movies").AddColumn("MovieFileId").AsInt32().WithDefaultValue(0); + + + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/105_fix_history_movieId.cs b/src/NzbDrone.Core/Datastore/Migration/105_fix_history_movieId.cs new file mode 100644 index 000000000..5de372a15 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/105_fix_history_movieId.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(105)] + public class fix_history_movieId : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("History") + .AddColumn("MovieId").AsInt32().WithDefaultValue(0); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/106_add_tmdb_stuff.cs b/src/NzbDrone.Core/Datastore/Migration/106_add_tmdb_stuff.cs new file mode 100644 index 000000000..106dcdbd1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/106_add_tmdb_stuff.cs @@ -0,0 +1,21 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(106)] + public class add_tmdb_stuff : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Movies") + .AddColumn("TmdbId").AsInt32().WithDefaultValue(0); + Alter.Table("Movies") + .AddColumn("Website").AsString().Nullable(); + Alter.Table("Movies") + .AlterColumn("ImdbId").AsString().Nullable(); + Alter.Table("Movies") + .AddColumn("AlternativeTitles").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/107_fix_movie_files.cs b/src/NzbDrone.Core/Datastore/Migration/107_fix_movie_files.cs new file mode 100644 index 000000000..d1b82b862 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/107_fix_movie_files.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(107)] + public class fix_movie_files : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("MovieFiles").AlterColumn("Path").AsString().Nullable(); //Should be deleted, but to much work, ¯\_(ツ)_/¯ + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/108_update_schedule_interval.cs b/src/NzbDrone.Core/Datastore/Migration/108_update_schedule_interval.cs new file mode 100644 index 000000000..82f204b3e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/108_update_schedule_interval.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(108)] + public class update_schedule_intervale : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("ScheduledTasks").AlterColumn("Interval").AsDouble(); + Execute.Sql("UPDATE ScheduledTasks SET Interval=0.25 WHERE TypeName='NzbDrone.Core.Download.CheckForFinishedDownloadCommand'"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/109_add_movie_formats_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/109_add_movie_formats_to_naming_config.cs new file mode 100644 index 000000000..c36d3f094 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/109_add_movie_formats_to_naming_config.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(109)] + public class add_movie_formats_to_naming_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("NamingConfig").AddColumn("StandardMovieFormat").AsString().Nullable(); + Alter.Table("NamingConfig").AddColumn("MovieFolderFormat").AsString().Nullable(); + + Execute.WithConnection(ConvertConfig); + } + + private void ConvertConfig(IDbConnection conn, IDbTransaction tran) + { + + using (IDbCommand namingConfigCmd = conn.CreateCommand()) + { + namingConfigCmd.Transaction = tran; + namingConfigCmd.CommandText = @"SELECT * FROM NamingConfig LIMIT 1"; + using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) + { + + while (namingConfigReader.Read()) + { + // Output Settings + var movieTitlePattern = ""; + var movieYearPattern = "({Release Year})"; + var qualityFormat = "[{Quality Title}]"; + + movieTitlePattern = "{Movie Title}"; + + var standardMovieFormat = string.Format("{0} {1} {2}", movieTitlePattern, + movieYearPattern, + qualityFormat); + + var movieFolderFormat = string.Format("{0} {1}", movieTitlePattern, movieYearPattern); + + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = string.Format("UPDATE NamingConfig " + + "SET StandardMovieFormat = '{0}', " + + "MovieFolderFormat = '{1}'", + standardMovieFormat, + movieFolderFormat); + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/110_add_physical_release_to_table.cs b/src/NzbDrone.Core/Datastore/Migration/110_add_physical_release_to_table.cs new file mode 100644 index 000000000..945fde4ad --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/110_add_physical_release_to_table.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(110)] + public class add_phyiscal_release : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Movies").AddColumn("PhysicalRelease").AsDateTime().Nullable(); + + } + + + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/111_remove_bitmetv.cs b/src/NzbDrone.Core/Datastore/Migration/111_remove_bitmetv.cs new file mode 100644 index 000000000..c31652530 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/111_remove_bitmetv.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(111)] + public class remove_bitmetv : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.FromTable("Indexers").Row(new { Implementation = "BitMeTv" }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/112_remove_torrentleech.cs b/src/NzbDrone.Core/Datastore/Migration/112_remove_torrentleech.cs new file mode 100644 index 000000000..efaef09c7 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/112_remove_torrentleech.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(112)] + public class remove_torrentleech : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.FromTable("Indexers").Row(new { Implementation = "Torrentleech" }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/113_remove_broadcasthenet.cs b/src/NzbDrone.Core/Datastore/Migration/113_remove_broadcasthenet.cs new file mode 100644 index 000000000..e290283c6 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/113_remove_broadcasthenet.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(113)] + public class remove_broadcasthenet : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.FromTable("Indexers").Row(new { Implementation = "BroadcastheNet" }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/114_remove_fanzub.cs b/src/NzbDrone.Core/Datastore/Migration/114_remove_fanzub.cs new file mode 100644 index 000000000..2963389b5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/114_remove_fanzub.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(114)] + public class remove_fanzub : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.FromTable("Indexers").Row(new { Implementation = "Fanzub" }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/115_update_movie_sorttitle.cs b/src/NzbDrone.Core/Datastore/Migration/115_update_movie_sorttitle.cs new file mode 100644 index 000000000..593665455 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/115_update_movie_sorttitle.cs @@ -0,0 +1,45 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(115)] + public class update_movie_sorttitle : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + // Create.Column("SortTitle").OnTable("Series").AsString().Nullable(); + Execute.WithConnection(SetSortTitles); + } + + private void SetSortTitles(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, Title FROM Movies"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var id = seriesReader.GetInt32(0); + var title = seriesReader.GetString(1); + + var sortTitle = Parser.Parser.NormalizeTitle(title).ToLower(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE Movies SET SortTitle = ? WHERE Id = ?"; + updateCmd.AddParameter(sortTitle); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/116_update_movie_sorttitle_again.cs b/src/NzbDrone.Core/Datastore/Migration/116_update_movie_sorttitle_again.cs new file mode 100644 index 000000000..45666b1c2 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/116_update_movie_sorttitle_again.cs @@ -0,0 +1,44 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(116)] + public class update_movie_sorttitle_again : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(SetSortTitles); + } + + private void SetSortTitles(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, Title FROM Movies"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var id = seriesReader.GetInt32(0); + var title = seriesReader.GetString(1); + + var sortTitle = Parser.Parser.NormalizeTitle(title).ToLower(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE Movies SET SortTitle = ? WHERE Id = ?"; + updateCmd.AddParameter(sortTitle); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs b/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs new file mode 100644 index 000000000..ee1db828d --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/117_update_movie_file.cs @@ -0,0 +1,52 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(117)] + public class update_movie_file : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.Column("Edition").OnTable("MovieFiles").AsString().Nullable(); + //Execute.WithConnection(SetSortTitles); + } + + private void SetSortTitles(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, RelativePath FROM MovieFiles"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var id = seriesReader.GetInt32(0); + var relativePath = seriesReader.GetString(1); + + var result = Parser.Parser.ParseMovieTitle(relativePath); + + var edition = ""; + + if (result != null) + { + edition = Parser.Parser.ParseMovieTitle(relativePath).Edition; + } + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE MovieFiles SET Edition = ? WHERE Id = ?"; + updateCmd.AddParameter(edition); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/118_update_movie_slug.cs b/src/NzbDrone.Core/Datastore/Migration/118_update_movie_slug.cs new file mode 100644 index 000000000..f7026ae5d --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/118_update_movie_slug.cs @@ -0,0 +1,71 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Text; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(118)] + public class update_movie_slug : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(SetTitleSlug); + } + + private void SetTitleSlug(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, Title, Year FROM Movies"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var id = seriesReader.GetInt32(0); + var title = seriesReader.GetString(1); + var year = seriesReader.GetInt32(2); + + var titleSlug = ToUrlSlug(title + "-" + year); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE Movies SET TitleSlug = ? WHERE Id = ?"; + updateCmd.AddParameter(titleSlug); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + + public static string ToUrlSlug(string value) + { + //First to lower case + value = value.ToLowerInvariant(); + + //Remove all accents + var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value); + value = Encoding.ASCII.GetString(bytes); + + //Replace spaces + value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled); + + //Remove invalid chars + value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled); + + //Trim dashes from end + value = value.Trim('-', '_'); + + //Replace double occurences of - or _ + value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled); + + return value; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index 793725e9f..310628715 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework sw.Stop(); - _announcer.ElapsedTime(sw.Elapsed); } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 62f6aeb8b..30c0b038f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -76,6 +76,8 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(s => s.Profile, s => s.ProfileId); + + Mapper.Entity().RegisterModel("EpisodeFiles") .Ignore(f => f.Path) .Relationships.AutoMapICollectionOrComplexProperties() @@ -84,6 +86,21 @@ namespace NzbDrone.Core.Datastore query: (db, parent) => db.Query().Where(c => c.EpisodeFileId == parent.Id).ToList()) .HasOne(file => file.Series, file => file.SeriesId); + Mapper.Entity().RegisterModel("MovieFiles") + .Ignore(f => f.Path) + .Relationships.AutoMapICollectionOrComplexProperties() + .For("Movie") + .LazyLoad(condition: parent => parent.Id > 0, + query: (db, parent) => db.Query().Where(c => c.MovieFileId == parent.Id).ToList()) + .HasOne(file => file.Movie, file => file.MovieId); + + Mapper.Entity().RegisterModel("Movies") + .Ignore(s => s.RootFolderPath) + .Relationship() + .HasOne(s => s.Profile, s => s.ProfileId) + .HasOne(m => m.MovieFile, m => m.MovieFileId); + + Mapper.Entity().RegisterModel("Episodes") .Ignore(e => e.SeriesTitle) .Ignore(e => e.Series) @@ -123,6 +140,7 @@ namespace NzbDrone.Core.Datastore RegisterEmbeddedConverter(); RegisterProviderSettingConverter(); + MapRepository.Instance.RegisterTypeConverter(typeof(int), new Int32Converter()); MapRepository.Instance.RegisterTypeConverter(typeof(double), new DoubleConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(DateTime), new UtcConverter()); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs index cad8177cb..68c2efee0 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecision.cs @@ -7,6 +7,10 @@ namespace NzbDrone.Core.DecisionEngine public class DownloadDecision { public RemoteEpisode RemoteEpisode { get; private set; } + + public RemoteMovie RemoteMovie { get; private set; } + + public bool IsForMovie = false; public IEnumerable Rejections { get; private set; } public bool Approved => !Rejections.Any(); @@ -30,6 +34,23 @@ namespace NzbDrone.Core.DecisionEngine public DownloadDecision(RemoteEpisode episode, params Rejection[] rejections) { RemoteEpisode = episode; + RemoteMovie = new RemoteMovie + { + Release = episode.Release, + ParsedEpisodeInfo = episode.ParsedEpisodeInfo + }; + Rejections = rejections.ToList(); + } + + public DownloadDecision(RemoteMovie movie, params Rejection[] rejections) + { + RemoteMovie = movie; + RemoteEpisode = new RemoteEpisode + { + Release = movie.Release, + ParsedEpisodeInfo = movie.ParsedEpisodeInfo + }; + IsForMovie = true; Rejections = rejections.ToList(); } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index 882105a9d..67117ca85 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -24,8 +24,6 @@ namespace NzbDrone.Core.DecisionEngine { CompareQuality, CompareProtocol, - CompareEpisodeCount, - CompareEpisodeNumber, ComparePeersIfTorrent, CompareAgeIfUsenet, CompareSize @@ -56,6 +54,12 @@ namespace NzbDrone.Core.DecisionEngine private int CompareQuality(DownloadDecision x, DownloadDecision y) { + if (x.IsForMovie && y.IsForMovie) + { + return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => remoteEpisode.Movie.Profile.Value.Items.FindIndex(v => v.Quality == remoteEpisode.ParsedMovieInfo.Quality.Quality)), + CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => remoteEpisode.ParsedMovieInfo.Quality.Revision.Real), + CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => remoteEpisode.ParsedMovieInfo.Quality.Revision.Version)); + } return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Series.Profile.Value.Items.FindIndex(v => v.Quality == remoteEpisode.ParsedEpisodeInfo.Quality.Quality)), CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Real), CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.Quality.Revision.Version)); @@ -63,6 +67,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareProtocol(DownloadDecision x, DownloadDecision y) { + var result = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => { var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Series.Tags); @@ -70,13 +75,22 @@ namespace NzbDrone.Core.DecisionEngine return downloadProtocol == delayProfile.PreferredProtocol; }); + if (x.IsForMovie) + { + result = CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => + { + var delayProfile = _delayProfileService.BestForTags(remoteEpisode.Movie.Tags); + var downloadProtocol = remoteEpisode.Release.DownloadProtocol; + return downloadProtocol == delayProfile.PreferredProtocol; + }); + } + return result; } private int CompareEpisodeCount(DownloadDecision x, DownloadDecision y) { - return CompareAll(CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.ParsedEpisodeInfo.FullSeason), - CompareByReverse(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Episodes.Count)); + return 0; } private int CompareEpisodeNumber(DownloadDecision x, DownloadDecision y) @@ -88,20 +102,20 @@ namespace NzbDrone.Core.DecisionEngine { // Different protocols should get caught when checking the preferred protocol, // since we're dealing with the same series in our comparisions - if (x.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent || - y.RemoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent) + if (x.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent || + y.RemoteMovie.Release.DownloadProtocol != DownloadProtocol.Torrent) { return 0; } return CompareAll( - CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => { var seeders = TorrentInfo.GetSeeders(remoteEpisode.Release); return seeders.HasValue && seeders.Value > 0 ? Math.Round(Math.Log10(seeders.Value)) : 0; }), - CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => { var peers = TorrentInfo.GetPeers(remoteEpisode.Release); @@ -117,7 +131,7 @@ namespace NzbDrone.Core.DecisionEngine return 0; } - return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => + return CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => { var ageHours = remoteEpisode.Release.AgeHours; var age = remoteEpisode.Release.Age; @@ -145,7 +159,7 @@ namespace NzbDrone.Core.DecisionEngine { // TODO: Is smaller better? Smaller for usenet could mean no par2 files. - return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Release.Size.Round(200.Megabytes())); + return CompareBy(x.RemoteMovie, y.RemoteMovie, remoteEpisode => remoteEpisode.Release.Size.Round(200.Megabytes())); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index d86653478..ec7513482 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -37,9 +37,92 @@ namespace NzbDrone.Core.DecisionEngine public List GetSearchDecision(List reports, SearchCriteriaBase searchCriteriaBase) { + if (searchCriteriaBase.Movie != null) + { + return GetMovieDecisions(reports, searchCriteriaBase).ToList(); + } + return GetDecisions(reports, searchCriteriaBase).ToList(); } + private IEnumerable GetMovieDecisions(List reports, SearchCriteriaBase searchCriteria = null) + { + if (reports.Any()) + { + _logger.ProgressInfo("Processing {0} releases", reports.Count); + } + + else + { + _logger.ProgressInfo("No results found"); + } + + var reportNumber = 1; + + foreach (var report in reports) + { + DownloadDecision decision = null; + _logger.ProgressTrace("Processing release {0}/{1}", reportNumber, reports.Count); + + try + { + var parsedEpisodeInfo = Parser.Parser.ParseMovieTitle(report.Title); + + if (parsedEpisodeInfo != null && !parsedEpisodeInfo.MovieTitle.IsNullOrWhiteSpace()) + { + RemoteMovie remoteEpisode = _parsingService.Map(parsedEpisodeInfo, "", searchCriteria); + remoteEpisode.Release = report; + + if (remoteEpisode.Movie == null) + { + //remoteEpisode.DownloadAllowed = true; //Fuck you :) + //decision = GetDecisionForReport(remoteEpisode, searchCriteria); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Movie not Found.")); + } + else + { + if (parsedEpisodeInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace()) + { + remoteEpisode.DownloadAllowed = false; + decision = new DownloadDecision(remoteEpisode, new Rejection("Hardcoded subs found: " + parsedEpisodeInfo.Quality.HardcodedSubs)); + } + else + { + remoteEpisode.DownloadAllowed = true; + decision = GetDecisionForReport(remoteEpisode, searchCriteria); + //decision = new DownloadDecision(remoteEpisode); + } + + } + } + } + catch (Exception e) + { + _logger.Error(e, "Couldn't process release."); + + var remoteEpisode = new RemoteEpisode { Release = report }; + decision = new DownloadDecision(remoteEpisode, new Rejection("Unexpected error processing release")); + } + + reportNumber++; + + if (decision != null) + { + if (decision.Rejections.Any()) + { + _logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); + } + + else + { + _logger.Debug("Release accepted"); + } + + yield return decision; + } + } + } + private IEnumerable GetDecisions(List reports, SearchCriteriaBase searchCriteria = null) { if (reports.Any()) @@ -80,7 +163,9 @@ namespace NzbDrone.Core.DecisionEngine if (remoteEpisode.Series == null) { - decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown Series")); + //remoteEpisode.DownloadAllowed = true; //Fuck you :) + //decision = GetDecisionForReport(remoteEpisode, searchCriteria); + decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Series not Found.")); } else if (remoteEpisode.Episodes.Empty()) { @@ -120,6 +205,14 @@ namespace NzbDrone.Core.DecisionEngine } } + private DownloadDecision GetDecisionForReport(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria = null) + { + var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) + .Where(c => c != null); + + return new DownloadDecision(remoteEpisode, reasons.ToArray()); + } + private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) @@ -143,8 +236,35 @@ namespace NzbDrone.Core.DecisionEngine { e.Data.Add("report", remoteEpisode.Release.ToJson()); e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); - _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title); - return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message)); + _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title + ", with spec: " + spec.GetType().Name); + //return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));//TODO UPDATE SPECS! + //return null; + } + + return null; + } + + private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) + { + try + { + var result = spec.IsSatisfiedBy(remoteEpisode, searchCriteriaBase); + + if (!result.Accepted) + { + return new Rejection(result.Reason, spec.Type); + } + } + catch (NotImplementedException e) + { + _logger.Trace("Spec " + spec.GetType().Name + " does not care about movies."); + } + catch (Exception e) + { + e.Data.Add("report", remoteEpisode.Release.ToJson()); + e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); + _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title + ", with spec: " + spec.GetType().Name); + return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));//TODO UPDATE SPECS! } return null; diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 33fc32f5d..9ba7b8ad2 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.DecisionEngine public interface IPrioritizeDownloadDecision { List PrioritizeDecisions(List decisions); + List PrioritizeDecisionsForMovies(List decisions); } public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision @@ -29,5 +30,17 @@ namespace NzbDrone.Core.DecisionEngine .Union(decisions.Where(c => c.RemoteEpisode.Series == null)) .ToList(); } + + public List PrioritizeDecisionsForMovies(List decisions) + { + return decisions.Where(c => c.RemoteMovie.Movie != null) + /*.GroupBy(c => c.RemoteMovie.Movie.Id, (movieId, downloadDecisions) => + { + return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService)); + }) + .SelectMany(c => c)*/ + .Union(decisions.Where(c => c.RemoteMovie.Movie == null)) + .ToList(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs index 199984734..e98a6977f 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs @@ -8,5 +8,7 @@ namespace NzbDrone.Core.DecisionEngine RejectionType Type { get; } Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); + + Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 4ab566d2e..e4d771cae 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -107,5 +107,58 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Item: {0}, meets size constraints.", subject); return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Beginning size check for: {0}", subject); + + var quality = subject.ParsedMovieInfo.Quality.Quality; + + if (subject.Release.Size == 0) + { + _logger.Debug("Release has unknown size, skipping size check."); + return Decision.Accept(); + } + + var qualityDefinition = _qualityDefinitionService.Get(quality); + if (qualityDefinition.MinSize.HasValue) + { + var minSize = qualityDefinition.MinSize.Value.Megabytes(); + + //Multiply maxSize by Series.Runtime + minSize = minSize * subject.Movie.Runtime; + + //If the parsed size is smaller than minSize we don't want it + if (subject.Release.Size < minSize) + { + var runtimeMessage = subject.Movie.Title; + + _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage); + return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); + } + } + if (!qualityDefinition.MaxSize.HasValue || qualityDefinition.MaxSize.Value == 0) + { + _logger.Debug("Max size is unlimited - skipping check."); + } + else + { + var maxSize = qualityDefinition.MaxSize.Value.Megabytes(); + + //Multiply maxSize by Series.Runtime + maxSize = maxSize * subject.Movie.Runtime; + + //If the parsed size is greater than maxSize we don't want it + if (subject.Release.Size > maxSize) + {; + + _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting.", subject, subject.Release.Size, maxSize, subject.Movie.Title); + return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), subject.Movie.Title); + } + } + + _logger.Debug("Item: {0}, meets size constraints.", subject); + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs index c2f93f7c0..33f6cff9f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -55,5 +56,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 18b216263..82bc2d83c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -2,6 +2,7 @@ using NLog; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using System; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -28,5 +29,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + + throw new NotImplementedException(); + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 6dfdbc64c..1ac882632 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -34,5 +35,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Movie.MovieFile.Value != null) + { + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Existing file meets cutoff: {0}", subject.Movie.Profile.Value.Cutoff); + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index 023b6be60..7d2f89d26 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -30,11 +30,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (subject.Episodes.Any(e => !e.AirDateUtc.HasValue || e.AirDateUtc.Value.After(DateTime.UtcNow))) { _logger.Debug("Full season release {0} rejected. All episodes haven't aired yet.", subject.Release.Title); - return Decision.Reject("Full season release rejected. All episodes haven't aired yet."); + //return Decision.Reject("Full season release rejected. All episodes haven't aired yet."); } } return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 9f7f75038..fe244d057 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -29,5 +29,20 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var wantedLanguage = subject.Movie.Profile.Value.Language; + + _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedMovieInfo.Language); + + if (subject.ParsedMovieInfo.Language != wantedLanguage) + { + _logger.Debug("Report Language: {0} rejected because it is not wanted, wanted {1}", subject.ParsedMovieInfo.Language, wantedLanguage); + return Decision.Reject("{0} is wanted, but found {1}", wantedLanguage, subject.ParsedMovieInfo.Language); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs index 449d7be76..bcec8d6cd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -36,6 +36,37 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } + _logger.Debug("Checking if report meets minimum age requirements. {0}", age); + + if (age < minimumAge) + { + _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + } + + _logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", age, minimumAge); + + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) + { + _logger.Debug("Not checking minimum age requirement for non-usenet report"); + return Decision.Accept(); + } + + var age = subject.Release.AgeMinutes; + var minimumAge = _configService.MinimumAge; + + if (minimumAge == 0) + { + _logger.Debug("Minimum age is not set."); + return Decision.Accept(); + } + + _logger.Debug("Checking if report meets minimum age requirements. {0}", age); if (age < minimumAge) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index 02ff7653a..eba2566e2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -25,5 +26,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.Title.ToLower().Contains("sample") && subject.Release.Size < 70.Megabytes()) + { + _logger.Debug("Sample release, rejecting."); + return Decision.Reject("Sample"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs index 008e58812..93846789c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs @@ -38,5 +38,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var delayProfile = _delayProfileService.BestForTags(subject.Movie.Tags); + + if (subject.Release.DownloadProtocol == DownloadProtocol.Usenet && !delayProfile.EnableUsenet) + { + _logger.Debug("[{0}] Usenet is not enabled for this series", subject.Release.Title); + return Decision.Reject("Usenet is not enabled for this series"); + } + + if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent && !delayProfile.EnableTorrent) + { + _logger.Debug("[{0}] Torrent is not enabled for this series", subject.Release.Title); + return Decision.Reject("Torrent is not enabled for this series"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 7913e0e7e..dcba1ef41 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -26,5 +26,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedMovieInfo.Quality); + if (!subject.Movie.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedMovieInfo.Quality.Quality)) + { + _logger.Debug("Quality {0} rejected by Series' quality profile", subject.ParsedMovieInfo.Quality); + return Decision.Reject("{0} is not wanted in profile", subject.ParsedMovieInfo.Quality.Quality); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 6f3ec1bea..08995e4d2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -50,5 +50,32 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var queue = _queueService.GetQueue() + .Select(q => q.RemoteMovie).ToList(); + + var matchingSeries = queue.Where(q => q.Movie.Id == subject.Movie.Id); + + foreach (var remoteEpisode in matchingSeries) + { + _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteEpisode.ParsedMovieInfo.Quality); + + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, remoteEpisode.ParsedMovieInfo.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for release in queue already meets cutoff: {0}", remoteEpisode.ParsedMovieInfo.Quality); + } + + _logger.Debug("Checking if release is higher quality than queued release. Queued quality is: {0}", remoteEpisode.ParsedMovieInfo.Quality); + + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, remoteEpisode.ParsedMovieInfo.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedMovieInfo.Quality); + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index 7f278cb7e..7a11bacfe 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -42,5 +42,27 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release == null || subject.Release.Container.IsNullOrWhiteSpace()) + { + return Decision.Accept(); + } + + if (_dvdContainerTypes.Contains(subject.Release.Container.ToLower())) + { + _logger.Debug("Release contains raw DVD, rejecting."); + return Decision.Reject("Raw DVD release"); + } + + if (_blurayContainerTypes.Contains(subject.Release.Container.ToLower())) + { + _logger.Debug("Release contains raw Bluray, rejecting."); + return Decision.Reject("Raw Bluray release"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 9fb8c13f5..42735995f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -62,6 +62,46 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Checking if release meets restrictions: {0}", subject); + + var title = subject.Release.Title; + var restrictions = _restrictionService.AllForTags(subject.Movie.Tags); + + var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace()); + var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); + + foreach (var r in required) + { + var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + var foundTerms = ContainsAny(requiredTerms, title); + if (foundTerms.Empty()) + { + var terms = string.Join(", ", requiredTerms); + _logger.Debug("[{0}] does not contain one of the required terms: {1}", title, terms); + return Decision.Reject("Does not contain one of the required terms: {0}", terms); + } + } + + foreach (var r in ignored) + { + var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + var foundTerms = ContainsAny(ignoredTerms, title); + if (foundTerms.Any()) + { + var terms = string.Join(", ", foundTerms); + _logger.Debug("[{0}] contains these ignored terms: {1}", title, terms); + return Decision.Reject("Contains these ignored terms: {0}", terms); + } + } + + _logger.Debug("[{0}] No restrictions apply, allowing", subject); + return Decision.Accept(); + } + private static List ContainsAny(List terms, string title) { return terms.Where(t => title.ToLowerInvariant().Contains(t.ToLowerInvariant())).ToList(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index 97802f871..7663c3fb1 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -38,5 +38,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) + { + _logger.Debug("Not checking retention requirement for non-usenet report"); + return Decision.Accept(); + } + + var age = subject.Release.Age; + var retention = _configService.Retention; + + _logger.Debug("Checking if report meets retention requirements. {0}", age); + if (retention > 0 && age > retention) + { + _logger.Debug("Report age: {0} rejected by user's retention limit", age); + return Decision.Reject("Older than configured retention"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 68551c66c..f75ac84af 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -28,6 +28,71 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Temporary; + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null && searchCriteria.UserInvokedSearch) + { + _logger.Debug("Ignoring delay for user invoked search"); + return Decision.Accept(); + } + + var profile = subject.Movie.Profile.Value; + var delayProfile = _delayProfileService.BestForTags(subject.Movie.Tags); + var delay = delayProfile.GetProtocolDelay(subject.Release.DownloadProtocol); + var isPreferredProtocol = subject.Release.DownloadProtocol == delayProfile.PreferredProtocol; + + if (delay == 0) + { + _logger.Debug("Profile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol); + return Decision.Accept(); + } + + var comparer = new QualityModelComparer(profile); + + if (isPreferredProtocol) + { + var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality); + + if (upgradable) + { + var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality); + + if (revisionUpgrade) + { + _logger.Debug("New quality is a better revision for existing quality, skipping delay"); + return Decision.Accept(); + } + } + + } + + // If quality meets or exceeds the best allowed quality in the profile accept it immediately + var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); + var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile) >= 0; + + if (isBestInProfile && isPreferredProtocol) + { + _logger.Debug("Quality is highest in profile for preferred protocol, will not delay"); + return Decision.Accept(); + } + + /* + var oldest = _pendingReleaseService.OldestPendingRelease(subject.Series.Id, episodeIds); + + if (oldest != null && oldest.Release.AgeMinutes > delay) + { + return Decision.Accept(); + } + + if (subject.Release.AgeMinutes < delay) + { + _logger.Debug("Waiting for better quality release, There is a {0} minute delay on {1}", delay, subject.Release.DownloadProtocol); + return Decision.Reject("Waiting for better quality release"); + }*/ //TODO: Update for movies! + + return Decision.Accept(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null && searchCriteria.UserInvokedSearch) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 9aa4fabf1..2a312fbdd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -28,6 +28,56 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Permanent; + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + _logger.Debug("Skipping history check during search"); + return Decision.Accept(); + } + + var cdhEnabled = _configService.EnableCompletedDownloadHandling; + + _logger.Debug("Performing history status check on report"); + _logger.Debug("Checking current status of episode [{0}] in history", subject.Movie.Id); + var mostRecent = _historyService.MostRecentForMovie(subject.Movie.Id); + + if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) + { + var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12)); + var cutoffUnmet = _qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, mostRecent.Quality, subject.ParsedMovieInfo.Quality); + var upgradeable = _qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, mostRecent.Quality, subject.ParsedMovieInfo.Quality); + + if (!recent && cdhEnabled) + { + return Decision.Accept(); + } + + if (!cutoffUnmet) + { + if (recent) + { + return Decision.Reject("Recent grab event in history already meets cutoff: {0}", mostRecent.Quality); + } + + return Decision.Reject("CDH is disabled and grab event in history already meets cutoff: {0}", mostRecent.Quality); + } + + if (!upgradeable) + { + if (recent) + { + return Decision.Reject("Recent grab event in history is of equal or higher quality: {0}", mostRecent.Quality); + } + + return Decision.Reject("CDH is disabled and grab event in history is of equal or higher quality: {0}", mostRecent.Quality); + } + } + + + return Decision.Accept(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs index f56f26478..ccb87c414 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -16,6 +17,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 0c6632d25..d029e929c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -47,6 +47,39 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } } + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + return Decision.Accept(); + } + + if (subject.Movie.MovieFile.Value == null) + { + return Decision.Accept(); + } + + var file = subject.Movie.MovieFile.Value; + + if (_qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedMovieInfo.Quality)) + { + if (file.DateAdded < DateTime.Today.AddDays(-7)) + { + _logger.Debug("Proper for old file, rejecting: {0}", subject); + return Decision.Reject("Proper for old file"); + } + + if (!_configService.AutoDownloadPropers) + { + _logger.Debug("Auto downloading of propers is disabled"); + return Decision.Reject("Proper downloading is disabled"); + } + } + + return Decision.Accept(); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs index 1a8c5db5b..bd3c2f908 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -27,5 +28,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Episode file on disk contains more episodes than this release contains"); return Decision.Reject("Episode file on disk contains more episodes than this release contains"); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs index 50fd9b3cc..285dc5b5e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -39,5 +40,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs index 60640442f..d8c0065a5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -17,6 +18,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index b09d888ec..c7f96303d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -15,6 +16,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) @@ -28,7 +34,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + //return Decision.Reject("Wrong season"); + //Unnecessary for Movies } return Decision.Accept(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs index 7f1201b33..dde54155f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs @@ -32,5 +32,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria) + { + if (searchCriteria == null) + { + return Decision.Accept(); + } + + _logger.Debug("Checking if movie matches searched movie"); + + if (remoteEpisode.Movie.Id != searchCriteria.Movie.Id) + { + _logger.Debug("Series {0} does not match {1}", remoteEpisode.Movie, searchCriteria.Series); + return Decision.Reject("Wrong movie"); + } + + return Decision.Accept(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs index fb056734f..18162d3f5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -16,6 +17,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) @@ -29,19 +35,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) { _logger.Debug("Season number does not match searched season number, skipping."); - return Decision.Reject("Wrong season"); + //return Decision.Reject("Wrong season"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Any()) { _logger.Debug("Full season result during single episode search, skipping."); - return Decision.Reject("Full season pack"); + //return Decision.Reject("Full season pack"); } if (!remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers.Contains(singleEpisodeSpec.EpisodeNumber)) { _logger.Debug("Episode number does not match searched episode number, skipping."); - return Decision.Reject("Wrong episode"); + //return Decision.Reject("Wrong episode"); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs index 87c244b53..142e2009a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs @@ -33,5 +33,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria) + { + var torrentInfo = remoteEpisode.Release as TorrentInfo; + + if (torrentInfo == null) + { + return Decision.Accept(); + } + + if (torrentInfo.Seeders != null && torrentInfo.Seeders < 1) + { + _logger.Debug("Not enough seeders. ({0})", torrentInfo.Seeders); + return Decision.Reject("Not enough seeders. ({0})", torrentInfo.Seeders); + } + + return Decision.Accept(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 5a24b6305..e30d0fc92 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -30,6 +30,25 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Movie.MovieFile.Value == null) + { + return Decision.Accept(); + } + + var file = subject.Movie.MovieFile.Value; + _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); + + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, file.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for existing file on disk is of equal or higher preference: {0}", file.Quality); + } + + return Decision.Accept(); } } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs index d05ee7f22..a716a3b8d 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/TorrentBlackholeSettings.cs @@ -26,10 +26,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Sonarr will store the .torrent file")] + [FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Radarr will store the .torrent file")] public string TorrentFolder { get; set; } - [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Sonarr should import completed downloads")] + [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Radarr should import completed downloads")] public string WatchFolder { get; set; } [DefaultValue(false)] @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [FieldDefinition(3, Label = "Read Only", Type = FieldType.Checkbox, HelpText = "Instead of moving files this will instruct Sonarr to Copy or Hardlink (depending on settings/system configuration)")] + [FieldDefinition(3, Label = "Read Only", Type = FieldType.Checkbox, HelpText = "Instead of moving files this will instruct Radarr to Copy or Hardlink (depending on settings/system configuration)")] public bool ReadOnly { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 2cc13a235..202e008c0 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole ScanGracePeriod = TimeSpan.FromSeconds(30); } - protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents) { var title = remoteEpisode.Release.Title; @@ -42,7 +42,25 @@ namespace NzbDrone.Core.Download.Clients.Blackhole using (var stream = _diskProvider.OpenWriteStream(filepath)) { - stream.Write(fileContent, 0, fileContent.Length); + stream.Write(fileContents, 0, fileContents.Length); + } + + _logger.Debug("NZB Download succeeded, saved to: {0}", filepath); + + return null; + } + + protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + var title = remoteMovie.Release.Title; + + title = FileNameBuilder.CleanFileName(title); + + var filepath = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + using (var stream = _diskProvider.OpenWriteStream(filepath)) + { + stream.Write(fileContents, 0, fileContents.Length); } _logger.Debug("NZB Download succeeded, saved to: {0}", filepath); @@ -60,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { DownloadClient = Definition.Name, DownloadId = Definition.Name + "_" + item.DownloadId, - Category = "sonarr", + Category = "Radarr", Title = item.Title, TotalSize = item.TotalSize, diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs index b2ff88149..59a8e6a0f 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackholeSettings.cs @@ -19,10 +19,10 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); - [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Sonarr will store the .nzb file")] + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Radarr will store the .nzb file")] public string NzbFolder { get; set; } - [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Sonarr should import completed downloads")] + [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path, HelpText = "Folder from which Radarr should import completed downloads")] public string WatchFolder { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 39174d3b8..831c31b4f 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -31,50 +31,44 @@ namespace NzbDrone.Core.Download.Clients.Deluge _proxy = proxy; } - protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) { var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings); - if (!Settings.TvCategory.IsNullOrWhiteSpace()) + if (!Settings.MovieCategory.IsNullOrWhiteSpace()) { - _proxy.SetLabel(actualHash, Settings.TvCategory, Settings); + _proxy.SetLabel(actualHash, Settings.MovieCategory, Settings); } _proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings); - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First) - { - _proxy.MoveTorrentToTopInQueue(actualHash, Settings); - } - return actualHash.ToUpper(); } - protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, byte[] fileContent) { var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings); - if (!Settings.TvCategory.IsNullOrWhiteSpace()) + if (!Settings.MovieCategory.IsNullOrWhiteSpace()) { - _proxy.SetLabel(actualHash, Settings.TvCategory, Settings); + _proxy.SetLabel(actualHash, Settings.MovieCategory, Settings); } _proxy.SetTorrentConfiguration(actualHash, "remove_at_ratio", false, Settings); - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)DelugePriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)DelugePriority.First) - { - _proxy.MoveTorrentToTopInQueue(actualHash, Settings); - } - return actualHash.ToUpper(); } + protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + public override string Name => "Deluge"; public override IEnumerable GetItems() @@ -83,9 +77,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge try { - if (!Settings.TvCategory.IsNullOrWhiteSpace()) + if (!Settings.MovieCategory.IsNullOrWhiteSpace()) { - torrents = _proxy.GetTorrentsByLabel(Settings.TvCategory, Settings); + torrents = _proxy.GetTorrentsByLabel(Settings.MovieCategory, Settings); } else { @@ -105,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge var item = new DownloadClientItem(); item.DownloadId = torrent.Hash.ToUpper(); item.Title = torrent.Name; - item.Category = Settings.TvCategory; + item.Category = Settings.MovieCategory; item.DownloadClient = Definition.Name; @@ -236,7 +230,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge private ValidationFailure TestCategory() { - if (Settings.TvCategory.IsNullOrWhiteSpace()) + if (Settings.MovieCategory.IsNullOrWhiteSpace()) { return null; } @@ -253,16 +247,16 @@ namespace NzbDrone.Core.Download.Clients.Deluge var labels = _proxy.GetAvailableLabels(Settings); - if (!labels.Contains(Settings.TvCategory)) + if (!labels.Contains(Settings.MovieCategory)) { - _proxy.AddLabel(Settings.TvCategory, Settings); + _proxy.AddLabel(Settings.MovieCategory, Settings); labels = _proxy.GetAvailableLabels(Settings); - if (!labels.Contains(Settings.TvCategory)) + if (!labels.Contains(Settings.MovieCategory)) { - return new NzbDroneValidationFailure("TvCategory", "Configuration of label failed") + return new NzbDroneValidationFailure("MovieCategory", "Configuration of label failed") { - DetailedDescription = "Sonarr as unable to add the label to Deluge." + DetailedDescription = "Radarr as unable to add the label to Deluge." }; } } diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index b5fd1153e..a7175dff6 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.Port).GreaterThan(0); - RuleFor(c => c.TvCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); + RuleFor(c => c.MovieCategory).Matches("^[-a-z]*$").WithMessage("Allowed characters a-z and -"); } } @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge Host = "localhost"; Port = 8112; Password = "deluge"; - TvCategory = "tv-sonarr"; + MovieCategory = "movie-radarr"; } [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] @@ -40,16 +40,10 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] - public string TvCategory { get; set; } + [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] + public string MovieCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] - public int RecentTvPriority { get; set; } - - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] - public int OlderTvPriority { get; set; } - - [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(5, Label = "Use SSL", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs index 6d45f0386..ce29dafad 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -29,11 +29,25 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex _proxy = proxy; } - protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents) { var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; + + var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings); - var response = _proxy.DownloadNzb(fileContent, filename, priority, Settings); + if (response == null) + { + throw new DownloadClientException("Failed to add nzb {0}", filename); + } + + return response; + } + + protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + var priority = Settings.RecentTvPriority; + + var response = _proxy.DownloadNzb(fileContents, filename, priority, Settings); if (response == null) { diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs index 411624c9d..749ef9d04 100644 --- a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex [FieldDefinition(2, Label = "API Key", Type = FieldType.Textbox)] public string ApiKey { get; set; } - [FieldDefinition(3, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(3, Label = "Group", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] public string TvCategory { get; set; } [FieldDefinition(4, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 5b6d756cc..754e67abd 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -29,12 +29,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy = proxy; } - protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents) { var category = Settings.TvCategory; var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; - var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings); + var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); if (response == null) { @@ -44,6 +44,21 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return response; } + protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + var category = Settings.TvCategory; // TODO: Update this to MovieCategory? + var priority = Settings.RecentTvPriority; + + var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); + + if(response == null) + { + throw new DownloadClientException("Failed to add nzb {0}", filename); + } + + return response; + } + private IEnumerable GetQueue() { NzbgetGlobalStatus globalStatus; @@ -72,13 +87,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var queueItem = new DownloadClientItem(); - queueItem.DownloadId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); - queueItem.Title = item.NzbName; - queueItem.TotalSize = totalSize; - queueItem.Category = item.Category; - queueItem.DownloadClient = Definition.Name; - + var queueItem = new DownloadClientItem() + { + DownloadId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(), + Title = item.NzbName, + TotalSize = totalSize, + Category = item.Category, + DownloadClient = Definition.Name + }; if (globalStatus.DownloadPaused || remainingSize == pausedSize && remainingSize != 0) { queueItem.Status = DownloadItemStatus.Paused; @@ -131,17 +147,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var historyItem = new DownloadClientItem(); - historyItem.DownloadClient = Definition.Name; - historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); - historyItem.Title = item.Name; - historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); - historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir)); - historyItem.Category = item.Category; - historyItem.Message = string.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus); - historyItem.Status = DownloadItemStatus.Completed; - historyItem.RemainingTime = TimeSpan.Zero; - + var historyItem = new DownloadClientItem() + { + DownloadClient = Definition.Name, + DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(), + Title = item.Name, + TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo), + OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir)), + Category = item.Category, + Message = string.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus), + Status = DownloadItemStatus.Completed, + RemainingTime = TimeSpan.Zero + }; if (item.DeleteStatus == "MANUAL") { continue; @@ -320,7 +337,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return new NzbDroneValidationFailure(string.Empty, "NzbGet setting KeepHistory should be greater than 0") { InfoLink = string.Format("http://{0}:{1}/", Settings.Host, Settings.Port), - DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Sonarr from seeing completed downloads." + DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Radarr from seeing completed downloads." }; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs index cbd104964..3321c7671 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs @@ -26,7 +26,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { Host = "localhost"; Port = 6789; - TvCategory = "tv"; + TvCategory = "Movies"; + Username = "nzbget"; + Password = "tegbzn6789"; RecentTvPriority = (int)NzbgetPriority.Normal; OlderTvPriority = (int)NzbgetPriority.Normal; } @@ -43,8 +45,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] - public string TvCategory { get; set; } + [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] + public string TvCategory { get; set; } [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbgetPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] public int RecentTvPriority { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 5eab58b3b..299a56b13 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -58,6 +58,32 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic return GetDownloadClientId(strmFile); } + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + if (remoteEpisode.ParsedEpisodeInfo.FullSeason) + { + throw new NotSupportedException("Full season releases are not supported with Pneumatic."); + } + + title = FileNameBuilder.CleanFileName(title); + + //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) + var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); + _httpClient.DownloadFile(url, nzbFile); + + _logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile); + + var strmFile = WriteStrmFile(title, nzbFile); + + + return GetDownloadClientId(strmFile); + } + public bool IsConfigured => !string.IsNullOrWhiteSpace(Settings.NzbFolder); public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index ecd75c911..5db723ff0 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -32,40 +32,34 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) { _proxy.AddTorrentFromUrl(magnetLink, Settings); - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); - } - - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) - { - _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + _proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings); } return hash; } - protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent) + protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, Byte[] fileContent) { _proxy.AddTorrentFromFile(filename, fileContent, Settings); - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); - } - - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) - { - _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + _proxy.SetTorrentLabel(hash.ToLower(), Settings.MovieCategory, Settings); } return hash; @@ -196,7 +190,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent else if (version < 6) { // API version 6 introduced support for labels - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { return new NzbDroneValidationFailure("Category", "Category is not supported") { @@ -204,13 +198,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent }; } } - else if (Settings.TvCategory.IsNullOrWhiteSpace()) + else if (Settings.MovieCategory.IsNullOrWhiteSpace()) { // warn if labels are supported, but category is not provided return new NzbDroneValidationFailure("TvCategory", "Category is recommended") { IsWarning = true, - DetailedDescription = "Sonarr will not attempt to import completed downloads without a category." + DetailedDescription = "Radarr will not attempt to import completed downloads without a category." }; } @@ -220,7 +214,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit") { - DetailedDescription = "Sonarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." + DetailedDescription = "Radarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." }; } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs index e00c57585..be4ca16c5 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxy.cs @@ -58,8 +58,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public List GetTorrents(QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/query/torrents") - .AddQueryParam("label", settings.TvCategory) - .AddQueryParam("category", settings.TvCategory); + .AddQueryParam("label", settings.MovieCategory) + .AddQueryParam("category", settings.MovieCategory); var response = ProcessRequest>(request, settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index fa6908f2c..c02619f2f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { Host = "localhost"; Port = 9091; - TvCategory = "tv-sonarr"; + MovieCategory = "movie-radarr"; } [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] @@ -37,16 +37,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] - public string TvCategory { get; set; } + [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] + public string MovieCategory { get; set; } - [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] - public int RecentTvPriority { get; set; } - - [FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] - public int OlderTvPriority { get; set; } - - [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] + [FieldDefinition(5, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 64a5e23de..52fef65b0 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -30,14 +30,29 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } // patch can be a number (releases) or 'x' (git) - private static readonly Regex VersionRegex = new Regex(@"(?\d+)\.(?\d+)\.(?\d+|x)(?.*)", RegexOptions.Compiled); + private static readonly Regex VersionRegex = new Regex(@"(?\d+)\.(?\d+)\.(?\d+|x)", RegexOptions.Compiled); - protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents) { var category = Settings.TvCategory; var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; - var response = _proxy.DownloadNzb(fileContent, filename, category, priority, Settings); + var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); + + if (response != null && response.Ids.Any()) + { + return response.Ids.First(); + } + + return null; + } + + protected override string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + var category = Settings.TvCategory; + var priority = Settings.RecentTvPriority; + + var response = _proxy.DownloadNzb(fileContents, filename, category, priority, Settings); if (response != null && response.Ids.Any()) { @@ -269,110 +284,103 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd failures.AddIfNotNull(TestCategory()); } - private bool HasVersion(int major, int minor, int patch = 0, string candidate = null) + private bool HasVersion(int major, int minor, int patch = 0) { - candidate = candidate ?? string.Empty; + var rawVersion = _proxy.GetVersion(Settings); + var version = ParseVersion(rawVersion); - var version = _proxy.GetVersion(Settings); + if (version == null) + { + return false; + } + + if (version.Major > major) + { + return true; + } + else if (version.Major < major) + { + return false; + } + + if (version.Minor > minor) + { + return true; + } + else if (version.Minor < minor) + { + return false; + } + + if (version.Build > patch) + { + return true; + } + else if (version.Build < patch) + { + return false; + } + + return true; + } + + private Version ParseVersion(string version) + { var parsed = VersionRegex.Match(version); - int actualMajor; - int actualMinor; - int actualPatch; - string actualCandidate; + int major; + int minor; + int patch; - if (!parsed.Success) + if (parsed.Success) + { + major = Convert.ToInt32(parsed.Groups["major"].Value); + minor = Convert.ToInt32(parsed.Groups["minor"].Value); + patch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", "0")); + } + + else { if (!version.Equals("develop", StringComparison.InvariantCultureIgnoreCase)) { - return false; + return null; } - actualMajor = 1; - actualMinor = 1; - actualPatch = 0; - actualCandidate = null; + major = 1; + minor = 1; + patch = 0; } - else - { - actualMajor = Convert.ToInt32(parsed.Groups["major"].Value); - actualMinor = Convert.ToInt32(parsed.Groups["minor"].Value); - actualPatch = Convert.ToInt32(parsed.Groups["patch"].Value.Replace("x", "")); - actualCandidate = parsed.Groups["candidate"].Value.ToUpper(); - } - - if (actualMajor > major) - { - return true; - } - else if (actualMajor < major) - { - return false; - } - - if (actualMinor > minor) - { - return true; - } - else if (actualMinor < minor) - { - return false; - } - - if (actualPatch > patch) - { - return true; - } - else if (actualPatch < patch) - { - return false; - } - - if (actualCandidate.IsNullOrWhiteSpace()) - { - return true; - } - else if (candidate.IsNullOrWhiteSpace()) - { - return false; - } - else - { - return actualCandidate.CompareTo(candidate) > 0; - } + return new Version(major, minor, patch); } private ValidationFailure TestConnectionAndVersion() { try { - var version = _proxy.GetVersion(Settings); - var parsed = VersionRegex.Match(version); + var rawVersion = _proxy.GetVersion(Settings); + var version = ParseVersion(rawVersion); - if (!parsed.Success) + if (version == null) { - if (version.Equals("develop", StringComparison.InvariantCultureIgnoreCase)) - { - return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 1.1.0 or higher.") - { - IsWarning = true, - DetailedDescription = "Sonarr may not be able to support new features added to SABnzbd when running develop versions." - }; - } - return new ValidationFailure("Version", "Unknown Version: " + version); } - var major = Convert.ToInt32(parsed.Groups["major"].Value); - var minor = Convert.ToInt32(parsed.Groups["minor"].Value); + if (rawVersion.Equals("develop", StringComparison.InvariantCultureIgnoreCase)) + { + return new NzbDroneValidationFailure("Version", "Sabnzbd develop version, assuming version 1.1.0 or higher.") + { + IsWarning = true, + DetailedDescription = "Radarr may not be able to support new features added to SABnzbd when running develop versions." + }; + } - if (major >= 1) + if (version.Major >= 1) { return null; } - if (minor >= 7) + if (version.Minor >= 7) { return null; } @@ -416,7 +424,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return new NzbDroneValidationFailure("", "Disable 'Check before download' option in Sabnbzd") { InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/switches/", Settings.Host, Settings.Port), - DetailedDescription = "Using Check before download affects Sonarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective." + DetailedDescription = "Using Check before download affects Radarr ability to track new downloads. Also Sabnzbd recommends 'Abort jobs that cannot be completed' instead since it's more effective." }; } @@ -435,7 +443,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return new NzbDroneValidationFailure("TvCategory", "Enable Job folders") { InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/categories/", Settings.Host, Settings.Port), - DetailedDescription = "Sonarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it." + DetailedDescription = "Radarr prefers each download to have a separate folder. With * appended to the Folder/Path Sabnzbd will not create these job folders. Go to Sabnzbd to fix it." }; } } @@ -460,7 +468,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return new NzbDroneValidationFailure("TvCategory", "Disable TV Sorting") { InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port), - DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = "You must disable Sabnzbd TV Sorting for the category Radarr uses to prevent import issues. Go to Sabnzbd to fix it." }; } } @@ -474,7 +482,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return new NzbDroneValidationFailure("TvCategory", "Disable Movie Sorting") { InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port), - DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = "You must disable Sabnzbd Movie Sorting for the category Radarr uses to prevent import issues. Go to Sabnzbd to fix it." }; } } @@ -488,7 +496,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return new NzbDroneValidationFailure("TvCategory", "Disable Date Sorting") { InfoLink = string.Format("http://{0}:{1}/sabnzbd/config/sorting/", Settings.Host, Settings.Port), - DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Sonarr uses to prevent import issues. Go to Sabnzbd to fix it." + DetailedDescription = "You must disable Sabnzbd Date Sorting for the category Radarr uses to prevent import issues. Go to Sabnzbd to fix it." }; } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs index 22a3389bf..0c1dc8221 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdSettings.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [FieldDefinition(4, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] public string TvCategory { get; set; } [FieldDefinition(6, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index 8358e9e79..1eb6b6ce8 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -54,21 +54,21 @@ namespace NzbDrone.Core.Download.Clients.Transmission var outputPath = new OsPath(torrent.DownloadDir); - if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) + if (Settings.MovieDirectory.IsNotNullOrWhiteSpace()) { - if (!new OsPath(Settings.TvDirectory).Contains(outputPath)) continue; + if (!new OsPath(Settings.MovieDirectory).Contains(outputPath)) continue; } - else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + else if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { var directories = outputPath.FullPath.Split('\\', '/'); - if (!directories.Contains(Settings.TvCategory)) continue; + if (!directories.Contains(Settings.MovieCategory)) continue; } outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath); var item = new DownloadClientItem(); item.DownloadId = torrent.HashString.ToUpper(); - item.Category = Settings.TvCategory; + item.Category = Settings.MovieCategory; item.Title = torrent.Name; item.DownloadClient = Definition.Name; @@ -123,9 +123,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission var config = _proxy.GetConfig(Settings); var destDir = config.GetValueOrDefault("download-dir") as string; - if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { - destDir = string.Format("{0}/.{1}", destDir, Settings.TvCategory); + destDir = string.Format("{0}/.{1}", destDir, Settings.MovieCategory); } return new DownloadClientStatus @@ -137,31 +137,23 @@ namespace NzbDrone.Core.Download.Clients.Transmission protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) { - _proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings); - - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First) - { - _proxy.MoveTorrentToTopInQueue(hash, Settings); - } - - return hash; + throw new NotImplementedException("Episodes are not working with Radarr"); } protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings); + return hash; + } + + protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) { _proxy.AddTorrentFromData(fileContent, GetDownloadDirectory(), Settings); - - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - - if (isRecentEpisode && Settings.RecentTvPriority == (int)TransmissionPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)TransmissionPriority.First) - { - _proxy.MoveTorrentToTopInQueue(hash, Settings); - } - return hash; } @@ -179,16 +171,16 @@ namespace NzbDrone.Core.Download.Clients.Transmission protected string GetDownloadDirectory() { - if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) + if (Settings.MovieDirectory.IsNotNullOrWhiteSpace()) { - return Settings.TvDirectory; + return Settings.MovieDirectory; } - else if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + else if (Settings.MovieCategory.IsNotNullOrWhiteSpace()) { var config = _proxy.GetConfig(Settings); var destDir = (string)config.GetValueOrDefault("download-dir"); - return string.Format("{0}/{1}", destDir.TrimEnd('/'), Settings.TvCategory); + return string.Format("{0}/{1}", destDir.TrimEnd('/'), Settings.MovieCategory); } else { @@ -207,7 +199,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Username", "Authentication failure") { - DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name) + DetailedDescription = string.Format("Please verify your username and password. Also verify if the host running Radarr isn't blocked from accessing {0} by WhiteList limitations in the {0} configuration.", Name) }; } catch (WebException ex) diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs index 633ee7a57..9d0a860ec 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionSettings.cs @@ -16,10 +16,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission RuleFor(c => c.UrlBase).ValidUrlBase(); - RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -"); + RuleFor(c => c.MovieCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -"); - RuleFor(c => c.TvCategory).Empty() - .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) + RuleFor(c => c.MovieCategory).Empty() + .When(c => c.MovieDirectory.IsNotNullOrWhiteSpace()) .WithMessage("Cannot use Category and Directory"); } } @@ -50,19 +50,13 @@ namespace NzbDrone.Core.Download.Clients.Transmission [FieldDefinition(4, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] - public string TvCategory { get; set; } + [FieldDefinition(5, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + public string MovieCategory { get; set; } [FieldDefinition(6, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default Transmission location")] - public string TvDirectory { get; set; } + public string MovieDirectory { get; set; } - [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] - public int RecentTvPriority { get; set; } - - [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(TransmissionPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] - public int OlderTvPriority { get; set; } - - [FieldDefinition(9, Label = "Use SSL", Type = FieldType.Checkbox)] + [FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox)] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 041708a93..cc9b80aa5 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -38,19 +38,22 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException("Episodes are not working with Radarr"); + } + + protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) { _proxy.AddTorrentFromUrl(magnetLink, Settings); // Download the magnet to the appropriate directory. - _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings); - SetPriority(remoteEpisode, hash); + _proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings); SetDownloadDirectory(hash); - - // Once the magnet meta download finishes, rTorrent replaces it with the actual torrent download with default settings. - // Schedule an event to apply the appropriate settings when that happens. - var priority = (RTorrentPriority)(remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority); - _proxy.SetDeferredMagnetProperties(hash, Settings.TvCategory, Settings.TvDirectory, priority, Settings); - _proxy.StartTorrent(hash, Settings); // Wait for the magnet to be resolved. @@ -68,7 +71,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent } } - protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) { _proxy.AddTorrentFromFile(filename, fileContent, Settings); @@ -76,13 +79,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent var retryDelay = 100; if (WaitForTorrent(hash, tries, retryDelay)) { - _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings); - - SetPriority(remoteEpisode, hash); + _proxy.SetTorrentLabel(hash, Settings.MovieCategory, Settings); SetDownloadDirectory(hash); - _proxy.StartTorrent(hash, Settings); - return hash; } else @@ -90,13 +89,13 @@ namespace NzbDrone.Core.Download.Clients.RTorrent _logger.Debug("rTorrent could not add file"); RemoveItem(hash, true); - throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed"); + throw new ReleaseDownloadException(remoteMovie.Release, "Downloading torrent failed"); } } public override string Name => "rTorrent"; - public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); + public override ProviderMessage Message => new ProviderMessage("Radarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); public override IEnumerable GetItems() { @@ -110,7 +109,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent foreach (RTorrentTorrent torrent in torrents) { // Don't concern ourselves with categories other than specified - if (torrent.Category != Settings.TvCategory) continue; + if (torrent.Category != Settings.MovieCategory) continue; if (torrent.Path.StartsWith(".")) { @@ -230,17 +229,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent return result.Errors.First(); } - private void SetPriority(RemoteEpisode remoteEpisode, string hash) - { - var priority = (RTorrentPriority)(remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority); - _proxy.SetTorrentPriority(hash, priority, Settings); - } - private void SetDownloadDirectory(string hash) { - if (Settings.TvDirectory.IsNotNullOrWhiteSpace()) + if (Settings.MovieDirectory.IsNotNullOrWhiteSpace()) { - _proxy.SetTorrentDownloadDirectory(hash, Settings.TvDirectory, Settings); + _proxy.SetTorrentDownloadDirectory(hash, Settings.MovieDirectory, Settings); } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs index 3cb2d6a8b..1d0f5063a 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs @@ -18,13 +18,13 @@ namespace NzbDrone.Core.Download.Clients.rTorrent DroneFactoryValidator droneFactoryValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { - RuleFor(c => c.TvDirectory).Cascade(CascadeMode.StopOnFirstFailure) + RuleFor(c => c.MovieDirectory).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(droneFactoryValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(pathExistsValidator) - .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) + .When(c => c.MovieDirectory.IsNotNullOrWhiteSpace()) .When(c => c.Host == "localhost" || c.Host == "127.0.0.1"); } } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs index 81715246c..4965c9a78 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.Port).InclusiveBetween(0, 65535); - RuleFor(c => c.TvCategory).NotEmpty() + RuleFor(c => c.MovieCategory).NotEmpty() .WithMessage("A category is recommended") .AsWarning(); } @@ -26,9 +26,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent Host = "localhost"; Port = 8080; UrlBase = "RPC2"; - TvCategory = "tv-sonarr"; - OlderTvPriority = (int)RTorrentPriority.Normal; - RecentTvPriority = (int)RTorrentPriority.Normal; + MovieCategory = "movies-radarr"; } [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)] @@ -49,17 +47,11 @@ namespace NzbDrone.Core.Download.Clients.RTorrent [FieldDefinition(5, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional.")] - public string TvCategory { get; set; } + [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional.")] + public string MovieCategory { get; set; } [FieldDefinition(7, Label = "Directory", Type = FieldType.Textbox, Advanced = true, HelpText = "Optional location to put downloads in, leave blank to use the default rTorrent location")] - public string TvDirectory { get; set; } - - [FieldDefinition(8, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] - public int RecentTvPriority { get; set; } - - [FieldDefinition(9, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")] - public int OlderTvPriority { get; set; } + public string MovieDirectory { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index da942b7f1..171910744 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -68,6 +68,38 @@ namespace NzbDrone.Core.Download.Clients.UTorrent return hash; } + protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, Settings); + _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings); + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash, Settings); + }*/ + + return hash; + } + + protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, byte[] fileContent) + { + _proxy.AddTorrentFromFile(filename, fileContent, Settings); + _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings); + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)UTorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)UTorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash, Settings); + }*/ + + return hash; + } + public override string Name => "uTorrent"; public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs index a5e5b006f..394fc53b4 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrentSettings.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")] + [FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Radarr avoids conflicts with unrelated downloads, but it's optional")] public string TvCategory { get; set; } [FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(UTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index c4fbe11a2..c55b98e84 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -27,7 +27,9 @@ namespace NzbDrone.Core.Download private readonly IEventAggregator _eventAggregator; private readonly IHistoryService _historyService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IDownloadedMovieImportService _downloadedMovieImportService; private readonly IParsingService _parsingService; + private readonly IMovieService _movieService; private readonly Logger _logger; private readonly ISeriesService _seriesService; @@ -35,15 +37,19 @@ namespace NzbDrone.Core.Download IEventAggregator eventAggregator, IHistoryService historyService, IDownloadedEpisodesImportService downloadedEpisodesImportService, + IDownloadedMovieImportService downloadedMovieImportService, IParsingService parsingService, ISeriesService seriesService, + IMovieService movieService, Logger logger) { _configService = configService; _eventAggregator = eventAggregator; _historyService = historyService; _downloadedEpisodesImportService = downloadedEpisodesImportService; + _downloadedMovieImportService = downloadedMovieImportService; _parsingService = parsingService; + _movieService = movieService; _logger = logger; _seriesService = seriesService; } @@ -61,7 +67,7 @@ namespace NzbDrone.Core.Download if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) { - trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping."); + trackedDownload.Warn("Download wasn't grabbed by Radarr and not in a category, Skipping."); return; } @@ -88,18 +94,17 @@ namespace NzbDrone.Core.Download return; } - var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title); - - if (series == null) + var movie = _parsingService.GetMovie(trackedDownload.DownloadItem.Title); + if (movie == null) { if (historyItem != null) { - series = _seriesService.GetSeries(historyItem.SeriesId); + movie = _movieService.GetMovie(historyItem.MovieId); } - if (series == null) + if (movie == null) { - trackedDownload.Warn("Series title mismatch, automatic import is not possible."); + trackedDownload.Warn("Movie title mismatch, automatic import is not possible."); return; } } @@ -111,7 +116,7 @@ namespace NzbDrone.Core.Download private void Import(TrackedDownload trackedDownload) { var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; - var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); + var importResults = _downloadedMovieImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem); if (importResults.Empty()) { @@ -119,7 +124,7 @@ namespace NzbDrone.Core.Download return; } - if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + if (importResults.Count(c => c.Result == ImportResultType.Imported) >= 1) { trackedDownload.State = TrackedDownloadStage.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); @@ -130,12 +135,11 @@ namespace NzbDrone.Core.Download { var statusMessages = importResults .Where(v => v.Result != ImportResultType.Imported) - .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) + .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalMovie.Path), v.Errors)) .ToArray(); trackedDownload.Warn(statusMessages); } - } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 98ade2a69..14f7f1b71 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -57,6 +57,7 @@ namespace NzbDrone.Core.Download get; } + public abstract string Download(RemoteEpisode remoteEpisode); public abstract IEnumerable GetItems(); public abstract void RemoveItem(string downloadId, bool deleteData); @@ -132,7 +133,7 @@ namespace NzbDrone.Core.Download { return new NzbDroneValidationFailure(propertyName, "Folder does not exist") { - DetailedDescription = string.Format("The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName) + DetailedDescription = string.Format("The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account '{0}', which is used to execute Radarr.", Environment.UserName) }; } @@ -141,11 +142,13 @@ namespace NzbDrone.Core.Download _logger.Error("Folder '{0}' is not writable.", folder); return new NzbDroneValidationFailure(propertyName, "Unable to write to folder") { - DetailedDescription = string.Format("The folder you specified is not writable. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName) + DetailedDescription = string.Format("The folder you specified is not writable. Please verify the folder permissions for the user account '{0}', which is used to execute Radarr.", Environment.UserName) }; } return null; } + + public abstract string Download(RemoteMovie remoteMovie); } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 1c0ca855a..57b423b14 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Download } public int SeriesId { get; set; } + public int MovieId { get; set; } public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } public string SourceTitle { get; set; } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index 4f76b1507..b7c4b205a 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -1,6 +1,5 @@ using System; using NLog; -using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation.Extensions; @@ -15,6 +14,7 @@ namespace NzbDrone.Core.Download public interface IDownloadService { void DownloadReport(RemoteEpisode remoteEpisode); + void DownloadReport(RemoteMovie remoteMovie); } @@ -41,8 +41,8 @@ namespace NzbDrone.Core.Download public void DownloadReport(RemoteEpisode remoteEpisode) { - Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); - Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); + //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); + //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit var downloadTitle = remoteEpisode.Release.Title; var downloadClient = _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol); @@ -91,5 +91,62 @@ namespace NzbDrone.Core.Download _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); _eventAggregator.PublishEvent(episodeGrabbedEvent); } + + public void DownloadReport(RemoteMovie remoteMovie) + { + //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); + //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit + + var downloadTitle = remoteMovie.Release.Title; + var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol); + + if (downloadClient == null) + { + _logger.Warn("{0} Download client isn't configured yet.", remoteMovie.Release.DownloadProtocol); + return; + } + + // Limit grabs to 2 per second. + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + var url = new HttpUri(remoteMovie.Release.DownloadUrl); + _rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2)); + } + + string downloadClientId = ""; + try + { + downloadClientId = downloadClient.Download(remoteMovie); + _indexerStatusService.RecordSuccess(remoteMovie.Release.IndexerId); + } + catch (NotImplementedException ex) + { + _logger.Error(ex, "The download client you are using is currently not configured to download movies. Please choose another one."); + } + catch (ReleaseDownloadException ex) + { + var http429 = ex.InnerException as TooManyRequestsException; + if (http429 != null) + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId, http429.RetryAfter); + } + else + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId); + } + throw; + } + + var episodeGrabbedEvent = new MovieGrabbedEvent(remoteMovie); + episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; + + if (!string.IsNullOrWhiteSpace(downloadClientId)) + { + episodeGrabbedEvent.DownloadId = downloadClientId; + } + + _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); + _eventAggregator.PublishEvent(episodeGrabbedEvent); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index d56349f7f..4168e0451 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Download if (grabbedItems.Empty()) { - trackedDownload.Warn("Download wasn't grabbed by sonarr, skipping"); + trackedDownload.Warn("Download wasn't grabbed by Radarr, skipping"); return; } @@ -88,6 +88,7 @@ namespace NzbDrone.Core.Download var downloadFailedEvent = new DownloadFailedEvent { SeriesId = historyItem.SeriesId, + MovieId = historyItem.MovieId, EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), Quality = historyItem.Quality, SourceTitle = historyItem.SourceTitle, diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 6703d8a22..ecf76844e 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download DownloadProtocol Protocol { get; } string Download(RemoteEpisode remoteEpisode); + string Download(RemoteMovie remoteMovie); IEnumerable GetItems(); void RemoveItem(string downloadId, bool deleteData); DownloadClientStatus GetStatus(); diff --git a/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs new file mode 100644 index 000000000..cb331b24a --- /dev/null +++ b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs @@ -0,0 +1,17 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public class MovieGrabbedEvent : IEvent + { + public RemoteMovie Movie { get; private set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } + + public MovieGrabbedEvent(RemoteMovie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs index a713fe48c..504db7e36 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Download.Pending public class PendingRelease : ModelBase { public int SeriesId { get; set; } + public int MovieId { get; set; } public string Title { get; set; } public DateTime Added { get; set; } public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } @@ -14,5 +15,6 @@ namespace NzbDrone.Core.Download.Pending //Not persisted public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 8585a1704..5a53e2d18 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.Download.Pending public class PendingReleaseService : IPendingReleaseService, IHandle, IHandle, + IHandle, IHandle { private readonly IIndexerStatusService _indexerStatusService; @@ -341,6 +342,11 @@ namespace NzbDrone.Core.Download.Pending RemoveGrabbed(message.Episode); } + public void Handle(MovieGrabbedEvent message) + { + //RemoveGrabbed(message.Movie); + } + public void Handle(RssSyncCompleteEvent message) { RemoveRejected(message.ProcessedDecisions.Rejected); diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 05719587d..70aba1c41 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -32,53 +32,118 @@ namespace NzbDrone.Core.Download public ProcessedDecisions ProcessDecisions(List decisions) { - var qualifiedReports = GetQualifiedReports(decisions); - var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); + //var qualifiedReports = GetQualifiedReports(decisions); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); var grabbed = new List(); var pending = new List(); foreach (var report in prioritizedDecisions) { - var remoteEpisode = report.RemoteEpisode; - var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList(); - - //Skip if already grabbed - if (grabbed.SelectMany(r => r.RemoteEpisode.Episodes) - .Select(e => e.Id) - .ToList() - .Intersect(episodeIds) - .Any()) + if (report.IsForMovie) { - continue; + var remoteMovie = report.RemoteMovie; + + if (report.TemporarilyRejected) + { + _pendingReleaseService.Add(report); + pending.Add(report); + continue; + } + + if (report.Rejections.Any()) + { + _logger.Debug("Rejecting release {0} because {1}", report.ToString(), report.Rejections.First().Reason); + continue; + } + + if (remoteMovie == null || remoteMovie.Movie == null) + { + continue; + } + + List movieIds = new List { remoteMovie.Movie.Id }; + + + //Skip if already grabbed + if (grabbed.Select(r => r.RemoteMovie.Movie) + .Select(e => e.Id) + .ToList() + .Intersect(movieIds) + .Any()) + { + continue; + } + + if (pending.Select(r => r.RemoteMovie.Movie) + .Select(e => e.Id) + .ToList() + .Intersect(movieIds) + .Any()) + { + continue; + } + + try + { + _downloadService.DownloadReport(remoteMovie); + grabbed.Add(report); + } + catch (Exception e) + { + //TODO: support for store & forward + //We'll need to differentiate between a download client error and an indexer error + _logger.Warn(e, "Couldn't add report to download queue. " + remoteMovie); + } } + else + { + var remoteEpisode = report.RemoteEpisode; - if (report.TemporarilyRejected) - { - _pendingReleaseService.Add(report); - pending.Add(report); - continue; - } + if (remoteEpisode == null || remoteEpisode.Episodes == null) + { + continue; + } - if (pending.SelectMany(r => r.RemoteEpisode.Episodes) - .Select(e => e.Id) - .ToList() - .Intersect(episodeIds) - .Any()) - { - continue; - } + var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList(); - try - { - _downloadService.DownloadReport(remoteEpisode); - grabbed.Add(report); - } - catch (Exception e) - { - //TODO: support for store & forward - //We'll need to differentiate between a download client error and an indexer error - _logger.Warn(e, "Couldn't add report to download queue. " + remoteEpisode); + //Skip if already grabbed + if (grabbed.SelectMany(r => r.RemoteEpisode.Episodes) + .Select(e => e.Id) + .ToList() + .Intersect(episodeIds) + .Any()) + { + continue; + } + + if (report.TemporarilyRejected) + { + _pendingReleaseService.Add(report); + pending.Add(report); + continue; + } + + if (pending.SelectMany(r => r.RemoteEpisode.Episodes) + .Select(e => e.Id) + .ToList() + .Intersect(episodeIds) + .Any()) + { + continue; + } + + try + { + _downloadService.DownloadReport(remoteEpisode); + grabbed.Add(report); + } + catch (Exception e) + { + //TODO: support for store & forward + //We'll need to differentiate between a download client error and an indexer error + _logger.Warn(e, "Couldn't add report to download queue. " + remoteEpisode); + } } } diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index d85729775..0e5b3a13a 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -34,6 +34,15 @@ namespace NzbDrone.Core.Download return; } + if (message.MovieId != 0) + { + _logger.Debug("Failed download contains a movie, searching again."); + + _commandQueueManager.Push(new MoviesSearchCommand { MovieId = message.MovieId }); + + return; + } + if (message.EpisodeIds.Count == 1) { _logger.Debug("Failed download only contains one episode, searching again"); diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index b1fcd7e2e..70681f992 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -40,35 +40,43 @@ namespace NzbDrone.Core.Download protected abstract string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink); protected abstract string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent); - - public override string Download(RemoteEpisode remoteEpisode) + protected virtual string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) { - var torrentInfo = remoteEpisode.Release as TorrentInfo; + throw new NotImplementedException(); + } + protected virtual string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException(); + } + + public override string Download(RemoteMovie remoteMovie) + { + var torrentInfo = remoteMovie.Release as TorrentInfo; string magnetUrl = null; string torrentUrl = null; - if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteEpisode.Release.DownloadUrl.StartsWith("magnet:")) + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) { - magnetUrl = remoteEpisode.Release.DownloadUrl; + magnetUrl = remoteMovie.Release.DownloadUrl; } else { - torrentUrl = remoteEpisode.Release.DownloadUrl; + torrentUrl = remoteMovie.Release.DownloadUrl; } if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) { magnetUrl = torrentInfo.MagnetUrl; } - + if (PreferTorrentFile) { if (torrentUrl.IsNotNullOrWhiteSpace()) { try { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } catch (Exception ex) { @@ -85,11 +93,11 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } } } @@ -99,13 +107,13 @@ namespace NzbDrone.Core.Download { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { if (torrentUrl.IsNullOrWhiteSpace()) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); @@ -114,13 +122,193 @@ namespace NzbDrone.Core.Download if (torrentUrl.IsNotNullOrWhiteSpace()) { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } } return null; } + public override string Download(RemoteEpisode remoteMovie) + { + var torrentInfo = remoteMovie.Release as TorrentInfo; + + string magnetUrl = null; + string torrentUrl = null; + + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + magnetUrl = remoteMovie.Release.DownloadUrl; + } + else + { + torrentUrl = remoteMovie.Release.DownloadUrl; + } + + if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) + { + magnetUrl = torrentInfo.MagnetUrl; + } + + if (PreferTorrentFile) + { + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + catch (Exception ex) + { + if (!magnetUrl.IsNullOrWhiteSpace()) + { + throw; + } + + _logger.Debug("Torrent download failed, trying magnet. ({0})", ex.Message); + } + } + + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + } + } + else + { + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + if (torrentUrl.IsNullOrWhiteSpace()) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + + _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); + } + } + + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + } + + return null; + } + + private string DownloadFromWebUrl(RemoteMovie remoteEpisode, string torrentUrl) + { + byte[] torrentFile = null; + + try + { + var request = new HttpRequest(torrentUrl); + request.Headers.Accept = "application/x-bittorrent"; + request.AllowAutoRedirect = false; + + var response = _httpClient.Get(request); + + if (response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.Found) + { + var locationHeader = response.Headers.GetSingleValue("Location"); + + _logger.Trace("Torrent request is being redirected to: {0}", locationHeader); + + if (locationHeader != null) + { + if (locationHeader.StartsWith("magnet:")) + { + return DownloadFromMagnetUrl(remoteEpisode, locationHeader); + } + + return DownloadFromWebUrl(remoteEpisode, locationHeader); + } + + throw new WebException("Remote website tried to redirect without providing a location."); + } + + torrentFile = response.ResponseData; + + _logger.Debug("Downloading torrent for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, torrentFile.Length, torrentUrl); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", torrentUrl); + } + else + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + + var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteEpisode.Release.Title)); + var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile); + var actualHash = AddFromTorrentFile(remoteEpisode, hash, filename, torrentFile); + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + + private string DownloadFromMagnetUrl(RemoteMovie remoteEpisode, string magnetUrl) + { + string hash = null; + string actualHash = null; + + try + { + hash = new MagnetLink(magnetUrl).InfoHash.ToHex(); + } + catch (FormatException ex) + { + _logger.Error(ex, "Failed to parse magnetlink for episode '{0}': '{1}'", remoteEpisode.Release.Title, magnetUrl); + + return null; + } + + if (hash != null) + { + actualHash = AddFromMagnetLink(remoteEpisode, hash, magnetUrl); + } + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + private string DownloadFromWebUrl(RemoteEpisode remoteEpisode, string torrentUrl) { byte[] torrentFile = null; @@ -183,7 +371,7 @@ namespace NzbDrone.Core.Download if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) { _logger.Debug( - "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", Definition.Implementation, remoteEpisode.Release.DownloadUrl); } @@ -214,7 +402,7 @@ namespace NzbDrone.Core.Download if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) { _logger.Debug( - "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", Definition.Implementation, remoteEpisode.Release.DownloadUrl); } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index be012d57b..57ce35578 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads public TrackedDownloadStage State { get; set; } public TrackedDownloadStatus Status { get; private set; } public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 55ce7398d..01f87f07a 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -57,8 +57,14 @@ namespace NzbDrone.Core.Download.TrackedDownloads try { var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); + var parsedMovieInfo = Parser.Parser.ParseMovieTitle(trackedDownload.DownloadItem.Title); var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId); + if (parsedMovieInfo != null) + { + trackedDownload.RemoteMovie = _parsingService.Map(parsedMovieInfo, "", null); + } + if (parsedEpisodeInfo != null) { trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0); @@ -69,10 +75,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); - if (parsedEpisodeInfo == null || + if ((parsedEpisodeInfo == null || trackedDownload.RemoteEpisode == null || trackedDownload.RemoteEpisode.Series == null || - trackedDownload.RemoteEpisode.Episodes.Empty()) + trackedDownload.RemoteEpisode.Episodes.Empty()) && trackedDownload.RemoteMovie == null) { // Try parsing the original source title and if that fails, try parsing it as a special // TODO: Pass the TVDB ID and TVRage IDs in as well so we have a better chance for finding the item @@ -85,7 +91,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - if (trackedDownload.RemoteEpisode == null) + if (trackedDownload.RemoteEpisode == null && trackedDownload.RemoteMovie == null) { return null; } diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index a6c0ed7d5..0f2ea47de 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -1,4 +1,5 @@ using System.Net; +using System; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; @@ -29,7 +30,9 @@ namespace NzbDrone.Core.Download public override DownloadProtocol Protocol => DownloadProtocol.Usenet; - protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent); + protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContents); + + protected abstract string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents); public override string Download(RemoteEpisode remoteEpisode) { @@ -67,5 +70,42 @@ namespace NzbDrone.Core.Download _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); return AddFromNzbFile(remoteEpisode, filename, nzbData); } + + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var filename = FileNameBuilder.CleanFileName(remoteEpisode.Release.Title) + ".nzb"; + + byte[] nzbData; + + try + { + nzbData = _httpClient.Get(new HttpRequest(url)).ResponseData; + + _logger.Debug("Downloaded nzb for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, nzbData.Length, url); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", url); + } + else + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + + _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); + return AddFromNzbFile(remoteEpisode, filename, nzbData); + } } } diff --git a/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs new file mode 100644 index 000000000..c2345bd93 --- /dev/null +++ b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs @@ -0,0 +1,27 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Exceptions +{ + public class MovieNotFoundException : NzbDroneException + { + public string ImdbId { get; set; } + + public MovieNotFoundException(string imdbid) + : base(string.Format("Movie with imdbid {0} was not found, it may have been removed from IMDb.", imdbid)) + { + ImdbId = imdbid; + } + + public MovieNotFoundException(string imdbid, string message, params object[] args) + : base(message, args) + { + ImdbId = imdbid; + } + + public MovieNotFoundException(string imdbid, string message) + : base(message) + { + ImdbId = imdbid; + } + } +} diff --git a/src/NzbDrone.Core/Extras/ExtraService.cs b/src/NzbDrone.Core/Extras/ExtraService.cs index 5906de176..012e8f921 100644 --- a/src/NzbDrone.Core/Extras/ExtraService.cs +++ b/src/NzbDrone.Core/Extras/ExtraService.cs @@ -111,6 +111,18 @@ namespace NzbDrone.Core.Extras } } + //TODO: Implementing this will fix a lot of our warning exceptions + //public void Handle(MediaCoversUpdatedEvent message) + //{ + // var movie = message.Movie; + // var movieFiles = GetMovieFiles(movie.Id); + + // foreach (var extraFileManager in _extraFileManagers) + // { + // extraFileManager.CreateAfterMovieScan(movie, movieFiles); + // } + //} + public void Handle(EpisodeFolderCreatedEvent message) { var series = message.Series; diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index be35637c8..451e9b1d5 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -17,9 +17,11 @@ namespace NzbDrone.Core.History public int EpisodeId { get; set; } public int SeriesId { get; set; } + public int MovieId { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } + public Movie Movie { get; set; } public Episode Episode { get; set; } public Series Series { get; set; } public HistoryEventType EventType { get; set; } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 35199a878..bc0a54a5a 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.History List FindByDownloadId(string downloadId); List FindDownloadHistory(int idSeriesId, QualityModel quality); void DeleteForSeries(int seriesId); + History MostRecentForMovie(int movieId); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -71,10 +73,20 @@ namespace NzbDrone.Core.History protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { - 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); + 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)*/ + .Join(JoinType.Inner, h => h.Movie, (h, e) => h.MovieId == e.Id); + + return base.GetPagedQuery(baseQuery, pagingSpec); } + + public History MostRecentForMovie(int movieId) + { + return Query.Where(h => h.MovieId == movieId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 32815beef..634cf8b00 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.History { QualityModel GetBestQualityInHistory(Profile profile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); + History MostRecentForMovie(int movieId); History MostRecentForEpisode(int episodeId); History MostRecentForDownloadId(string downloadId); History Get(int historyId); @@ -29,6 +30,8 @@ namespace NzbDrone.Core.History public class HistoryService : IHistoryService, IHandle, + IHandle, + IHandle, IHandle, IHandle, IHandle, @@ -53,6 +56,11 @@ namespace NzbDrone.Core.History return _historyRepository.MostRecentForEpisode(episodeId); } + public History MostRecentForMovie(int movieId) + { + return _historyRepository.MostRecentForMovie(movieId); + } + public History MostRecentForDownloadId(string downloadId) { return _historyRepository.MostRecentForDownloadId(downloadId); @@ -138,7 +146,8 @@ namespace NzbDrone.Core.History SourceTitle = message.Episode.Release.Title, SeriesId = episode.SeriesId, EpisodeId = episode.Id, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + MovieId = 0 }; history.Data.Add("Indexer", message.Episode.Release.Indexer); @@ -172,6 +181,50 @@ namespace NzbDrone.Core.History } } + public void Handle(MovieGrabbedEvent message) + { + var history = new History + { + EventType = HistoryEventType.Grabbed, + Date = DateTime.UtcNow, + Quality = message.Movie.ParsedMovieInfo.Quality, + SourceTitle = message.Movie.Release.Title, + SeriesId = 0, + EpisodeId = 0, + DownloadId = message.DownloadId, + MovieId = message.Movie.Movie.Id + }; + + history.Data.Add("Indexer", message.Movie.Release.Indexer); + history.Data.Add("NzbInfoUrl", message.Movie.Release.InfoUrl); + history.Data.Add("ReleaseGroup", message.Movie.ParsedMovieInfo.ReleaseGroup); + history.Data.Add("Age", message.Movie.Release.Age.ToString()); + history.Data.Add("AgeHours", message.Movie.Release.AgeHours.ToString()); + history.Data.Add("AgeMinutes", message.Movie.Release.AgeMinutes.ToString()); + history.Data.Add("PublishedDate", message.Movie.Release.PublishDate.ToString("s") + "Z"); + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("Size", message.Movie.Release.Size.ToString()); + history.Data.Add("DownloadUrl", message.Movie.Release.DownloadUrl); + history.Data.Add("Guid", message.Movie.Release.Guid); + history.Data.Add("TvdbId", message.Movie.Release.TvdbId.ToString()); + history.Data.Add("TvRageId", message.Movie.Release.TvRageId.ToString()); + history.Data.Add("Protocol", ((int)message.Movie.Release.DownloadProtocol).ToString()); + + if (!message.Movie.ParsedMovieInfo.ReleaseHash.IsNullOrWhiteSpace()) + { + history.Data.Add("ReleaseHash", message.Movie.ParsedMovieInfo.ReleaseHash); + } + + var torrentRelease = message.Movie.Release as TorrentInfo; + + if (torrentRelease != null) + { + history.Data.Add("TorrentInfoHash", torrentRelease.InfoHash); + } + + _historyRepository.Insert(history); + } + public void Handle(EpisodeImportedEvent message) { if (!message.NewDownload) @@ -189,15 +242,18 @@ namespace NzbDrone.Core.History foreach (var episode in message.EpisodeInfo.Episodes) { var history = new History - { - EventType = HistoryEventType.DownloadFolderImported, - Date = DateTime.UtcNow, - Quality = message.EpisodeInfo.Quality, - SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), - SeriesId = message.ImportedEpisode.SeriesId, - EpisodeId = episode.Id, - DownloadId = downloadId - }; + { + EventType = HistoryEventType.DownloadFolderImported, + Date = DateTime.UtcNow, + Quality = message.EpisodeInfo.Quality, + SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), + SeriesId = message.ImportedEpisode.SeriesId, + EpisodeId = episode.Id, + DownloadId = downloadId, + MovieId = 0, + + + }; //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); @@ -209,6 +265,45 @@ namespace NzbDrone.Core.History } } + public void Handle(MovieImportedEvent message) + { + if (!message.NewDownload) + { + return; + } + + var downloadId = message.DownloadId; + + if (downloadId.IsNullOrWhiteSpace()) + { + //downloadId = FindDownloadId(message); For now fuck off. + } + + var movie = message.MovieInfo.Movie; + var history = new History + { + EventType = HistoryEventType.DownloadFolderImported, + Date = DateTime.UtcNow, + Quality = message.MovieInfo.Quality, + SourceTitle = movie.Title, + SeriesId = 0, + EpisodeId = 0, + DownloadId = downloadId, + MovieId = movie.Id, + + + }; + + //Won't have a value since we publish this event before saving to DB. + //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); + history.Data.Add("DroppedPath", message.MovieInfo.Path); + history.Data.Add("ImportedPath", Path.Combine(movie.Path, message.ImportedMovie.RelativePath)); + history.Data.Add("DownloadClient", message.DownloadClient); + + _historyRepository.Insert(history); + + } + public void Handle(DownloadFailedEvent message) { foreach (var episodeId in message.EpisodeIds) @@ -249,6 +344,7 @@ namespace NzbDrone.Core.History SourceTitle = message.EpisodeFile.Path, SeriesId = message.EpisodeFile.SeriesId, EpisodeId = episode.Id, + MovieId = 0 }; history.Data.Add("Reason", message.Reason.ToString()); diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs index ca03130e6..e8ee88d68 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupOrphanedHistoryItems.cs @@ -13,8 +13,9 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public void Clean() { - CleanupOrphanedBySeries(); - CleanupOrphanedByEpisode(); + //CleanupOrphanedBySeries(); + //CleanupOrphanedByEpisode(); + CleanupOrphanedByMovie(); } private void CleanupOrphanedBySeries() @@ -29,6 +30,18 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers WHERE Series.Id IS NULL)"); } + private void CleanupOrphanedByMovie() + { + var mapper = _database.GetDataMapper(); + + mapper.ExecuteNonQuery(@"DELETE FROM History + WHERE Id IN ( + SELECT History.Id FROM History + LEFT OUTER JOIN Movies + ON History.MovieId = Movies.Id + WHERE Movies.Id IS NULL)"); + } + private void CleanupOrphanedByEpisode() { var mapper = _database.GetDataMapper(); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs new file mode 100644 index 000000000..12f9baf1d --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/MovieSearchCriteria.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Core.IndexerSearch.Definitions +{ + public class MovieSearchCriteria : SearchCriteriaBase + { + + public override string ToString() + { + return string.Format("[{0}]", Movie.Title); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs index c5e602e59..937b27880 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions private static readonly Regex BeginningThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); public Series Series { get; set; } + + public Movie Movie { get; set; } public List SceneTitles { get; set; } public List Episodes { get; set; } public virtual bool MonitoredEpisodesOnly { get; set; } diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs new file mode 100644 index 000000000..da0b9a8c1 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class MoviesSearchCommand : Command + { + public int MovieId { get; set; } + + public override bool SendUpdatesToClient => true; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs new file mode 100644 index 000000000..656423178 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs @@ -0,0 +1,46 @@ +using System.Linq; +using NLog; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.IndexerSearch +{ + public class MovieSearchService : IExecute + { + private readonly IMovieService _seriesService; + private readonly ISearchForNzb _nzbSearchService; + private readonly IProcessDownloadDecisions _processDownloadDecisions; + private readonly Logger _logger; + + public MovieSearchService(IMovieService seriesService, + ISearchForNzb nzbSearchService, + IProcessDownloadDecisions processDownloadDecisions, + Logger logger) + { + _seriesService = seriesService; + _nzbSearchService = nzbSearchService; + _processDownloadDecisions = processDownloadDecisions; + _logger = logger; + } + + public void Execute(MoviesSearchCommand message) + { + var series = _seriesService.GetMovie(message.MovieId); + + var downloadedCount = 0; + + if (!series.Monitored) + { + _logger.Debug("Movie {0} is not monitored, skipping search", series.Title); + } + + var decisions = _nzbSearchService.MovieSearch(message.MovieId, false);//_nzbSearchService.SeasonSearch(message.MovieId, season.SeasonNumber, false, message.Trigger == CommandTrigger.Manual); + downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count; + + + _logger.ProgressInfo("Movie search completed. {0} reports downloaded.", downloadedCount); + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index cff3e290c..98865538e 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; @@ -19,6 +19,8 @@ namespace NzbDrone.Core.IndexerSearch { List EpisodeSearch(int episodeId, bool userInvokedSearch); List EpisodeSearch(Episode episode, bool userInvokedSearch); + List MovieSearch(int movieId, bool userInvokedSearch); + List MovieSearch(Movie movie, bool userInvokedSearch); List SeasonSearch(int seriesId, int seasonNumber, bool missingOnly, bool userInvokedSearch); } @@ -29,6 +31,7 @@ namespace NzbDrone.Core.IndexerSearch private readonly ISeriesService _seriesService; private readonly IEpisodeService _episodeService; private readonly IMakeDownloadDecision _makeDownloadDecision; + private readonly IMovieService _movieService; private readonly Logger _logger; public NzbSearchService(IIndexerFactory indexerFactory, @@ -36,6 +39,7 @@ namespace NzbDrone.Core.IndexerSearch ISeriesService seriesService, IEpisodeService episodeService, IMakeDownloadDecision makeDownloadDecision, + IMovieService movieService, Logger logger) { _indexerFactory = indexerFactory; @@ -43,6 +47,7 @@ namespace NzbDrone.Core.IndexerSearch _seriesService = seriesService; _episodeService = episodeService; _makeDownloadDecision = makeDownloadDecision; + _movieService = movieService; _logger = logger; } @@ -53,6 +58,20 @@ namespace NzbDrone.Core.IndexerSearch return EpisodeSearch(episode, userInvokedSearch); } + public List MovieSearch(int movieId, bool userInvokedSearch) + { + var movie = _movieService.GetMovie(movieId); + + return MovieSearch(movie, userInvokedSearch); + } + + public List MovieSearch(Movie movie, bool userInvokedSearch) + { + var searchSpec = Get(movie, userInvokedSearch); + + return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); + } + public List EpisodeSearch(Episode episode, bool userInvokedSearch) { var series = _seriesService.GetSeries(episode.SeriesId); @@ -230,21 +249,38 @@ namespace NzbDrone.Core.IndexerSearch private TSpec Get(Series series, List episodes, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new() { - var spec = new TSpec(); - - spec.Series = series; - spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId, + var spec = new TSpec() + { + Series = series, + SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId, episodes.Select(e => e.SeasonNumber).Distinct().ToList(), - episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList()); - - spec.Episodes = episodes; + episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList()), + Episodes = episodes + }; spec.SceneTitles.Add(series.Title); spec.UserInvokedSearch = userInvokedSearch; return spec; } + private TSpec Get(Movie movie, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new() + { + var spec = new TSpec() + { + Movie = movie, + /*spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId, + episodes.Select(e => e.SeasonNumber).Distinct().ToList(), + episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList()); + + spec.Episodes = episodes; + + spec.SceneTitles.Add(series.Title);*/ + UserInvokedSearch = userInvokedSearch + }; + return spec; + } + private List Dispatch(Func> searchAction, SearchCriteriaBase criteriaBase) { var indexers = _indexerFactory.SearchEnabled(); diff --git a/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHD.cs b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHD.cs new file mode 100644 index 000000000..afe7892a0 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHD.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.AwesomeHD +{ + public class AwesomeHD : HttpIndexerBase + { + public override string Name => "AwesomeHD"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + + public AwesomeHD(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new AwesomeHDRequestGenerator() { Settings = Settings }; + } + + public override IParseIndexerResponse GetParser() + { + return new AwesomeHDRssParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDApi.cs b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDApi.cs new file mode 100644 index 000000000..77744b4ac --- /dev/null +++ b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDApi.cs @@ -0,0 +1,80 @@ +using System; +using Newtonsoft.Json; + +using System.Xml.Serialization; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.AwesomeHD +{ + public class Torrent + { + [XmlElement(ElementName = "id")] + public string Id { get; set; } + [XmlElement(ElementName = "groupid")] + public string GroupId { get; set; } + [XmlElement(ElementName = "time")] + public DateTime Time { get; set; } + [XmlElement(ElementName = "userid")] + public string Userid { get; set; } + [XmlElement(ElementName = "size")] + public long Size { get; set; } + [XmlElement(ElementName = "snatched")] + public string Snatched { get; set; } + [XmlElement(ElementName = "seeders")] + public string Seeders { get; set; } + [XmlElement(ElementName = "leechers")] + public string Leechers { get; set; } + [XmlElement(ElementName = "releasegroup")] + public string Releasegroup { get; set; } + [XmlElement(ElementName = "resolution")] + public string Resolution { get; set; } + [XmlElement(ElementName = "media")] + public string Media { get; set; } + [XmlElement(ElementName = "format")] + public string Format { get; set; } + [XmlElement(ElementName = "encoding")] + public string Encoding { get; set; } + [XmlElement(ElementName = "audioformat")] + public string Audioformat { get; set; } + [XmlElement(ElementName = "audiobitrate")] + public string Audiobitrate { get; set; } + [XmlElement(ElementName = "audiochannels")] + public string Audiochannels { get; set; } + [XmlElement(ElementName = "subtitles")] + public string Subtitles { get; set; } + [XmlElement(ElementName = "encodestatus")] + public string Encodestatus { get; set; } + [XmlElement(ElementName = "freeleech")] + public string Freeleech { get; set; } + [XmlElement(ElementName = "cover")] + public string Cover { get; set; } + [XmlElement(ElementName = "smallcover")] + public string Smallcover { get; set; } + [XmlElement(ElementName = "year")] + public string Year { get; set; } + [XmlElement(ElementName = "name")] + public string Name { get; set; } + [XmlElement(ElementName = "imdb")] + public string Imdb { get; set; } + [XmlElement(ElementName = "type")] + public string Type { get; set; } + [XmlElement(ElementName = "plotoutline")] + public string Plotoutline { get; set; } + } + + public class SearchResults + { + [XmlElement(ElementName = "authkey")] + public string AuthKey { get; set; } + [XmlElement(ElementName = "torrent")] + public List Torrent { get; set; } + } + + public class AwesomeHDSearchResponse + { + [XmlElement(ElementName = "?xml")] + public string Xml { get; set; } + [XmlElement(ElementName = "searchresults")] + public SearchResults SearchResults { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRequestGenerator.cs similarity index 50% rename from src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs rename to src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRequestGenerator.cs index ebfa73788..00289d7e4 100644 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRequestGenerator.cs @@ -1,18 +1,21 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.IndexerSearch.Definitions; -namespace NzbDrone.Core.Indexers.Torrentleech +namespace NzbDrone.Core.Indexers.AwesomeHD { - public class TorrentleechRequestGenerator : IIndexerRequestGenerator + public class AwesomeHDRequestGenerator : IIndexerRequestGenerator { - public TorrentleechSettings Settings { get; set; } - + public AwesomeHDSettings Settings { get; set; } + public virtual IndexerPageableRequestChain GetRecentRequests() { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRssRequests(null)); + pageableRequests.Add(GetRequest(null)); return pageableRequests; } @@ -22,16 +25,6 @@ namespace NzbDrone.Core.Indexers.Torrentleech return new IndexerPageableRequestChain(); } - public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); @@ -42,9 +35,34 @@ namespace NzbDrone.Core.Indexers.Torrentleech return new IndexerPageableRequestChain(); } - private IEnumerable GetRssRequests(string searchParameters) + public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) { - yield return new IndexerRequest(string.Format("{0}/{1}{2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.ApiKey, searchParameters), HttpAccept.Rss); + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(searchCriteria.Movie.ImdbId)); + return pageableRequests; + } + + private IEnumerable GetRequest(string searchParameters) + { + if (searchParameters != null) + { + yield return new IndexerRequest(string.Format("{0}/searchapi.php?action=imdbsearch&passkey={1}&imdb={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.Passkey.Trim(), searchParameters), HttpAccept.Rss); + } + else + { + yield return new IndexerRequest(string.Format("{0}/searchapi.php?action=latestmovies&passkey={1}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.Passkey.Trim()), HttpAccept.Rss); + } + } } } diff --git a/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRssParser.cs b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRssParser.cs new file mode 100644 index 000000000..e7a2632a0 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDRssParser.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; +using System; +using System.Linq; +using System.Xml; + +namespace NzbDrone.Core.Indexers.AwesomeHD +{ + public class AwesomeHDRssParser : IParseIndexerResponse + { + private readonly AwesomeHDSettings _settings; + + public AwesomeHDRssParser(AwesomeHDSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, + "Unexpected response status {0} code from API request", + indexerResponse.HttpResponse.StatusCode); + } + + // Hacky ¯\_(ツ)_/¯ + XmlDocument doc = new XmlDocument(); + doc.LoadXml(indexerResponse.Content); + + var json = JsonConvert.SerializeXmlNode(doc); + + Console.WriteLine(json); + + var jsonResponse = JsonConvert.DeserializeObject(json); + + if (jsonResponse == null) + { + throw new IndexerException(indexerResponse, "Unexpected response from request"); + } + + foreach (var torrent in jsonResponse.SearchResults.Torrent) + { + var id = torrent.Id; + var title = $"{torrent.Name}.{torrent.Year}.{torrent.Resolution}.{torrent.Media}.{torrent.Encoding}.{torrent.Audioformat}-{torrent.Releasegroup}"; + + torrentInfos.Add(new TorrentInfo() + { + Guid = string.Format("AwesomeHD-{0}", id), + Title = title, + Size = torrent.Size, + DownloadUrl = GetDownloadUrl(id, jsonResponse.SearchResults.AuthKey, _settings.Passkey), + InfoUrl = GetInfoUrl(torrent.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.Time.ToUniversalTime() + }); + } + + return torrentInfos.OrderByDescending(o => ((dynamic)o).Seeders).ToArray(); + } + + private string GetDownloadUrl(string torrentId, string authKey, string passKey) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("action", "download") + .AddQueryParam("id", torrentId) + .AddQueryParam("authkey", authKey) + .AddQueryParam("torrent_pass", passKey); + + return url.FullUri; + } + + private string GetInfoUrl(string groupId, string torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("id", groupId) + .AddQueryParam("torrentid", torrentId); + + return url.FullUri; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDSettings.cs b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDSettings.cs new file mode 100644 index 000000000..3c6f525c4 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/AwesomeHD/AwesomeHDSettings.cs @@ -0,0 +1,37 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.AwesomeHD +{ + public class AwesomeHDSettingsValidator : AbstractValidator + { + public AwesomeHDSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.Passkey).NotEmpty(); + } + } + + public class AwesomeHDSettings : IProviderConfig + { + private static readonly AwesomeHDSettingsValidator Validator = new AwesomeHDSettingsValidator(); + + public AwesomeHDSettings() + { + BaseUrl = "https://awesome-hd.me"; + } + + [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since you Passkey will be sent to that host.")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Passkey")] + public string Passkey { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTv.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTv.cs deleted file mode 100644 index d6bfec2fb..000000000 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTv.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NLog; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Indexers.BitMeTv -{ - public class BitMeTv : HttpIndexerBase - { - public override string Name => "BitMeTV"; - - public override DownloadProtocol Protocol => DownloadProtocol.Torrent; - public override bool SupportsSearch => false; - public override int PageSize => 0; - - public BitMeTv(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new BitMeTvRequestGenerator() { Settings = Settings }; - } - - public override IParseIndexerResponse GetParser() - { - return new TorrentRssParser() { ParseSizeInDescription = true }; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs deleted file mode 100644 index 6e48f46de..000000000 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvSettings.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.RegularExpressions; -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.BitMeTv -{ - public class BitMeTvSettingsValidator : AbstractValidator - { - public BitMeTvSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - RuleFor(c => c.UserId).NotEmpty(); - RuleFor(c => c.RssPasskey).NotEmpty(); - - RuleFor(c => c.Cookie).NotEmpty(); - - RuleFor(c => c.Cookie) - .Matches(@"pass=[0-9a-f]{32}", RegexOptions.IgnoreCase) - .WithMessage("Wrong pattern") - .AsWarning(); - } - } - - public class BitMeTvSettings : IProviderConfig - { - private static readonly BitMeTvSettingsValidator Validator = new BitMeTvSettingsValidator(); - - public BitMeTvSettings() - { - BaseUrl = "https://www.bitmetv.org"; - } - - [FieldDefinition(0, Label = "Website URL")] - public string BaseUrl { get; set; } - - [FieldDefinition(1, Label = "UserId")] - public string UserId { get; set; } - - [FieldDefinition(2, Label = "RSS Passkey")] - public string RssPasskey { get; set; } - - [FieldDefinition(3, Label = "Cookie", HelpText = "BitMeTv uses a login cookie needed to access the rss, you'll have to retrieve it via a browser.")] - public string Cookie { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs deleted file mode 100644 index fec611710..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNet.cs +++ /dev/null @@ -1,45 +0,0 @@ -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNet : HttpIndexerBase - { - public override string Name => "BroadcastheNet"; - - public override DownloadProtocol Protocol => DownloadProtocol.Torrent; - public override bool SupportsRss => true; - public override bool SupportsSearch => true; - public override int PageSize => 100; - - public BroadcastheNet(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize }; - - var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id); - if (releaseInfo != null) - { - int torrentID; - if (int.TryParse(releaseInfo.Guid.Replace("BTN-", string.Empty), out torrentID)) - { - requestGenerator.LastRecentTorrentID = torrentID; - } - } - - return requestGenerator; - } - - public override IParseIndexerResponse GetParser() - { - return new BroadcastheNetParser(); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs deleted file mode 100644 index 9d126da54..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetParser.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.RegularExpressions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Indexers.Exceptions; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetParser : IParseIndexerResponse - { - private static readonly Regex RegexProtocol = new Regex("^https?:", RegexOptions.Compiled); - - public IList ParseResponse(IndexerResponse indexerResponse) - { - var results = new List(); - - switch (indexerResponse.HttpResponse.StatusCode) - { - case HttpStatusCode.Unauthorized: - throw new ApiKeyException("API Key invalid or not authorized"); - case HttpStatusCode.NotFound: - throw new IndexerException(indexerResponse, "Indexer API call returned NotFound, the Indexer API may have changed."); - case HttpStatusCode.ServiceUnavailable: - throw new RequestLimitReachedException("Cannot do more than 150 API requests per hour."); - default: - if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) - { - throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); - } - break; - } - - if (indexerResponse.Content == "Query execution was interrupted") - { - throw new IndexerException(indexerResponse, "Indexer API returned an internal server error"); - } - - - JsonRpcResponse jsonResponse = new HttpResponse>(indexerResponse.HttpResponse).Resource; - - if (jsonResponse.Error != null || jsonResponse.Result == null) - { - throw new IndexerException(indexerResponse, "Indexer API call returned an error [{0}]", jsonResponse.Error); - } - - if (jsonResponse.Result.Results == 0) - { - return results; - } - - var protocol = indexerResponse.HttpRequest.Url.Scheme + ":"; - - foreach (var torrent in jsonResponse.Result.Torrents.Values) - { - var torrentInfo = new TorrentInfo(); - - torrentInfo.Guid = string.Format("BTN-{0}", torrent.TorrentID); - torrentInfo.Title = torrent.ReleaseName; - torrentInfo.Size = torrent.Size; - torrentInfo.DownloadUrl = RegexProtocol.Replace(torrent.DownloadURL, protocol); - torrentInfo.InfoUrl = string.Format("{0}//broadcasthe.net/torrents.php?id={1}&torrentid={2}", protocol, torrent.GroupID, torrent.TorrentID); - //torrentInfo.CommentUrl = - if (torrent.TvdbID.HasValue) - { - torrentInfo.TvdbId = torrent.TvdbID.Value; - } - if (torrent.TvrageID.HasValue) - { - torrentInfo.TvRageId = torrent.TvrageID.Value; - } - torrentInfo.PublishDate = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).ToUniversalTime().AddSeconds(torrent.Time); - //torrentInfo.MagnetUrl = - torrentInfo.InfoHash = torrent.InfoHash; - torrentInfo.Seeders = torrent.Seeders; - torrentInfo.Peers = torrent.Leechers + torrent.Seeders; - - torrentInfo.Origin = torrent.Origin; - torrentInfo.Source = torrent.Source; - torrentInfo.Container = torrent.Container; - torrentInfo.Codec = torrent.Codec; - torrentInfo.Resolution = torrent.Resolution; - - results.Add(torrentInfo); - } - - return results; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs deleted file mode 100644 index b5a39a94c..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System.Linq; -using System.Collections.Generic; -using NzbDrone.Common.Http; -using NzbDrone.Core.IndexerSearch.Definitions; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetRequestGenerator : IIndexerRequestGenerator - { - public int MaxPages { get; set; } - public int PageSize { get; set; } - public BroadcastheNetSettings Settings { get; set; } - - public int? LastRecentTorrentID { get; set; } - - public BroadcastheNetRequestGenerator() - { - MaxPages = 10; - PageSize = 100; - } - - public virtual IndexerPageableRequestChain GetRecentRequests() - { - var pageableRequests = new IndexerPageableRequestChain(); - - if (LastRecentTorrentID.HasValue) - { - pageableRequests.Add(GetPagedRequests(MaxPages, new BroadcastheNetTorrentQuery() - { - Id = ">=" + (LastRecentTorrentID.Value - 100) - })); - } - - pageableRequests.AddTier(GetPagedRequests(MaxPages, new BroadcastheNetTorrentQuery() - { - Age = "<=86400" - })); - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - var parameters = new BroadcastheNetTorrentQuery(); - if (AddSeriesSearchParameters(parameters, searchCriteria)) - { - foreach (var episode in searchCriteria.Episodes) - { - parameters = parameters.Clone(); - - parameters.Category = "Episode"; - parameters.Name = string.Format("S{0:00}%E{1:00}%", episode.SeasonNumber, episode.EpisodeNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - - foreach (var seasonNumber in searchCriteria.Episodes.Select(v => v.SeasonNumber).Distinct()) - { - parameters = parameters.Clone(); - - parameters.Category = "Season"; - parameters.Name = string.Format("Season {0}", seasonNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - } - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - var parameters = new BroadcastheNetTorrentQuery(); - if (AddSeriesSearchParameters(parameters, searchCriteria)) - { - foreach (var seasonNumber in searchCriteria.Episodes.Select(v => v.SeasonNumber).Distinct()) - { - parameters.Category = "Season"; - parameters.Name = string.Format("Season {0}", seasonNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - - parameters = parameters.Clone(); - - parameters.Category = "Episode"; - parameters.Name = string.Format("S{0:00}E%", seasonNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - } - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - var parameters = new BroadcastheNetTorrentQuery(); - if (AddSeriesSearchParameters(parameters, searchCriteria)) - { - parameters.Category = "Episode"; - parameters.Name = string.Format("{0:yyyy}.{0:MM}.{0:dd}", searchCriteria.AirDate); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - - pageableRequests.AddTier(); - - foreach (var episode in searchCriteria.Episodes) - { - parameters = parameters.Clone(); - - parameters.Category = "Episode"; - parameters.Name = string.Format("S{0:00}E{1:00}", episode.SeasonNumber, episode.EpisodeNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - } - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - var parameters = new BroadcastheNetTorrentQuery(); - if (AddSeriesSearchParameters(parameters, searchCriteria)) - { - foreach (var episode in searchCriteria.Episodes) - { - parameters = parameters.Clone(); - - parameters.Category = "Episode"; - parameters.Name = string.Format("S{0:00}E{1:00}", episode.SeasonNumber, episode.EpisodeNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - - foreach (var seasonNumber in searchCriteria.Episodes.Select(v => v.SeasonNumber).Distinct()) - { - parameters = parameters.Clone(); - - parameters.Category = "Season"; - parameters.Name = string.Format("Season {0}", seasonNumber); - - pageableRequests.Add(GetPagedRequests(MaxPages, parameters)); - } - } - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - private bool AddSeriesSearchParameters(BroadcastheNetTorrentQuery parameters, SearchCriteriaBase searchCriteria) - { - if (searchCriteria.Series.TvdbId != 0) - { - parameters.Tvdb = string.Format("{0}", searchCriteria.Series.TvdbId); - return true; - } - if (searchCriteria.Series.TvRageId != 0) - { - parameters.Tvrage = string.Format("{0}", searchCriteria.Series.TvRageId); - return true; - } - // BTN is very neatly managed, so it's unlikely they map tvrage/tvdb wrongly. - return false; - } - - private IEnumerable GetPagedRequests(int maxPages, BroadcastheNetTorrentQuery parameters) - { - var builder = new JsonRpcRequestBuilder(Settings.BaseUrl) - .Call("getTorrents", Settings.ApiKey, parameters, PageSize, 0); - builder.SuppressHttpError = true; - - for (var page = 0; page < maxPages; page++) - { - builder.JsonParameters[3] = page * PageSize; - - yield return new IndexerRequest(builder.Build()); - } - } - } -} diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs deleted file mode 100644 index ba3d2f969..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetSettings.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetSettingsValidator : AbstractValidator - { - public BroadcastheNetSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - RuleFor(c => c.ApiKey).NotEmpty(); - } - } - - public class BroadcastheNetSettings : IProviderConfig - { - private static readonly BroadcastheNetSettingsValidator Validator = new BroadcastheNetSettingsValidator(); - - public BroadcastheNetSettings() - { - BaseUrl = "http://api.btnapps.net/"; - } - - [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] - public string BaseUrl { get; set; } - - [FieldDefinition(1, Label = "API Key")] - public string ApiKey { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrent.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrent.cs deleted file mode 100644 index fd33c3bac..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrent.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetTorrent - { - public string GroupName { get; set; } - public int GroupID { get; set; } - public int TorrentID { get; set; } - public int SeriesID { get; set; } - public string Series { get; set; } - public string SeriesBanner { get; set; } - public string SeriesPoster { get; set; } - public string YoutubeTrailer { get; set; } - public string Category { get; set; } - public int? Snatched { get; set; } - public int? Seeders { get; set; } - public int? Leechers { get; set; } - public string Source { get; set; } - public string Container { get; set; } - public string Codec { get; set; } - public string Resolution { get; set; } - public string Origin { get; set; } - public string ReleaseName { get; set; } - public long Size { get; set; } - public long Time { get; set; } - public int? TvdbID { get; set; } - public int? TvrageID { get; set; } - public string ImdbID { get; set; } - public string InfoHash { get; set; } - public string DownloadURL { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrentQuery.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrentQuery.cs deleted file mode 100644 index 1180f9b63..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrentQuery.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Newtonsoft.Json; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetTorrentQuery - { - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Id { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Category { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Name { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Search { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Codec { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Container { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Source { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Resolution { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Origin { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Hash { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Tvdb { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Tvrage { get; set; } - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Age { get; set; } - - public BroadcastheNetTorrentQuery Clone() - { - return MemberwiseClone() as BroadcastheNetTorrentQuery; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrents.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrents.cs deleted file mode 100644 index f9329e7ea..000000000 --- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetTorrents.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Indexers.BroadcastheNet -{ - public class BroadcastheNetTorrents - { - public Dictionary Torrents { get; set; } - public int Results { get; set; } - } -} diff --git a/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs b/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs deleted file mode 100644 index fc66a83f1..000000000 --- a/src/NzbDrone.Core/Indexers/Fanzub/Fanzub.cs +++ /dev/null @@ -1,30 +0,0 @@ -using NLog; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; - -namespace NzbDrone.Core.Indexers.Fanzub -{ - public class Fanzub : HttpIndexerBase - { - public override string Name => "Fanzub"; - - public override DownloadProtocol Protocol => DownloadProtocol.Usenet; - - public Fanzub(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new FanzubRequestGenerator() { Settings = Settings }; - } - - public override IParseIndexerResponse GetParser() - { - return new RssParser() { UseEnclosureUrl = true, UseEnclosureLength = true }; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs deleted file mode 100644 index 19585dad5..000000000 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.IndexerSearch.Definitions; - -namespace NzbDrone.Core.Indexers.Fanzub -{ - public class FanzubRequestGenerator : IIndexerRequestGenerator - { - private static readonly Regex RemoveCharactersRegex = new Regex(@"[!?`]", RegexOptions.Compiled); - - public FanzubSettings Settings { get; set; } - public int PageSize { get; set; } - - public FanzubRequestGenerator() - { - PageSize = 100; - } - - public virtual IndexerPageableRequestChain GetRecentRequests() - { - var pageableRequests = new IndexerPageableRequestChain(); - - pageableRequests.Add(GetPagedRequests(null)); - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) - { - var pageableRequests = new IndexerPageableRequestChain(); - - var searchTitles = searchCriteria.QueryTitles.SelectMany(v => GetTitleSearchStrings(v, searchCriteria.AbsoluteEpisodeNumber)).ToList(); - - pageableRequests.Add(GetPagedRequests(string.Join("|", searchTitles))); - - return pageableRequests; - } - - public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - private IEnumerable GetPagedRequests(string query) - { - var url = new StringBuilder(); - url.AppendFormat("{0}?cat=anime&max={1}", Settings.BaseUrl, PageSize); - - if (query.IsNotNullOrWhiteSpace()) - { - url.AppendFormat("&q={0}", query); - } - - yield return new IndexerRequest(url.ToString(), HttpAccept.Rss); - } - - private IEnumerable GetTitleSearchStrings(string title, int absoluteEpisodeNumber) - { - var formats = new[] { "{0}%20{1:00}", "{0}%20-%20{1:00}" }; - - return formats.Select(s => "\"" + string.Format(s, CleanTitle(title), absoluteEpisodeNumber) + "\""); - } - - private string CleanTitle(string title) - { - return RemoveCharactersRegex.Replace(title, ""); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs deleted file mode 100644 index 1f9f25028..000000000 --- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubSettings.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.Fanzub -{ - public class FanzubSettingsValidator : AbstractValidator - { - public FanzubSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - } - } - - public class FanzubSettings : IProviderConfig - { - private static readonly FanzubSettingsValidator Validator = new FanzubSettingsValidator(); - - public FanzubSettings() - { - BaseUrl = "http://fanzub.com/rss/"; - } - - [FieldDefinition(0, Label = "Rss URL", HelpText = "Enter to URL to an Fanzub compatible RSS feed")] - public string BaseUrl { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index dacb87490..84fef6bb8 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -21,21 +22,7 @@ namespace NzbDrone.Core.Indexers.HDBits public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - var queryBase = new TorrentQuery(); - if (TryAddSearchParameters(queryBase, searchCriteria)) - { - foreach (var episode in searchCriteria.Episodes) - { - var query = queryBase.Clone(); - - query.TvdbInfo.Season = episode.SeasonNumber; - query.TvdbInfo.Episode = episode.EpisodeNumber; - } - } - - return pageableRequests; + return new IndexerPageableRequestChain(); } public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria) @@ -45,69 +32,28 @@ namespace NzbDrone.Core.Indexers.HDBits public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - var query = new TorrentQuery(); - if (TryAddSearchParameters(query, searchCriteria)) - { - query.Search = string.Format("{0:yyyy}-{0:MM}-{0:dd}", searchCriteria.AirDate); - - pageableRequests.Add(GetRequest(query)); - } - - return pageableRequests; + return new IndexerPageableRequestChain(); } public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - var queryBase = new TorrentQuery(); - if (TryAddSearchParameters(queryBase, searchCriteria)) - { - foreach (var seasonNumber in searchCriteria.Episodes.Select(e => e.SeasonNumber).Distinct()) - { - var query = queryBase.Clone(); - - query.TvdbInfo.Season = seasonNumber; - - pageableRequests.Add(GetRequest(query)); - } - } - - return pageableRequests; + return new IndexerPageableRequestChain(); } public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) { - var pageableRequests = new IndexerPageableRequestChain(); - - var queryBase = new TorrentQuery(); - if (TryAddSearchParameters(queryBase, searchCriteria)) - { - foreach (var episode in searchCriteria.Episodes) - { - var query = queryBase.Clone(); - - query.TvdbInfo.Season = episode.SeasonNumber; - query.TvdbInfo.Episode = episode.EpisodeNumber; - - pageableRequests.Add(GetRequest(query)); - } - } - - return pageableRequests; + return new IndexerPageableRequestChain(); } - private bool TryAddSearchParameters(TorrentQuery query, SearchCriteriaBase searchCriteria) + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { - if (searchCriteria.Series.TvdbId != 0) - { - query.TvdbInfo = query.TvdbInfo ?? new TvdbInfo(); - query.TvdbInfo.Id = searchCriteria.Series.TvdbId; - return true; - } - return false; + + var pageableRequests = new IndexerPageableRequestChain(); + var queryBase = new TorrentQuery(); + var query = queryBase.Clone(); + query.ImdbInfo.Id = int.Parse(searchCriteria.Movie.ImdbId.Substring(2)); + pageableRequests.Add(GetRequest(query)); + return pageableRequests; } private IEnumerable GetRequest(TorrentQuery query) diff --git a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs index 99ad741ca..c912291fa 100644 --- a/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/HttpIndexerBase.cs @@ -111,6 +111,18 @@ namespace NzbDrone.Core.Indexers return FetchReleases(generator.GetSearchRequests(searchCriteria)); } + public override IList Fetch(MovieSearchCriteria searchCriteria) + { + if (!SupportsSearch) + { + return new List(); + } + + var generator = GetRequestGenerator(); + + return FetchReleases(generator.GetSearchRequests(searchCriteria)); + } + protected virtual IList FetchReleases(IndexerPageableRequestChain pageableRequestChain, bool isRecent = false) { var releases = new List(); diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 9f028b569..f83bc3162 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -17,5 +17,6 @@ namespace NzbDrone.Core.Indexers IList Fetch(DailyEpisodeSearchCriteria searchCriteria); IList Fetch(AnimeEpisodeSearchCriteria searchCriteria); IList Fetch(SpecialEpisodeSearchCriteria searchCriteria); + IList Fetch(MovieSearchCriteria searchCriteria); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs index 5ad2cc79e..f321dacd7 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs @@ -10,5 +10,6 @@ namespace NzbDrone.Core.Indexers IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria); IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria); IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria); + IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs index bf4d9e7b8..bd63b6f46 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers.IPTorrents return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 4e08e5aad..95fda4871 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -67,6 +67,7 @@ namespace NzbDrone.Core.Indexers public abstract IList Fetch(DailyEpisodeSearchCriteria searchCriteria); public abstract IList Fetch(AnimeEpisodeSearchCriteria searchCriteria); public abstract IList Fetch(SpecialEpisodeSearchCriteria searchCriteria); + public abstract IList Fetch(MovieSearchCriteria searchCriteria); protected virtual IList CleanupReleases(IEnumerable releases) { diff --git a/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs index 228b3e607..9caaa1685 100644 --- a/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/KickassTorrents/KickassTorrentsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -147,5 +148,10 @@ namespace NzbDrone.Core.Indexers.KickassTorrents { return query.Replace('+', ' '); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index bd75f0382..de939341b 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -39,14 +39,15 @@ namespace NzbDrone.Core.Indexers.Newznab { get { - yield return GetDefinition("Dognzb.cr", GetSettings("https://api.dognzb.cr")); + yield return GetDefinition("DOGnzb", GetSettings("https://api.dognzb.cr")); yield return GetDefinition("DrunkenSlug", GetSettings("https://api.drunkenslug.com")); + yield return GetDefinition("Nzb-Tortuga", GetSettings("https://www.nzb-tortuga.com")); yield return GetDefinition("Nzb.su", GetSettings("https://api.nzb.su")); yield return GetDefinition("NZBCat", GetSettings("https://nzb.cat")); - yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", 5010, 5030, 5040, 5045)); + yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws")); yield return GetDefinition("NZBgeek", GetSettings("https://api.nzbgeek.info")); yield return GetDefinition("nzbplanet.net", GetSettings("https://api.nzbplanet.net")); - yield return GetDefinition("Nzbs.org", GetSettings("http://nzbs.org", 5000)); + yield return GetDefinition("Nzbs.org", GetSettings("http://nzbs.org")); yield return GetDefinition("OZnzb.com", GetSettings("https://api.oznzb.com")); yield return GetDefinition("PFmonkey", GetSettings("https://www.pfmonkey.com")); yield return GetDefinition("SimplyNZBs", GetSettings("https://simplynzbs.com")); @@ -106,8 +107,8 @@ namespace NzbDrone.Core.Indexers.Newznab } if (capabilities.SupportedTvSearchParameters != null && - new[] { "q", "tvdbid", "rid" }.Any(v => capabilities.SupportedTvSearchParameters.Contains(v)) && - new[] { "season", "ep" }.All(v => capabilities.SupportedTvSearchParameters.Contains(v))) + new[] { "q", "imdbid" }.Any(v => capabilities.SupportedMovieSearchParameters.Contains(v)) && + new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParameters.Contains(v))) { return null; } diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs index 11e73da34..717d24a9f 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers.Newznab public int MaxPageSize { get; set; } public string[] SupportedSearchParameters { get; set; } public string[] SupportedTvSearchParameters { get; set; } + public string[] SupportedMovieSearchParameters { get; set; } public bool SupportsAggregateIdSearch { get; set; } public List Categories { get; set; } @@ -16,6 +17,7 @@ namespace NzbDrone.Core.Indexers.Newznab DefaultPageSize = 100; MaxPageSize = 100; SupportedSearchParameters = new[] { "q" }; + SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" }; SupportedTvSearchParameters = new[] { "q", "rid", "season", "ep" }; // This should remain 'rid' for older newznab installs. SupportsAggregateIdSearch = false; Categories = new List(); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs index 9cb004f67..35f9b677b 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilitiesProvider.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Indexers.Newznab public NewznabCapabilities GetCapabilities(NewznabSettings indexerSettings) { var key = indexerSettings.ToJson(); + _capabilitiesCache.Clear(); var capabilities = _capabilitiesCache.Get(key, () => FetchCapabilities(indexerSettings), TimeSpan.FromDays(7)); return capabilities; @@ -98,6 +99,16 @@ namespace NzbDrone.Core.Indexers.Newznab capabilities.SupportedTvSearchParameters = xmlTvSearch.Attribute("supportedParams").Value.Split(','); capabilities.SupportsAggregateIdSearch = true; } + var xmlMovieSearch = xmlSearching.Element("movie-search"); + if (xmlMovieSearch == null || xmlMovieSearch.Attribute("available").Value != "yes") + { + capabilities.SupportedMovieSearchParameters = null; + } + else if (xmlMovieSearch.Attribute("supportedParams") != null) + { + capabilities.SupportedMovieSearchParameters = xmlMovieSearch.Attribute("supportedParams").Value.Split(','); + capabilities.SupportsAggregateIdSearch = true; + } } var xmlCategories = xmlRoot.Element("categories"); diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 915603c15..7ccaba8d8 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -84,6 +85,17 @@ namespace NzbDrone.Core.Indexers.Newznab } } + private bool SupportsMovieSearch + { + get + { + var capabilities = _capabilitiesProvider.GetCapabilities(Settings); + + return capabilities.SupportedMovieSearchParameters != null && + capabilities.SupportedMovieSearchParameters.Contains("imdbid"); + } + } + private bool SupportsAggregatedIdSearch { get @@ -100,9 +112,28 @@ namespace NzbDrone.Core.Indexers.Newznab var capabilities = _capabilitiesProvider.GetCapabilities(Settings); - if (capabilities.SupportedTvSearchParameters != null) + if (capabilities.SupportedMovieSearchParameters != null) { - pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories.Concat(Settings.AnimeCategories), "tvsearch", "")); + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories.Concat(Settings.AnimeCategories), "movie", "")); + } + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + if (SupportsMovieSearch) + { + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "movie", + string.Format("&imdbid={0}", searchCriteria.Movie.ImdbId.Substring(2)))); //strip off the "tt" - VERY HACKY + } + else + { + //Let's try anyways with q parameter, worst case nothing found. + pageableRequests.Add(GetPagedRequests(MaxPages, Settings.Categories, "search", + string.Format("&q={0}", searchCriteria.Movie.Title))); } return pageableRequests; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs index 16c4dea9b..521b63fa6 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRssParser.cs @@ -48,9 +48,7 @@ namespace NzbDrone.Core.Indexers.Newznab protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) { releaseInfo = base.ProcessItem(item, releaseInfo); - - releaseInfo.TvdbId = GetTvdbId(item); - releaseInfo.TvRageId = GetTvRageId(item); + releaseInfo.ImdbId = GetImdbId(item); return releaseInfo; } @@ -114,27 +112,14 @@ namespace NzbDrone.Core.Indexers.Newznab return url; } - protected virtual int GetTvdbId(XElement item) + protected virtual int GetImdbId(XElement item) { - var tvdbIdString = TryGetNewznabAttribute(item, "tvdbid"); - int tvdbId; + var imdbIdString = TryGetNewznabAttribute(item, "imdb"); + int imdbId; - if (!tvdbIdString.IsNullOrWhiteSpace() && int.TryParse(tvdbIdString, out tvdbId)) + if (!imdbIdString.IsNullOrWhiteSpace() && int.TryParse(imdbIdString, out imdbId)) { - return tvdbId; - } - - return 0; - } - - protected virtual int GetTvRageId(XElement item) - { - var tvRageIdString = TryGetNewznabAttribute(item, "rageid"); - int tvRageId; - - if (!tvRageIdString.IsNullOrWhiteSpace() && int.TryParse(tvRageIdString, out tvRageId)) - { - return tvRageId; + return imdbId; } return 0; diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs index b33ef566d..125391f57 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FluentValidation; @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Indexers.Newznab public NewznabSettings() { - Categories = new[] { 5030, 5040 }; + Categories = new[] { 2000, 2010, 2020, 2030, 2035, 2040, 2045, 2050, 2060 }; AnimeCategories = Enumerable.Empty(); } diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs index b54f4576f..6eac44084 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -102,5 +103,10 @@ namespace NzbDrone.Core.Indexers.Nyaa { return query.Replace(' ', '+'); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } } } diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs index 17663e8bf..a0b81decd 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -91,7 +92,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs private IEnumerable GetPagedRequests(string query) { var url = new StringBuilder(); - url.AppendFormat("{0}?catid=19,20&user={1}&api={2}&eng=1&delay={3}", BaseUrl, Settings.Username, Settings.ApiKey, Settings.Delay); + url.AppendFormat("{0}?catid=15,16,17&user={1}&api={2}&eng=1&delay={3}", BaseUrl, Settings.Username, Settings.ApiKey, Settings.Delay); if (query.IsNotNullOrWhiteSpace()) { @@ -101,5 +102,15 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs yield return new IndexerRequest(url.ToString(), HttpAccept.Rss); } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests(string.Format("{0}", + searchCriteria.Movie.Title))); + + return pageableRequests; + } } } diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRssParser.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRssParser.cs index a5946e5ff..644fff382 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/OmgwtfnzbsRssParser.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs protected override string GetInfoUrl(XElement item) { //Todo: Me thinks I need to parse details to get this... - var match = Regex.Match(item.Description(), @"(?:\View NZB\:\<\/b\>\s\.+)(?:\""\starget)", + var match = Regex.Match(item.Description(), @"(?:\View NZB\:\<\/b\>\s\.+)(?:\"")", RegexOptions.IgnoreCase | RegexOptions.Compiled); if (match.Success) diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs new file mode 100644 index 000000000..301894f57 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcorn.cs @@ -0,0 +1,30 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcorn : HttpIndexerBase + { + public override string Name => "PassThePopcorn"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 50; + + public PassThePopcorn(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new PassThePopcornRequestGenerator() { Settings = Settings }; + } + + public override IParseIndexerResponse GetParser() + { + return new PassThePopcornParser(Settings); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs new file mode 100644 index 000000000..9d7c93ea8 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornApi.cs @@ -0,0 +1,59 @@ +using System; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class Director + { + public string Name { get; set; } + public string Id { get; set; } + } + + public class Torrent + { + public int Id { get; set; } + public string Quality { get; set; } + public string Source { get; set; } + public string Container { get; set; } + public string Codec { get; set; } + public string Resolution { get; set; } + public bool Scene { get; set; } + public string Size { get; set; } + public DateTime UploadTime { get; set; } + public string RemasterTitle { get; set; } + public string Snatched { get; set; } + public string Seeders { get; set; } + public string Leechers { get; set; } + public string ReleaseName { get; set; } + public bool Checked { get; set; } + public bool GoldenPopcorn { get; set; } + } + + public class Movie + { + public string GroupId { get; set; } + public string Title { get; set; } + public string Year { get; set; } + public string Cover { get; set; } + public List Tags { get; set; } + public List Directors { get; set; } + public string ImdbId { get; set; } + public int TotalLeechers { get; set; } + public int TotalSeeders { get; set; } + public int TotalSnatched { get; set; } + public long MaxSize { get; set; } + public string LastUploadTime { get; set; } + public List Torrents { get; set; } + } + + public class PassThePopcornResponse + { + public string TotalResults { get; set; } + public List Movies { get; set; } + public string Page { get; set; } + public string AuthKey { get; set; } + public string PassKey { get; set; } + } + +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs new file mode 100644 index 000000000..3fec7eaff --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornInfo.cs @@ -0,0 +1,15 @@ +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornInfo : TorrentInfo + { + public bool? Golden { get; set; } + public bool? Scene { get; set; } + public bool? Approved { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs new file mode 100644 index 000000000..ef1ad3909 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornParser.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; +using System; +using System.Linq; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornParser : IParseIndexerResponse + { + private readonly PassThePopcornSettings _settings; + + public PassThePopcornParser(PassThePopcornSettings settings) + { + _settings = settings; + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, + "Unexpected response status {0} code from API request", + indexerResponse.HttpResponse.StatusCode); + } + + var jsonResponse = JsonConvert.DeserializeObject(indexerResponse.Content); + + var responseData = jsonResponse.Movies; + if (responseData == null) + { + throw new IndexerException(indexerResponse, + "Indexer API call response missing result data"); + } + + foreach (var result in responseData) + { + foreach (var torrent in result.Torrents) + { + var id = torrent.Id; + var title = torrent.ReleaseName; + + if (torrent.GoldenPopcorn) + { + title = $"{title} 🍿"; + } + + if (torrent.Checked) + { + title = $"{title} ✔"; + } + + //if (IsPropertyExist(torrent, "RemasterTitle")) + //{ + // if (torrent.RemasterTitle != null) + // { + // title = $"{title} - {torrent.RemasterTitle}"; + // } + //} + + // Only add approved torrents + if (_settings.Approved && torrent.Checked) + { + torrentInfos.Add(new PassThePopcornInfo() + { + Guid = string.Format("PassThePopcorn-{0}", id), + Title = title, + Size = long.Parse(torrent.Size), + DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey), + InfoUrl = GetInfoUrl(result.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.UploadTime.ToUniversalTime(), + Golden = torrent.GoldenPopcorn, + Scene = torrent.Scene, + Approved = torrent.Checked + }); + } + // Add all torrents + else if (!_settings.Approved) + { + torrentInfos.Add(new PassThePopcornInfo() + { + Guid = string.Format("PassThePopcorn-{0}", id), + Title = title, + Size = long.Parse(torrent.Size), + DownloadUrl = GetDownloadUrl(id, jsonResponse.AuthKey, jsonResponse.PassKey), + InfoUrl = GetInfoUrl(result.GroupId, id), + Seeders = int.Parse(torrent.Seeders), + Peers = int.Parse(torrent.Leechers) + int.Parse(torrent.Seeders), + PublishDate = torrent.UploadTime.ToUniversalTime(), + Golden = torrent.GoldenPopcorn, + Scene = torrent.Scene, + Approved = torrent.Checked + }); + } + // Don't add any torrents + else if (_settings.Approved && !torrent.Checked) + { + continue; + } + } + } + + // prefer golden + // prefer scene + // require approval + return torrentInfos.OrderBy(o => ((dynamic)o).Golden ? 0 : 1).ThenBy(o => ((dynamic)o).Scene ? 0 : 1).ThenByDescending(o => ((dynamic)o).PublishDate).ToArray(); + } + + private string GetDownloadUrl(int torrentId, string authKey, string passKey) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("action", "download") + .AddQueryParam("id", torrentId) + .AddQueryParam("authkey", authKey) + .AddQueryParam("torrent_pass", passKey); + + return url.FullUri; + } + + private string GetInfoUrl(string groupId, int torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/torrents.php") + .AddQueryParam("id", groupId) + .AddQueryParam("torrentid", torrentId); + + return url.FullUri; + } + + //public static bool IsPropertyExist(dynamic torrents, string name) + //{ + // return torrents.GetType().GetProperty(name) != null; + //} + } +} diff --git a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs similarity index 64% rename from src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs rename to src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs index e7966dcba..773f18b79 100644 --- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs @@ -1,37 +1,37 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.IndexerSearch.Definitions; -namespace NzbDrone.Core.Indexers.BitMeTv +namespace NzbDrone.Core.Indexers.PassThePopcorn { - public class BitMeTvRequestGenerator : IIndexerRequestGenerator + public class PassThePopcornRequestGenerator : IIndexerRequestGenerator { - public BitMeTvSettings Settings { get; set; } - + public PassThePopcornSettings Settings { get; set; } + public virtual IndexerPageableRequestChain GetRecentRequests() { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRssRequests()); + pageableRequests.Add(GetRequest(null)); return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetRequest(searchCriteria.Movie.ImdbId)); + return pageableRequests; + } + public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); } - public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - - public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) - { - return new IndexerPageableRequestChain(); - } - public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); @@ -42,9 +42,19 @@ namespace NzbDrone.Core.Indexers.BitMeTv return new IndexerPageableRequestChain(); } - private IEnumerable GetRssRequests() + public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) { - var request = new IndexerRequest(string.Format("{0}/rss.php?uid={1}&passkey={2}", Settings.BaseUrl.Trim().TrimEnd('/'), Settings.UserId, Settings.RssPasskey), HttpAccept.Html); + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + private IEnumerable GetRequest(string searchParameters) + { + var request = new IndexerRequest(string.Format("{0}/torrents.php?json=noredirect&searchstr={1}", Settings.BaseUrl.Trim().TrimEnd('/'), searchParameters), HttpAccept.Json); foreach (var cookie in HttpHeader.ParseCookies(Settings.Cookie)) { diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs new file mode 100644 index 000000000..88d7e15b2 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornSettings.cs @@ -0,0 +1,52 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Indexers.PassThePopcorn +{ + public class PassThePopcornSettingsValidator : AbstractValidator + { + public PassThePopcornSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.Cookie).NotEmpty(); + + RuleFor(c => c.Cookie) + .Matches(@"__cfduid=[0-9a-f]{43}", RegexOptions.IgnoreCase) + .WithMessage("Wrong pattern") + .AsWarning(); + } + } + + public class PassThePopcornSettings : IProviderConfig + { + private static readonly PassThePopcornSettingsValidator Validator = new PassThePopcornSettingsValidator(); + + public PassThePopcornSettings() + { + BaseUrl = "https://passthepopcorn.me"; + } + + [FieldDefinition(0, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your cookie will be sent to that host.")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Cookie", HelpText = "PassThePopcorn uses a login cookie needed to access the API, you'll have to retrieve it via a browser.")] + public string Cookie { get; set; } + + [FieldDefinition(2, Type = FieldType.Checkbox, Label = "Prefer Golden", HelpText = "Favors Golden Popcorn-releases over all other releases.")] + public bool Golden { get; set; } + + [FieldDefinition(3, Type = FieldType.Checkbox, Label = "Prefer Scene", HelpText = "Favors scene-releases over non-scene releases.")] + public bool Scene { get; set; } + + [FieldDefinition(4, Type = FieldType.Checkbox, Label = "Require Approved", HelpText = "Require staff-approval for releases to be accepted.")] + public bool Approved { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs index 3b43e0f35..b3cb1d9d8 100644 --- a/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Rarbg/RarbgRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -101,7 +102,7 @@ namespace NzbDrone.Core.Indexers.Rarbg requestBuilder.AddQueryParam("ranked", "0"); } - requestBuilder.AddQueryParam("category", "18;41"); + requestBuilder.AddQueryParam("category", "tv"); requestBuilder.AddQueryParam("limit", "100"); requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); requestBuilder.AddQueryParam("format", "json_extended"); @@ -109,5 +110,48 @@ namespace NzbDrone.Core.Indexers.Rarbg yield return new IndexerRequest(requestBuilder.Build()); } + + private IEnumerable GetMovieRequest(MovieSearchCriteria searchCriteria) + { + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl) + .Resource("/pubapi_v2.php") + .Accept(HttpAccept.Json); + + if (Settings.CaptchaToken.IsNotNullOrWhiteSpace()) + { + requestBuilder.UseSimplifiedUserAgent = true; + requestBuilder.SetCookie("cf_clearance", Settings.CaptchaToken); + } + + requestBuilder.AddQueryParam("mode", "search"); + + requestBuilder.AddQueryParam("search_imdb", searchCriteria.Movie.ImdbId); + + if (!Settings.RankedOnly) + { + requestBuilder.AddQueryParam("ranked", "0"); + } + + requestBuilder.AddQueryParam("category", "movies"); + requestBuilder.AddQueryParam("limit", "100"); + requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); + requestBuilder.AddQueryParam("format", "json_extended"); + requestBuilder.AddQueryParam("app_id", "Sonarr"); + + yield return new IndexerRequest(requestBuilder.Build()); + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + + + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetMovieRequest(searchCriteria)); + + return pageableRequests; + + + } } } diff --git a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs index 2ae5d4ed4..f9de0d54c 100644 --- a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common.Http; +using System; +using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; namespace NzbDrone.Core.Indexers @@ -22,6 +23,11 @@ namespace NzbDrone.Core.Indexers return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + throw new NotImplementedException(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotato.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotato.cs new file mode 100644 index 000000000..2eeb09f37 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotato.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Http.CloudFlare; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.TorrentPotato +{ + public class TorrentPotato : HttpIndexerBase + { + public override string Name => "TorrentPotato"; + + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override TimeSpan RateLimit => TimeSpan.FromSeconds(2); + + public TorrentPotato(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { + + } + + public override IEnumerable DefaultDefinitions + { + get + { + yield return GetDefinition("Jackett", new TorrentPotatoSettings { BaseUrl = "http://localhost:9117/potato/YOURINDEXER"}); + } + } + + private IndexerDefinition GetDefinition(string name, TorrentPotatoSettings settings) + { + return new IndexerDefinition + { + EnableRss = false, + EnableSearch = false, + Name = name, + Implementation = GetType().Name, + Settings = settings, + Protocol = DownloadProtocol.Torrent, + SupportsRss = SupportsRss, + SupportsSearch = SupportsSearch + }; + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new TorrentPotatoRequestGenerator() { Settings = Settings }; + } + + public override IParseIndexerResponse GetParser() + { + return new TorrentPotatoParser(); + } + + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoParser.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoParser.cs new file mode 100644 index 000000000..fdaf7bb9d --- /dev/null +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoParser.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Net; +using System.Text.RegularExpressions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Indexers.TorrentPotato +{ + public class TorrentPotatoParser : IParseIndexerResponse + { + private static readonly Regex RegexGuid = new Regex(@"^magnet:\?xt=urn:btih:([a-f0-9]+)", RegexOptions.Compiled); + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var results = new List(); + + switch (indexerResponse.HttpResponse.StatusCode) + { + default: + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, "Indexer API call returned an unexpected StatusCode [{0}]", indexerResponse.HttpResponse.StatusCode); + } + break; + } + + var jsonResponse = new HttpResponse(indexerResponse.HttpResponse); + + foreach (var torrent in jsonResponse.Resource.results) + { + var torrentInfo = new TorrentInfo(); + + torrentInfo.Guid = GetGuid(torrent); + torrentInfo.Title = torrent.release_name; + torrentInfo.Size = (long)torrent.size*1000*1000; + torrentInfo.DownloadUrl = torrent.download_url; + torrentInfo.InfoUrl = torrent.details_url; + torrentInfo.PublishDate = new System.DateTime(); + torrentInfo.Seeders = torrent.seeders; + torrentInfo.Peers = torrent.leechers + torrent.seeders; + torrentInfo.Freeleech = torrent.freeleech; + torrentInfo.PublishDate = torrent.publishdate.ToUniversalTime(); + + results.Add(torrentInfo); + } + + return results; + } + + private string GetGuid(Result torrent) + { + var match = RegexGuid.Match(torrent.download_url); + + if (match.Success) + { + return string.Format("potato-{0}", match.Groups[1].Value); + } + else + { + return string.Format("potato-{0}", torrent.download_url); + } + } + + } +} diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs new file mode 100644 index 000000000..808a40b89 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.Indexers.TorrentPotato +{ + public class TorrentPotatoRequestGenerator : IIndexerRequestGenerator + { + + public TorrentPotatoSettings Settings { get; set; } + + public TorrentPotatoRequestGenerator() + { + + } + + public virtual IndexerPageableRequestChain GetRecentRequests() + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests("list", null, null)); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests("search", searchCriteria.Series.TvdbId, "S{0:00}E{1:00}", searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber)); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests("search", searchCriteria.Series.TvdbId, "S{0:00}", searchCriteria.SeasonNumber)); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetPagedRequests("search", searchCriteria.Series.TvdbId, "\"{0:yyyy MM dd}\"", searchCriteria.AirDate)); + + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + foreach (var queryTitle in searchCriteria.EpisodeQueryTitles) + { + var query = queryTitle.Replace('+', ' '); + query = System.Web.HttpUtility.UrlEncode(query); + + pageableRequests.Add(GetPagedRequests("search", searchCriteria.Series.TvdbId, query)); + } + + return pageableRequests; + } + + private IEnumerable GetPagedRequests(string mode, int? tvdbId, string query, params object[] args) + { + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl) + .Accept(HttpAccept.Json); + + requestBuilder.AddQueryParam("passkey", Settings.Passkey); + if (!string.IsNullOrWhiteSpace(Settings.User)) + { + requestBuilder.AddQueryParam("user", Settings.User); + } + else + { + requestBuilder.AddQueryParam("user", ""); + } + + requestBuilder.AddQueryParam("search", "the"); + + yield return new IndexerRequest(requestBuilder.Build()); + } + + private IEnumerable GetMovieRequest(MovieSearchCriteria searchCriteria) + { + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl) + .Accept(HttpAccept.Json); + + requestBuilder.AddQueryParam("passkey", Settings.Passkey); + + if (!string.IsNullOrWhiteSpace(Settings.User)) + { + requestBuilder.AddQueryParam("user", Settings.User); + } + else + { + requestBuilder.AddQueryParam("user", ""); + } + + if (searchCriteria.Movie.ImdbId.IsNotNullOrWhiteSpace()) + { + requestBuilder.AddQueryParam("imdbid", searchCriteria.Movie.ImdbId); + } + else + { + requestBuilder.AddQueryParam("search", searchCriteria.Movie.Title); + } + + yield return new IndexerRequest(requestBuilder.Build()); + } + + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + + + var pageableRequests = new IndexerPageableRequestChain(); + + pageableRequests.Add(GetMovieRequest(searchCriteria)); + + return pageableRequests; + + + } + } +} diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoResponse.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoResponse.cs new file mode 100644 index 000000000..b0551ca67 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoResponse.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Indexers.TorrentPotato +{ + + public class TorrentPotatoResponse + { + public Result[] results { get; set; } + public int total_results { get; set; } + } + + public class Result + { + public string release_name { get; set; } + public string torrent_id { get; set; } + public string details_url { get; set; } + public string download_url { get; set; } + public bool freeleech { get; set; } + public string type { get; set; } + public int size { get; set; } + public int leechers { get; set; } + public int seeders { get; set; } + public DateTime publishdate { get; set; } + } + +} diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoSettings.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoSettings.cs new file mode 100644 index 000000000..14c91e2d0 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoSettings.cs @@ -0,0 +1,38 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.TorrentPotato +{ + public class TorrentPotatoSettingsValidator : AbstractValidator + { + public TorrentPotatoSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + } + } + + public class TorrentPotatoSettings : IProviderConfig + { + private static readonly TorrentPotatoSettingsValidator Validator = new TorrentPotatoSettingsValidator(); + + public TorrentPotatoSettings() + { + BaseUrl = "http://127.0.0.1"; + } + + [FieldDefinition(0, Label = "API URL", HelpText = "URL to TorrentPotato api.")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "Username", HelpText = "The username you use at your indexer.")] + public string User { get; set; } + + [FieldDefinition(2, Label = "Passkey", HelpText = "The password you use at your Indexer,")] + public string Passkey { get; set; } + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs index a0bf58cbc..1a77709cd 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; @@ -23,6 +24,11 @@ namespace NzbDrone.Core.Indexers.TorrentRss return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public virtual IndexerPageableRequestChain GetSearchRequests(SeasonSearchCriteria searchCriteria) { return new IndexerPageableRequestChain(); diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs b/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs deleted file mode 100644 index 5c7620a1a..000000000 --- a/src/NzbDrone.Core/Indexers/Torrentleech/Torrentleech.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Parser; -using NLog; - -namespace NzbDrone.Core.Indexers.Torrentleech -{ - public class Torrentleech : HttpIndexerBase - { - public override string Name => "TorrentLeech"; - - public override DownloadProtocol Protocol => DownloadProtocol.Torrent; - public override bool SupportsSearch => false; - public override int PageSize => 0; - - public Torrentleech(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(httpClient, indexerStatusService, configService, parsingService, logger) - { - - } - - public override IIndexerRequestGenerator GetRequestGenerator() - { - return new TorrentleechRequestGenerator() { Settings = Settings }; - } - - public override IParseIndexerResponse GetParser() - { - return new TorrentRssParser() { UseGuidInfoUrl = true, ParseSeedersInDescription = true }; - } - } -} diff --git a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs deleted file mode 100644 index 957bfc3ed..000000000 --- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechSettings.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FluentValidation; -using NzbDrone.Core.Annotations; -using NzbDrone.Core.ThingiProvider; -using NzbDrone.Core.Validation; - -namespace NzbDrone.Core.Indexers.Torrentleech -{ - public class TorrentleechSettingsValidator : AbstractValidator - { - public TorrentleechSettingsValidator() - { - RuleFor(c => c.BaseUrl).ValidRootUrl(); - RuleFor(c => c.ApiKey).NotEmpty(); - } - } - - public class TorrentleechSettings : IProviderConfig - { - private static readonly TorrentleechSettingsValidator Validator = new TorrentleechSettingsValidator(); - - public TorrentleechSettings() - { - BaseUrl = "http://rss.torrentleech.org"; - } - - [FieldDefinition(0, Label = "Website URL")] - public string BaseUrl { get; set; } - - [FieldDefinition(1, Label = "API Key")] - public string ApiKey { get; set; } - - public NzbDroneValidationResult Validate() - { - return new NzbDroneValidationResult(Validator.Validate(this)); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 8d2649c2d..b17573758 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -7,7 +7,9 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Newznab; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Torznab @@ -95,8 +97,7 @@ namespace NzbDrone.Core.Indexers.Torznab } if (capabilities.SupportedTvSearchParameters != null && - new[] { "q", "tvdbid", "rid" }.Any(v => capabilities.SupportedTvSearchParameters.Contains(v)) && - new[] { "season", "ep" }.All(v => capabilities.SupportedTvSearchParameters.Contains(v))) + new[] { "q", "imdbid" }.Any(v => capabilities.SupportedTvSearchParameters.Contains(v))) { return null; } @@ -110,5 +111,6 @@ namespace NzbDrone.Core.Indexers.Torznab return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details"); } } + } } diff --git a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs index 253386963..27ad87129 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/TorznabRssParser.cs @@ -39,10 +39,7 @@ namespace NzbDrone.Core.Indexers.Torznab protected override ReleaseInfo ProcessItem(XElement item, ReleaseInfo releaseInfo) { var torrentInfo = base.ProcessItem(item, releaseInfo) as TorrentInfo; - - torrentInfo.TvdbId = GetTvdbId(item); - torrentInfo.TvRageId = GetTvRageId(item); - + torrentInfo.ImdbId = int.Parse(GetImdbId(item).Substring(2)); return torrentInfo; } @@ -100,31 +97,12 @@ namespace NzbDrone.Core.Indexers.Torznab return url; } - protected virtual int GetTvdbId(XElement item) + protected virtual string GetImdbId(XElement item) { - var tvdbIdString = TryGetTorznabAttribute(item, "tvdbid"); - int tvdbId; - - if (!tvdbIdString.IsNullOrWhiteSpace() && int.TryParse(tvdbIdString, out tvdbId)) - { - return tvdbId; - } - - return 0; + var imdbIdString = TryGetTorznabAttribute(item, "imdbid"); + return (!imdbIdString.IsNullOrWhiteSpace() ? imdbIdString.Substring(2) : null); } - protected virtual int GetTvRageId(XElement item) - { - var tvRageIdString = TryGetTorznabAttribute(item, "rageid"); - int tvRageId; - - if (!tvRageIdString.IsNullOrWhiteSpace() && int.TryParse(tvRageIdString, out tvRageId)) - { - return tvRageId; - } - - return 0; - } protected override string GetInfoHash(XElement item) { return TryGetTorznabAttribute(item, "infohash"); diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index 571a85288..eab281ee8 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -1,7 +1,11 @@ -using NLog; +using System; +using System.Collections.Generic; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Wombles @@ -20,7 +24,7 @@ namespace NzbDrone.Core.Indexers.Wombles public override IIndexerRequestGenerator GetRequestGenerator() { - return new RssIndexerRequestGenerator("http://newshost.co.za/rss/?sec=TV&fr=false"); + return new RssIndexerRequestGenerator("http://newshost.co.za/rss/?sec=Movies&fr=false"); } public Wombles(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) diff --git a/src/NzbDrone.Core/Jobs/ScheduledTask.cs b/src/NzbDrone.Core/Jobs/ScheduledTask.cs index 5d842696d..a91faf3d1 100644 --- a/src/NzbDrone.Core/Jobs/ScheduledTask.cs +++ b/src/NzbDrone.Core/Jobs/ScheduledTask.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Jobs public class ScheduledTask : ModelBase { public string TypeName { get; set; } - public int Interval { get; set; } + public double Interval { get; set; } public DateTime LastExecution { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 3ad7b909a..33ba087b4 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Jobs { var defaultTasks = new[] { - new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, + new ScheduledTask{ Interval = 0.25f, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, diff --git a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs index deb2b35a5..048d04068 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoverService.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoverService.cs @@ -22,6 +22,8 @@ namespace NzbDrone.Core.MediaCover public class MediaCoverService : IHandleAsync, + IHandleAsync, + IHandleAsync, IHandleAsync, IMapCoversToLocal { @@ -83,6 +85,8 @@ namespace NzbDrone.Core.MediaCover return Path.Combine(_coverRootFolder, seriesId.ToString()); } + + private void EnsureCovers(Series series) { foreach (var cover in series.Images) @@ -110,6 +114,33 @@ namespace NzbDrone.Core.MediaCover } } + private void EnsureCovers(Movie movie) + { + foreach (var cover in movie.Images) + { + var fileName = GetCoverPath(movie.Id, cover.CoverType); + var alreadyExists = false; + try + { + alreadyExists = _coverExistsSpecification.AlreadyExists(cover.Url, fileName); + if (!alreadyExists) + { + DownloadCover(movie, cover); + } + } + catch (WebException e) + { + _logger.Warn(string.Format("Couldn't download media cover for {0}. {1}", movie, e.Message)); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't download media cover for " + movie); + } + + EnsureResizedCovers(movie, cover, !alreadyExists); + } + } + private void DownloadCover(Series series, MediaCover cover) { var fileName = GetCoverPath(series.Id, cover.CoverType); @@ -118,6 +149,14 @@ namespace NzbDrone.Core.MediaCover _httpClient.DownloadFile(cover.Url, fileName); } + private void DownloadCover(Movie series, MediaCover cover) + { + var fileName = GetCoverPath(series.Id, cover.CoverType); + + _logger.Info("Downloading {0} for {1} {2}", cover.CoverType, series, cover.Url); + _httpClient.DownloadFile(cover.Url, fileName); + } + private void EnsureResizedCovers(Series series, MediaCover cover, bool forceResize) { int[] heights; @@ -163,12 +202,69 @@ namespace NzbDrone.Core.MediaCover } } + private void EnsureResizedCovers(Movie series, MediaCover cover, bool forceResize) + { + int[] heights; + + switch (cover.CoverType) + { + default: + return; + + case MediaCoverTypes.Poster: + case MediaCoverTypes.Headshot: + heights = new[] { 500, 250 }; + break; + + case MediaCoverTypes.Banner: + heights = new[] { 70, 35 }; + break; + + case MediaCoverTypes.Fanart: + case MediaCoverTypes.Screenshot: + heights = new[] { 360, 180 }; + break; + } + + foreach (var height in heights) + { + var mainFileName = GetCoverPath(series.Id, cover.CoverType); + var resizeFileName = GetCoverPath(series.Id, cover.CoverType, height); + + if (forceResize || !_diskProvider.FileExists(resizeFileName) || _diskProvider.GetFileSize(resizeFileName) == 0) + { + _logger.Debug("Resizing {0}-{1} for {2}", cover.CoverType, height, series); + + try + { + _resizer.Resize(mainFileName, resizeFileName, height); + } + catch + { + _logger.Debug("Couldn't resize media cover {0}-{1} for {2}, using full size image instead.", cover.CoverType, height, series); + } + } + } + } + public void HandleAsync(SeriesUpdatedEvent message) { EnsureCovers(message.Series); _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Series)); } + public void HandleAsync(MovieUpdatedEvent message) + { + EnsureCovers(message.Movie); + _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Movie)); + } + + public void HandleAsync(MovieAddedEvent message) + { + EnsureCovers(message.Movie); + _eventAggregator.PublishEvent(new MediaCoversUpdatedEvent(message.Movie)); + } + public void HandleAsync(SeriesDeletedEvent message) { var path = GetSeriesCoverPath(message.Series.Id); diff --git a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs index 7335f7f9b..2f56e7cb0 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCoversUpdatedEvent.cs @@ -7,9 +7,16 @@ namespace NzbDrone.Core.MediaCover { public Series Series { get; set; } + public Movie Movie { get; set; } + public MediaCoversUpdatedEvent(Series series) { Series = series; } + + public MediaCoversUpdatedEvent(Movie movie) + { + Movie = movie; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedMovieScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedMovieScanCommand.cs new file mode 100644 index 000000000..69e1bb34d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedMovieScanCommand.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class DownloadedMovieScanCommand : Command + { + public override bool SendUpdatesToClient => SendUpdates; + + public bool SendUpdates { get; set; } + + // Properties used by third-party apps, do not modify. + public string Path { get; set; } + public string DownloadClientId { get; set; } + public ImportMode ImportMode { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs index e0dc34e10..5cbbe7dfb 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.MediaFiles.Commands diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieCommand.cs new file mode 100644 index 000000000..012a835f8 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieCommand.cs @@ -0,0 +1,19 @@ +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RenameMovieCommand : Command + { + public List MovieIds { get; set; } + + public override bool SendUpdatesToClient => true; + + public RenameMovieCommand() + { + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieFilesCommand.cs new file mode 100644 index 000000000..d2781e3ab --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameMovieFilesCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RenameMovieFilesCommand : Command + { + public int MovieId { get; set; } + public List Files { get; set; } + + public override bool SendUpdatesToClient => true; + + public RenameMovieFilesCommand() + { + } + + public RenameMovieFilesCommand(int movieId, List files) + { + MovieId = movieId; + Files = files; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs new file mode 100644 index 000000000..3671aa6af --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.MediaFiles.Commands +{ + public class RescanMovieCommand : Command + { + public int? MovieId { get; set; } + + public override bool SendUpdatesToClient => true; + + public RescanMovieCommand() + { + } + + public RescanMovieCommand(int movieId) + { + MovieId = movieId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 84a75e8e6..1c37adb82 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.MediaFiles public interface IDiskScanService { void Scan(Series series); + void Scan(Movie movie); string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true); List FilterFiles(Series series, IEnumerable files); @@ -30,33 +31,41 @@ namespace NzbDrone.Core.MediaFiles public class DiskScanService : IDiskScanService, IHandle, + IHandle, + IExecute, IExecute { private readonly IDiskProvider _diskProvider; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly IImportApprovedMovie _importApprovedMovies; private readonly IConfigService _configService; private readonly ISeriesService _seriesService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; private readonly IEventAggregator _eventAggregator; + private readonly IMovieService _movieService; private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, IMakeImportDecision importDecisionMaker, IImportApprovedEpisodes importApprovedEpisodes, + IImportApprovedMovie importApprovedMovies, IConfigService configService, ISeriesService seriesService, IMediaFileTableCleanupService mediaFileTableCleanupService, IEventAggregator eventAggregator, + IMovieService movieService, Logger logger) { _diskProvider = diskProvider; _importDecisionMaker = importDecisionMaker; _importApprovedEpisodes = importApprovedEpisodes; + _importApprovedMovies = importApprovedMovies; _configService = configService; _seriesService = seriesService; _mediaFileTableCleanupService = mediaFileTableCleanupService; _eventAggregator = eventAggregator; + _movieService = movieService; _logger = logger; } @@ -121,6 +130,65 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); } + public void Scan(Movie movie) + { + var rootFolder = _diskProvider.GetParentFolder(movie.Path); + + if (!_diskProvider.FolderExists(rootFolder)) + { + _logger.Warn("Series' root folder ({0}) doesn't exist.", rootFolder); + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderDoesNotExist)); + return; + } + + if (_diskProvider.GetDirectories(rootFolder).Empty()) + { + _logger.Warn("Series' root folder ({0}) is empty.", rootFolder); + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderIsEmpty)); + return; + } + + _logger.ProgressInfo("Scanning disk for {0}", movie.Title); + + if (!_diskProvider.FolderExists(movie.Path)) + { + if (_configService.CreateEmptySeriesFolders && + _diskProvider.FolderExists(rootFolder)) + { + _logger.Debug("Creating missing series folder: {0}", movie.Path); + _diskProvider.CreateFolder(movie.Path); + SetPermissions(movie.Path); + } + else + { + _logger.Debug("Series folder doesn't exist: {0}", movie.Path); + } + + _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.MovieFolderDoesNotExist)); + return; + } + + var videoFilesStopwatch = Stopwatch.StartNew(); + var mediaFileList = FilterFiles(movie, GetVideoFiles(movie.Path)).ToList(); + + videoFilesStopwatch.Stop(); + _logger.Trace("Finished getting episode files for: {0} [{1}]", movie, videoFilesStopwatch.Elapsed); + + _logger.Debug("{0} Cleaning up media files in DB", movie); + _mediaFileTableCleanupService.Clean(movie, mediaFileList); + + var decisionsStopwatch = Stopwatch.StartNew(); + var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, movie, true); + decisionsStopwatch.Stop(); + _logger.Trace("Import decisions complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed); + + //_importApprovedEpisodes.Import(decisions, false); + _importApprovedMovies.Import(decisions, false); + + _logger.Info("Completed scanning disk for {0}", movie.Title); + _eventAggregator.PublishEvent(new MovieScannedEvent(movie)); + } + public string[] GetVideoFiles(string path, bool allDirectories = true) { _logger.Debug("Scanning '{0}' for video files", path); @@ -156,6 +224,13 @@ namespace NzbDrone.Core.MediaFiles .ToList(); } + public List FilterFiles(Movie movie, IEnumerable files) + { + return files.Where(file => !ExcludedSubFoldersRegex.IsMatch(movie.Path.GetRelativePath(file))) + .Where(file => !ExcludedFilesRegex.IsMatch(Path.GetFileName(file))) + .ToList(); + } + private void SetPermissions(string path) { if (!_configService.SetPermissionsLinux) @@ -182,6 +257,28 @@ namespace NzbDrone.Core.MediaFiles Scan(message.Series); } + public void Handle(MovieUpdatedEvent message) + { + Scan(message.Movie); + } + + public void Execute(RescanMovieCommand message) + { + if (message.MovieId.HasValue) + { + var series = _movieService.GetMovie(message.MovieId.Value); + } + else + { + var allMovies = _movieService.GetAllMovies(); + + foreach (var movie in allMovies) + { + Scan(movie); + } + } + } + public void Execute(RescanSeriesCommand message) { if (message.SeriesId.HasValue) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs new file mode 100644 index 000000000..5f9c5362f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs @@ -0,0 +1,107 @@ +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Commands; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles +{ + public class DownloadedMovieCommandService : IExecute + { + private readonly IDownloadedMovieImportService _downloadedMovieImportService; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public DownloadedMovieCommandService(IDownloadedMovieImportService downloadedMovieImportService, + ITrackedDownloadService trackedDownloadService, + IDiskProvider diskProvider, + IConfigService configService, + Logger logger) + { + _downloadedMovieImportService = downloadedMovieImportService; + _trackedDownloadService = trackedDownloadService; + _diskProvider = diskProvider; + _configService = configService; + _logger = logger; + } + + private List ProcessDroneFactoryFolder() + { + var downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; + + if (string.IsNullOrEmpty(downloadedEpisodesFolder)) + { + _logger.Trace("Drone Factory folder is not configured"); + return new List(); + } + + if (!_diskProvider.FolderExists(downloadedEpisodesFolder)) + { + _logger.Warn("Drone Factory folder [{0}] doesn't exist.", downloadedEpisodesFolder); + return new List(); + } + + return _downloadedMovieImportService.ProcessRootFolder(new DirectoryInfo(downloadedEpisodesFolder)); + } + + private List ProcessPath(DownloadedMovieScanCommand message) + { + if (!_diskProvider.FolderExists(message.Path) && !_diskProvider.FileExists(message.Path)) + { + _logger.Warn("Folder/File specified for import scan [{0}] doesn't exist.", message.Path); + return new List(); + } + + if (message.DownloadClientId.IsNotNullOrWhiteSpace()) + { + var trackedDownload = _trackedDownloadService.Find(message.DownloadClientId); + + if (trackedDownload != null) + { + _logger.Debug("External directory scan request for known download {0}. [{1}]", message.DownloadClientId, message.Path); + + return _downloadedMovieImportService.ProcessPath(message.Path, message.ImportMode, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem); + } + else + { + _logger.Warn("External directory scan request for unknown download {0}, attempting normal import. [{1}]", message.DownloadClientId, message.Path); + + return _downloadedMovieImportService.ProcessPath(message.Path, message.ImportMode); + } + } + + return _downloadedMovieImportService.ProcessPath(message.Path, message.ImportMode); + } + + public void Execute(DownloadedMovieScanCommand message) + { + List importResults; + + if (message.Path.IsNotNullOrWhiteSpace()) + { + importResults = ProcessPath(message); + } + else + { + importResults = ProcessDroneFactoryFolder(); + } + + if (importResults == null || importResults.All(v => v.Result != ImportResultType.Imported)) + { + // Atm we don't report it as a command failure, coz that would cause the download to be failed. + // Changing the message won't do a thing either, coz it will get set to 'Completed' a msec later. + //message.SetMessage("Failed to import"); + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedMovieImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedMovieImportService.cs new file mode 100644 index 000000000..1843bdf72 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DownloadedMovieImportService.cs @@ -0,0 +1,266 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.MediaFiles.Commands; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IDownloadedMovieImportService + { + List ProcessRootFolder(DirectoryInfo directoryInfo); + List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null); + bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie); + } + + public class DownloadedMovieImportService : IDownloadedMovieImportService + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + private readonly IMovieService _movieService; + private readonly IParsingService _parsingService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly IImportApprovedMovie _importApprovedMovie; + private readonly IDetectSample _detectSample; + private readonly Logger _logger; + + public DownloadedMovieImportService(IDiskProvider diskProvider, + IDiskScanService diskScanService, + IMovieService movieService, + IParsingService parsingService, + IMakeImportDecision importDecisionMaker, + IImportApprovedMovie importApprovedMovie, + IDetectSample detectSample, + Logger logger) + { + _diskProvider = diskProvider; + _diskScanService = diskScanService; + _movieService = movieService; + _parsingService = parsingService; + _importDecisionMaker = importDecisionMaker; + _importApprovedMovie = importApprovedMovie; + _detectSample = detectSample; + _logger = logger; + } + + public List ProcessRootFolder(DirectoryInfo directoryInfo) + { + var results = new List(); + + foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName)) + { + var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null); + results.AddRange(folderResults); + } + + foreach (var videoFile in _diskScanService.GetVideoFiles(directoryInfo.FullName, false)) + { + var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null); + results.AddRange(fileResults); + } + + return results; + } + + public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null) + { + if (_diskProvider.FolderExists(path)) + { + var directoryInfo = new DirectoryInfo(path); + + if (movie == null) + { + return ProcessFolder(directoryInfo, importMode, downloadClientItem); + } + + return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem); + } + + if (_diskProvider.FileExists(path)) + { + var fileInfo = new FileInfo(path); + + if (movie == null) + { + return ProcessFile(fileInfo, importMode, downloadClientItem); + } + + return ProcessFile(fileInfo, importMode, movie, downloadClientItem); + } + + _logger.Error("Import failed, path does not exist or is not accessible by Radarr: {0}", path); + return new List(); + } + + public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie) + { + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); + + foreach (var videoFile in videoFiles) + { + var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); + + if (episodeParseResult == null) + { + _logger.Warn("Unable to parse file on import: [{0}]", videoFile); + return false; + } + + var size = _diskProvider.GetFileSize(videoFile); + var quality = QualityParser.ParseQuality(videoFile); + + if (!_detectSample.IsSample(movie, quality, videoFile, size, episodeParseResult.IsPossibleSpecialEpisode)) + { + _logger.Warn("Non-sample file detected: [{0}]", videoFile); + return false; + } + } + + if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) + { + _logger.Warn("RAR file detected, will require manual cleanup"); + return false; + } + + return true; + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var movie = _parsingService.GetMovie(cleanedUpName); + + if (movie == null) + { + _logger.Debug("Unknown Movie {0}", cleanedUpName); + + return new List + { + UnknownMovieResult("Unknown Movie") + }; + } + + return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem); + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem) + { + if (_movieService.MoviePathExists(directoryInfo.FullName)) + { + _logger.Warn("Unable to process folder that is mapped to an existing show"); + return new List(); + } + + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var folderInfo = Parser.Parser.ParseMovieTitle(directoryInfo.Name); + + if (folderInfo != null) + { + _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); + } + + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + + if (downloadClientItem == null) + { + foreach (var videoFile in videoFiles) + { + if (_diskProvider.IsFileLocked(videoFile)) + { + return new List + { + FileIsLockedResult(videoFile) + }; + } + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), movie, folderInfo, true, false); + var importResults = _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode); + + if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && + importResults.Any(i => i.Result == ImportResultType.Imported) && + ShouldDeleteFolder(directoryInfo, movie)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(directoryInfo.FullName, true); + } + + return importResults; + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var movie = _parsingService.GetMovie(Path.GetFileNameWithoutExtension(fileInfo.Name)); + + if (movie == null) + { + _logger.Debug("Unknown Movie for file: {0}", fileInfo.Name); + + return new List + { + UnknownMovieResult(string.Format("Unknown Movie for file: {0}", fileInfo.Name), fileInfo.FullName) + }; + } + + return ProcessFile(fileInfo, importMode, movie, downloadClientItem); + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem) + { + if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) + { + _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); + + return new List + { + new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new Rejection("Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'") + }; + } + + if (downloadClientItem == null) + { + if (_diskProvider.IsFileLocked(fileInfo.FullName)) + { + return new List + { + FileIsLockedResult(fileInfo.FullName) + }; + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, movie, null, true, false); + + return _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode); + } + + private string GetCleanedUpFolderName(string folder) + { + folder = folder.Replace("_UNPACK_", "") + .Replace("_FAILED_", ""); + + return folder; + } + + private ImportResult FileIsLockedResult(string videoFile) + { + _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); + return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); + } + + private ImportResult UnknownMovieResult(string message, string videoFile = null) + { + var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; + + return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Movie")), message); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index b517cd76c..1785d4571 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IDetectSample { bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial); + bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial); } public class DetectSample : IDetectSample @@ -79,6 +80,57 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return false; } + public bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial) + { + if (isSpecial) + { + _logger.Debug("Special, skipping sample check"); + return false; + } + + var extension = Path.GetExtension(path); + + if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Skipping sample check for .flv file"); + return false; + } + + if (extension != null && extension.Equals(".strm", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Skipping sample check for .strm file"); + return false; + } + + try + { + var runTime = _videoFileInfoReader.GetRunTime(path); + var minimumRuntime = GetMinimumAllowedRuntime(movie); + + if (runTime.TotalMinutes.Equals(0)) + { + _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path); + return true; + } + + if (runTime.TotalSeconds < minimumRuntime) + { + _logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime); + return true; + } + } + + catch (DllNotFoundException) + { + _logger.Debug("Falling back to file size detection"); + + return CheckSize(size, quality); + } + + _logger.Debug("Runtime is over 90 seconds"); + return false; + } + private bool CheckSize(long size, QualityModel quality) { if (_largeSampleSizeQualities.Contains(quality.Quality)) @@ -99,6 +151,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return false; } + private int GetMinimumAllowedRuntime(Movie movie) + { + return 360; //6 minutes + } + private int GetMinimumAllowedRuntime(Series series) { //Webisodes - 90 seconds diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs index 86abb87b7..4dc6bcaf6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs @@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IImportDecisionEngineSpecification { Decision IsSatisfiedBy(LocalEpisode localEpisode); + + Decision IsSatisfiedBy(LocalMovie localMovie); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs new file mode 100644 index 000000000..1a2c812d6 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Download; +using NzbDrone.Core.Extras; + + +namespace NzbDrone.Core.MediaFiles.EpisodeImport +{ + public interface IImportApprovedMovie + { + List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto); + } + + public class ImportApprovedMovie : IImportApprovedMovie + { + private readonly IUpgradeMediaFiles _episodeFileUpgrader; + private readonly IMediaFileService _mediaFileService; + private readonly IExtraService _extraService; + private readonly IDiskProvider _diskProvider; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public ImportApprovedMovie(IUpgradeMediaFiles episodeFileUpgrader, + IMediaFileService mediaFileService, + IExtraService extraService, + IDiskProvider diskProvider, + IEventAggregator eventAggregator, + Logger logger) + { + _episodeFileUpgrader = episodeFileUpgrader; + _mediaFileService = mediaFileService; + _extraService = extraService; + _diskProvider = diskProvider; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) + { + _logger.Debug("Decisions: {0}", decisions.Count); + + var qualifiedImports = decisions.Where(c => c.Approved) + .GroupBy(c => c.LocalMovie.Movie.Id, (i, s) => s + .OrderByDescending(c => c.LocalMovie.Quality, new QualityModelComparer(s.First().LocalMovie.Movie.Profile)) + .ThenByDescending(c => c.LocalMovie.Size)) + .SelectMany(c => c) + .ToList(); + + var importResults = new List(); + + foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalMovie.Size) + .ThenByDescending(e => e.LocalMovie.Size)) + { + var localMovie = importDecision.LocalMovie; + var oldFiles = new List(); + + try + { + //check if already imported + if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie) + .Select(e => e.Id).Contains(localMovie.Movie.Id)) + { + importResults.Add(new ImportResult(importDecision, "Movie has already been imported")); + continue; + } + + var episodeFile = new MovieFile(); + episodeFile.DateAdded = DateTime.UtcNow; + episodeFile.MovieId = localMovie.Movie.Id; + episodeFile.Path = localMovie.Path.CleanFilePath(); + episodeFile.Size = _diskProvider.GetFileSize(localMovie.Path); + episodeFile.Quality = localMovie.Quality; + episodeFile.MediaInfo = localMovie.MediaInfo; + episodeFile.Movie = localMovie.Movie; + episodeFile.ReleaseGroup = localMovie.ParsedMovieInfo.ReleaseGroup; + episodeFile.Edition = localMovie.ParsedMovieInfo.Edition; + + bool copyOnly; + switch (importMode) + { + default: + case ImportMode.Auto: + copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly; + break; + case ImportMode.Move: + copyOnly = false; + break; + case ImportMode.Copy: + copyOnly = true; + break; + } + + if (newDownload) + { + episodeFile.SceneName = GetSceneName(downloadClientItem, localMovie); + + var moveResult = _episodeFileUpgrader.UpgradeMovieFile(episodeFile, localMovie, copyOnly); + oldFiles = moveResult.OldFiles; + } + else + { + episodeFile.RelativePath = localMovie.Movie.Path.GetRelativePath(episodeFile.Path); + } + + _mediaFileService.Add(episodeFile); + importResults.Add(new ImportResult(importDecision)); + + if (newDownload) + { + //_extraService.ImportExtraFiles(localMovie, episodeFile, copyOnly); TODO update for movie + } + + if (downloadClientItem != null) + { + _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); + } + else + { + _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload)); + } + + if (newDownload) + { + _eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, episodeFile, oldFiles)); + } + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't import episode " + localMovie); + importResults.Add(new ImportResult(importDecision, "Failed to import episode")); + } + } + + //Adding all the rejected decisions + importResults.AddRange(decisions.Where(c => !c.Approved) + .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); + + return importResults; + } + + private string GetSceneName(DownloadClientItem downloadClientItem, LocalMovie localMovie) + { + if (downloadClientItem != null) + { + var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title); + + var parsedTitle = Parser.Parser.ParseTitle(title); + + if (parsedTitle != null && !parsedTitle.FullSeason) + { + return title; + } + } + + var fileName = Path.GetFileNameWithoutExtension(localMovie.Path.CleanFilePath()); + + if (SceneChecker.IsSceneTitle(fileName)) + { + return fileName; + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs index 5e4e2ede2..406866b65 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportDecision { public LocalEpisode LocalEpisode { get; private set; } + public LocalMovie LocalMovie { get; private set; } public IEnumerable Rejections { get; private set; } public bool Approved => Rejections.Empty(); @@ -18,5 +19,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport LocalEpisode = localEpisode; Rejections = rejections.ToList(); } + + public ImportDecision(LocalMovie localMovie, params Rejection[] rejections) + { + LocalMovie = localMovie; + Rejections = rejections.ToList(); + //LocalMovie = new LocalMovie + //{ + // Quality = localMovie.Quality, + // ExistingFile = localMovie.ExistingFile, + // MediaInfo = localMovie.MediaInfo, + // ParsedMovieInfo = localMovie.ParsedMovieInfo, + // Path = localMovie.Path, + // Size = localMovie.Size + //}; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 8f03ca756..e1f095791 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -18,6 +18,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IMakeImportDecision { List GetImportDecisions(List videoFiles, Series series); + List GetImportDecisions(List videoFiles, Movie movie); + List GetImportDecisions(List videoFiles, Movie movie, bool shouldCheckQuality); + List GetImportDecisions(List videoFiles, Movie movie, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldCheckQuality); //TODO: Needs changing to ParsedMovieInfo!! List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); } @@ -29,6 +32,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport private readonly IDiskProvider _diskProvider; private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly IDetectSample _detectSample; + private readonly IQualityDefinitionService _qualitiesService; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, @@ -37,6 +41,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport IDiskProvider diskProvider, IVideoFileInfoReader videoFileInfoReader, IDetectSample detectSample, + IQualityDefinitionService qualitiesService, Logger logger) { _specifications = specifications; @@ -45,6 +50,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _diskProvider = diskProvider; _videoFileInfoReader = videoFileInfoReader; _detectSample = detectSample; + _qualitiesService = qualitiesService; _logger = logger; } @@ -53,6 +59,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return GetImportDecisions(videoFiles, series, null, false); } + public List GetImportDecisions(List videoFiles, Movie movie) + { + return GetImportDecisions(videoFiles, movie, null, true, false); + } + + public List GetImportDecisions(List videoFiles, Movie movie, bool shouldCheckQuality = false) + { + return GetImportDecisions(videoFiles, movie, null, true, shouldCheckQuality); + } + public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource) { var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series); @@ -70,6 +86,181 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return decisions; } + public List GetImportDecisions(List videoFiles, Movie movie, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldCheckQuality = false) + { + var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), movie); + + _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); + + var shouldUseFolderName = ShouldUseFolderName(videoFiles, movie, folderInfo); + var decisions = new List(); + + foreach (var file in newFiles) + { + decisions.AddIfNotNull(GetDecision(file, movie, folderInfo, sceneSource, shouldUseFolderName, shouldCheckQuality)); + } + + return decisions; + } + + private ImportDecision GetDecision(string file, Movie movie, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldUseFolderName, bool shouldCheckQuality = false) + { + ImportDecision decision = null; + + try + { + var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); + + if (localMovie != null) + { + localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie); + localMovie.Size = _diskProvider.GetFileSize(file); + + _logger.Debug("Size: {0}", localMovie.Size); + + //TODO: make it so media info doesn't ruin the import process of a new series + if (sceneSource) + { + localMovie.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + if (shouldCheckQuality) + { + var width = localMovie.MediaInfo.Width; + var current = localMovie.Quality; + var qualityName = current.Quality.Name.ToLower(); + QualityModel updated = null; + if (width > 1400) + { + if (qualityName.Contains("bluray")) + { + updated = new QualityModel(Quality.Bluray1080p); + } + + else if (qualityName.Contains("webdl")) + { + updated = new QualityModel(Quality.WEBDL1080p); + } + + else if (qualityName.Contains("hdtv")) + { + updated = new QualityModel(Quality.HDTV1080p); + } + + else + { + + var def = _qualitiesService.Get(Quality.HDTV1080p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.HDTV1080p); + } + def = _qualitiesService.Get(Quality.WEBDL1080p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.WEBDL1080p); + } + def = _qualitiesService.Get(Quality.Bluray1080p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.Bluray1080p); + } + if (updated == null) + { + updated = new QualityModel(Quality.Bluray1080p); + } + } + + } + else + if (width > 900) + { + if (qualityName.Contains("bluray")) + { + updated = new QualityModel(Quality.Bluray720p); + } + + else if (qualityName.Contains("webdl")) + { + updated = new QualityModel(Quality.WEBDL720p); + } + + else if (qualityName.Contains("hdtv")) + { + updated = new QualityModel(Quality.HDTV720p); + } + + else + { + + var def = _qualitiesService.Get(Quality.HDTV720p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.HDTV720p); + } + def = _qualitiesService.Get(Quality.WEBDL720p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.WEBDL720p); + } + def = _qualitiesService.Get(Quality.Bluray720p); + if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size) + { + updated = new QualityModel(Quality.Bluray720p); + } + if (updated == null) + { + updated = new QualityModel(Quality.Bluray720p); + } + + } + } + if (updated != null && updated != current) + { + updated.QualitySource = QualitySource.MediaInfo; + localMovie.Quality = updated; + } + } + + + + decision = GetDecision(localMovie); + } + else + { + decision = GetDecision(localMovie); + } + } + + else + { + localMovie = new LocalMovie(); + localMovie.Path = file; + + decision = new ImportDecision(localMovie, new Rejection("Unable to parse file")); + } + } + catch (Exception e) + { + _logger.Error(e, "Couldn't import file. " + file); + + var localMovie = new LocalMovie { Path = file }; + decision = new ImportDecision(localMovie, new Rejection("Unexpected error processing file")); + } + + //LocalMovie nullMovie = null; + + //decision = new ImportDecision(nullMovie, new Rejection("IMPLEMENTATION MISSING!!!")); + + return decision; + } + + private ImportDecision GetDecision(LocalMovie localMovie) + { + var reasons = _specifications.Select(c => EvaluateSpec(c, localMovie)) + .Where(c => c != null); + + return new ImportDecision(localMovie, reasons.ToArray()); + } + private ImportDecision GetDecision(string file, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName) { ImportDecision decision = null; @@ -128,6 +319,33 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return new ImportDecision(localEpisode, reasons.ToArray()); } + private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalMovie localMovie) + { + try + { + var result = spec.IsSatisfiedBy(localMovie); + + if (!result.Accepted) + { + return new Rejection(result.Reason); + } + } + catch (NotImplementedException e) + { + _logger.Warn(e, "Spec " + spec.ToString() + " currently does not implement evaluation for movies."); + return null; + } + catch (Exception e) + { + //e.Data.Add("report", remoteEpisode.Report.ToJson()); + //e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); + _logger.Error(e, "Couldn't evaluate decision on " + localMovie.Path); + return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message)); + } + + return null; + } + private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode) { try @@ -182,6 +400,51 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport }) == 1; } + private bool ShouldUseFolderName(List videoFiles, Movie movie, ParsedMovieInfo folderInfo) + { + if (folderInfo == null) + { + return false; + } + + //if (folderInfo.FullSeason) + //{ + // return false; + //} + + return videoFiles.Count(file => + { + var size = _diskProvider.GetFileSize(file); + var fileQuality = QualityParser.ParseQuality(file); + //var sample = null;//_detectSample.IsSample(movie, GetQuality(folderInfo, fileQuality, movie), file, size, folderInfo.IsPossibleSpecialEpisode); //Todo to this + + return true; + + //if (sample) + { + return false; + } + + if (SceneChecker.IsSceneTitle(Path.GetFileName(file))) + { + return false; + } + + return true; + }) == 1; + } + + private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie) + { + if (UseFolderQuality(folderInfo, fileQuality, movie)) + { + _logger.Debug("Using quality from folder: {0}", folderInfo.Quality); + return folderInfo.Quality; + } + + return fileQuality; + } + private QualityModel GetQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) { if (UseFolderQuality(folderInfo, fileQuality, series)) @@ -193,6 +456,31 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return fileQuality; } + private bool UseFolderQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.Quality.Quality == Quality.Unknown) + { + return false; + } + + if (fileQuality.QualitySource == QualitySource.Extension) + { + return true; + } + + if (new QualityModelComparer(movie.Profile).Compare(folderInfo.Quality, fileQuality) > 0) + { + return true; + } + + return false; + } + private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) { if (folderInfo == null) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index 4c9fecc7c..d9f089179 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -10,5 +10,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } public string DownloadId { get; set; } + public int MovieId { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index bd3954816..ecca2739c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -17,5 +17,6 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public QualityModel Quality { get; set; } public string DownloadId { get; set; } public IEnumerable Rejections { get; set; } + public Movie Movie { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index d85a2e119..d113d53b0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -30,11 +30,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual private readonly IDiskScanService _diskScanService; private readonly IMakeImportDecision _importDecisionMaker; private readonly ISeriesService _seriesService; + private readonly IMovieService _movieService; private readonly IEpisodeService _episodeService; private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly IImportApprovedEpisodes _importApprovedEpisodes; + private readonly IImportApprovedMovie _importApprovedMovie; private readonly ITrackedDownloadService _trackedDownloadService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IDownloadedMovieImportService _downloadedMovieImportService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -43,11 +46,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual IDiskScanService diskScanService, IMakeImportDecision importDecisionMaker, ISeriesService seriesService, + IMovieService movieService, IEpisodeService episodeService, IVideoFileInfoReader videoFileInfoReader, IImportApprovedEpisodes importApprovedEpisodes, + IImportApprovedMovie importApprovedMovie, ITrackedDownloadService trackedDownloadService, IDownloadedEpisodesImportService downloadedEpisodesImportService, + IDownloadedMovieImportService downloadedMovieImportService, IEventAggregator eventAggregator, Logger logger) { @@ -56,11 +62,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _diskScanService = diskScanService; _importDecisionMaker = importDecisionMaker; _seriesService = seriesService; + _movieService = movieService; _episodeService = episodeService; _videoFileInfoReader = videoFileInfoReader; _importApprovedEpisodes = importApprovedEpisodes; + _importApprovedMovie = importApprovedMovie; _trackedDownloadService = trackedDownloadService; _downloadedEpisodesImportService = downloadedEpisodesImportService; + _downloadedMovieImportService = downloadedMovieImportService; _eventAggregator = eventAggregator; _logger = logger; } @@ -126,62 +135,128 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual var relativeFile = folder.GetRelativePath(file); - var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + var movie = _parsingService.GetMovie(relativeFile.Split('\\', '/')[0]); - if (series == null) + if (movie == null) { - series = _parsingService.GetSeries(relativeFile); + movie = _parsingService.GetMovie(relativeFile); } - if (series == null && downloadId.IsNotNullOrWhiteSpace()) + if (movie == null && downloadId.IsNotNullOrWhiteSpace()) { var trackedDownload = _trackedDownloadService.Find(downloadId); - series = trackedDownload.RemoteEpisode.Series; + movie = trackedDownload.RemoteMovie.Movie; } - if (series == null) + if (movie == null) { - var localEpisode = new LocalEpisode(); - localEpisode.Path = file; - localEpisode.Quality = QualityParser.ParseQuality(file); - localEpisode.Size = _diskProvider.GetFileSize(file); + var localMovie = new LocalMovie() + { + Path = file, + Quality = QualityParser.ParseQuality(file), + Size = _diskProvider.GetFileSize(file) + }; - return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); + return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), folder, downloadId); } - var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, - series, null, SceneSource(series, folder)); + var importDecisions = _importDecisionMaker.GetImportDecisions(new List { file }, + movie, null, SceneSource(movie, folder), true); return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; } + //private ManualImportItem ProcessFile(string file, string downloadId, string folder = null) + //{ + // if (folder.IsNullOrWhiteSpace()) + // { + // folder = new FileInfo(file).Directory.FullName; + // } + + // var relativeFile = folder.GetRelativePath(file); + + // var series = _parsingService.GetSeries(relativeFile.Split('\\', '/')[0]); + + // if (series == null) + // { + // series = _parsingService.GetSeries(relativeFile); + // } + + // if (series == null && downloadId.IsNotNullOrWhiteSpace()) + // { + // var trackedDownload = _trackedDownloadService.Find(downloadId); + // series = trackedDownload.RemoteEpisode.Series; + // } + + // if (series == null) + // { + // var localEpisode = new LocalEpisode(); + // localEpisode.Path = file; + // localEpisode.Quality = QualityParser.ParseQuality(file); + // localEpisode.Size = _diskProvider.GetFileSize(file); + + // return MapItem(new ImportDecision(localEpisode, new Rejection("Unknown Series")), folder, downloadId); + // } + + // var importDecisions = _importDecisionMaker.GetImportDecisions(new List {file}, + // series, null, SceneSource(series, folder)); + + // return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : null; + //} + private bool SceneSource(Series series, string folder) { return !(series.Path.PathEquals(folder) || series.Path.IsParentPath(folder)); } + private bool SceneSource(Movie movie, string folder) + { + return !(movie.Path.PathEquals(folder) || movie.Path.IsParentPath(folder)); + } + + //private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId) + //{ + // var item = new ManualImportItem(); + + // item.Path = decision.LocalEpisode.Path; + // item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); + // item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); + // item.DownloadId = downloadId; + + // if (decision.LocalEpisode.Series != null) + // { + // item.Series = decision.LocalEpisode.Series; + // } + + // if (decision.LocalEpisode.Episodes.Any()) + // { + // item.SeasonNumber = decision.LocalEpisode.SeasonNumber; + // item.Episodes = decision.LocalEpisode.Episodes; + // } + + // item.Quality = decision.LocalEpisode.Quality; + // item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); + // item.Rejections = decision.Rejections; + + // return item; + //} + private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId) { var item = new ManualImportItem(); - item.Path = decision.LocalEpisode.Path; - item.RelativePath = folder.GetRelativePath(decision.LocalEpisode.Path); - item.Name = Path.GetFileNameWithoutExtension(decision.LocalEpisode.Path); + item.Path = decision.LocalMovie.Path; + item.RelativePath = folder.GetRelativePath(decision.LocalMovie.Path); + item.Name = Path.GetFileNameWithoutExtension(decision.LocalMovie.Path); item.DownloadId = downloadId; - if (decision.LocalEpisode.Series != null) + if (decision.LocalMovie.Movie != null) { - item.Series = decision.LocalEpisode.Series; + item.Movie = decision.LocalMovie.Movie; } - if (decision.LocalEpisode.Episodes.Any()) - { - item.SeasonNumber = decision.LocalEpisode.SeasonNumber; - item.Episodes = decision.LocalEpisode.Episodes; - } - - item.Quality = decision.LocalEpisode.Quality; - item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); + item.Quality = decision.LocalMovie.Quality; + item.Size = _diskProvider.GetFileSize(decision.LocalMovie.Path); item.Rejections = decision.Rejections; return item; @@ -199,45 +274,43 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); var file = message.Files[i]; - var series = _seriesService.GetSeries(file.SeriesId); - var episodes = _episodeService.GetEpisodes(file.EpisodeIds); - var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); + var movie = _movieService.GetMovie(file.MovieId); + var parsedMovieInfo = Parser.Parser.ParseMoviePath(file.Path) ?? new ParsedMovieInfo(); var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); - var existingFile = series.Path.IsParentPath(file.Path); + var existingFile = movie.Path.IsParentPath(file.Path); - var localEpisode = new LocalEpisode + var localMovie = new LocalMovie { ExistingFile = false, - Episodes = episodes, MediaInfo = mediaInfo, - ParsedEpisodeInfo = parsedEpisodeInfo, + ParsedMovieInfo = parsedMovieInfo, Path = file.Path, Quality = file.Quality, - Series = series, + Movie = movie, Size = 0 }; //TODO: Cleanup non-tracked downloads - var importDecision = new ImportDecision(localEpisode); + var importDecision = new ImportDecision(localMovie); if (file.DownloadId.IsNullOrWhiteSpace()) { - imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); + imported.AddRange(_importApprovedMovie.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); } else { var trackedDownload = _trackedDownloadService.Find(file.DownloadId); - var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); + var importResult = _importApprovedMovie.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); imported.Add(importResult); importedTrackedDownload.Add(new ManuallyImportedFile - { - TrackedDownload = trackedDownload, - ImportResult = importResult - }); + { + TrackedDownload = trackedDownload, + ImportResult = importResult + }); } } @@ -249,20 +322,98 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) { - if (_downloadedEpisodesImportService.ShouldDeleteFolder( + if (_downloadedMovieImportService.ShouldDeleteFolder( new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), - trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) + trackedDownload.RemoteMovie.Movie) && !trackedDownload.DownloadItem.IsReadOnly) { _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); } } - if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, 1)) //TODO: trackedDownload.RemoteMovie.Movie.Count is always 1? { trackedDownload.State = TrackedDownloadStage.Imported; _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); } } } + + //public void Execute(ManualImportCommand message) + //{ + // _logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode); + + // var imported = new List(); + // var importedTrackedDownload = new List(); + + // for (int i = 0; i < message.Files.Count; i++) + // { + // _logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count); + + // var file = message.Files[i]; + // var series = _seriesService.GetSeries(file.SeriesId); + // var episodes = _episodeService.GetEpisodes(file.EpisodeIds); + // var parsedEpisodeInfo = Parser.Parser.ParsePath(file.Path) ?? new ParsedEpisodeInfo(); + // var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path); + // var existingFile = series.Path.IsParentPath(file.Path); + + // var localEpisode = new LocalEpisode + // { + // ExistingFile = false, + // Episodes = episodes, + // MediaInfo = mediaInfo, + // ParsedEpisodeInfo = parsedEpisodeInfo, + // Path = file.Path, + // Quality = file.Quality, + // Series = series, + // Size = 0 + // }; + + // //TODO: Cleanup non-tracked downloads + + // var importDecision = new ImportDecision(localEpisode); + + // if (file.DownloadId.IsNullOrWhiteSpace()) + // { + // imported.AddRange(_importApprovedEpisodes.Import(new List { importDecision }, !existingFile, null, message.ImportMode)); + // } + + // else + // { + // var trackedDownload = _trackedDownloadService.Find(file.DownloadId); + // var importResult = _importApprovedEpisodes.Import(new List { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First(); + + // imported.Add(importResult); + + // importedTrackedDownload.Add(new ManuallyImportedFile + // { + // TrackedDownload = trackedDownload, + // ImportResult = importResult + // }); + // } + // } + + // _logger.ProgressTrace("Manually imported {0} files", imported.Count); + + // foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList()) + // { + // var trackedDownload = groupedTrackedDownload.First().TrackedDownload; + + // if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) + // { + // if (_downloadedEpisodesImportService.ShouldDeleteFolder( + // new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath), + // trackedDownload.RemoteEpisode.Series) && !trackedDownload.DownloadItem.IsReadOnly) + // { + // _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); + // } + // } + + // if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + // { + // trackedDownload.State = TrackedDownloadStage.Imported; + // _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + // } + // } + //} } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 158059e29..1e8432e84 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -63,5 +63,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + if (_configService.SkipFreeSpaceCheckWhenImporting) + { + _logger.Debug("Skipping free space check when importing"); + return Decision.Accept(); + } + + try + { + if (localMovie.ExistingFile) + { + _logger.Debug("Skipping free space check for existing episode"); + return Decision.Accept(); + } + + var path = Directory.GetParent(localMovie.Movie.Path); + var freeSpace = _diskProvider.GetAvailableSpace(path.FullName); + + if (!freeSpace.HasValue) + { + _logger.Debug("Free space check returned an invalid result for: {0}", path); + return Decision.Accept(); + } + + if (freeSpace < localMovie.Size + 100.Megabytes()) + { + _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size); + return Decision.Reject("Not enough free space"); + } + } + catch (DirectoryNotFoundException ex) + { + _logger.Error("Unable to check free disk space while importing. " + ex.Message); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to check free disk space while importing: " + localMovie.Path); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 7397c13e7..2daeda6cb 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -17,11 +17,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { if (localEpisode.ParsedEpisodeInfo.FullSeason) { - _logger.Debug("Single episode file detected as containing all episodes in the season"); + _logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah return Decision.Reject("Single episode file contains all episodes in seasons"); } return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index 79ef96f88..55a8da073 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; @@ -14,6 +15,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { _logger = logger; } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + if (localMovie.ExistingFile) + { + return Decision.Accept(); + } + + var dirInfo = new FileInfo(localMovie.Path).Directory; + + if (dirInfo == null) + { + return Decision.Accept(); + } + + var folderInfo = Parser.Parser.ParseTitle(dirInfo.Name); + + if (folderInfo == null) + { + return Decision.Accept(); + } + + if (folderInfo.FullSeason) + { + return Decision.Accept(); + } + + return Decision.Accept(); + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index c7b61d802..ed8859cde 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -37,5 +37,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + var sample = _detectSample.IsSample(localEpisode.Movie, + localEpisode.Quality, + localEpisode.Path, + localEpisode.Size, + false); + + if (sample) + { + return Decision.Reject("Sample"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index 2260ed71a..a19359457 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -56,5 +56,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + if (localEpisode.ExistingFile) + { + _logger.Debug("{0} is in series folder, skipping unpacking check", localEpisode.Path); + return Decision.Accept(); + } + + foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) + { + DirectoryInfo parent = Directory.GetParent(localEpisode.Path); + while (parent != null) + { + if (parent.Name.StartsWith(workingFolder)) + { + if (OsInfo.IsNotWindows) + { + _logger.Debug("{0} is still being unpacked", localEpisode.Path); + return Decision.Reject("File is still being unpacked"); + } + + if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) + { + _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); + return Decision.Reject("File is still being unpacked"); + } + } + + parent = parent.Parent; + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs index ee6c02c53..c24d62aaa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -27,5 +28,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Debug("Episode file on disk contains more episodes than this file contains"); return Decision.Reject("Episode file on disk contains more episodes than this file contains"); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs index ce65eb304..85becc0ba 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 3d07306af..b2d2c2c33 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs new file mode 100644 index 000000000..427996088 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieDownloadedEvent : IEvent + { + public LocalMovie Movie { get; private set; } + public MovieFile MovieFile { get; private set; } + public List OldFiles { get; private set; } + + public MovieDownloadedEvent(LocalMovie episode, MovieFile episodeFile, List oldFiles) + { + Movie = episode; + MovieFile = episodeFile; + OldFiles = oldFiles; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs new file mode 100644 index 000000000..17f93dc0b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFileAddedEvent : IEvent + { + public MovieFile MovieFile { get; private set; } + + public MovieFileAddedEvent(MovieFile episodeFile) + { + MovieFile = episodeFile; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs new file mode 100644 index 000000000..232f1686f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFileDeletedEvent : IEvent + { + public MovieFile MovieFile { get; private set; } + public DeleteMediaFileReason Reason { get; private set; } + + public MovieFileDeletedEvent(MovieFile episodeFile, DeleteMediaFileReason reason) + { + MovieFile = episodeFile; + Reason = reason; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs new file mode 100644 index 000000000..a26031413 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFolderCreatedEvent : IEvent + { + public Movie Movie { get; private set; } + public MovieFile MovieFile { get; private set; } + public string SeriesFolder { get; set; } + public string SeasonFolder { get; set; } + public string MovieFolder { get; set; } + + public MovieFolderCreatedEvent(Movie movie, MovieFile episodeFile) + { + Movie = movie; + MovieFile = episodeFile; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs new file mode 100644 index 000000000..91df27e3c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs @@ -0,0 +1,32 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieImportedEvent : IEvent + { + public LocalMovie MovieInfo { get; private set; } + public MovieFile ImportedMovie { get; private set; } + public bool NewDownload { get; private set; } + public string DownloadClient { get; private set; } + public string DownloadId { get; private set; } + public bool IsReadOnly { get; set; } + + public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload) + { + MovieInfo = episodeInfo; + ImportedMovie = importedMovie; + NewDownload = newDownload; + } + + public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) + { + MovieInfo = episodeInfo; + ImportedMovie = importedMovie; + NewDownload = newDownload; + DownloadClient = downloadClient; + DownloadId = downloadId; + IsReadOnly = isReadOnly; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs new file mode 100644 index 000000000..d7e264fa3 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieRenamedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieRenamedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieRenamedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs new file mode 100644 index 000000000..ecca0436d --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieScanSkippedEvent.cs @@ -0,0 +1,24 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieScanSkippedEvent : IEvent + { + public Movie Movie { get; private set; } + public MovieScanSkippedReason Reason { get; set; } + + public MovieScanSkippedEvent(Movie movie, MovieScanSkippedReason reason) + { + Movie = movie; + Reason = reason; + } + } + + public enum MovieScanSkippedReason + { + RootFolderDoesNotExist, + RootFolderIsEmpty, + MovieFolderDoesNotExist + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs new file mode 100644 index 000000000..a0299d408 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieScannedEvent.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieScannedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieScannedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs index 206942356..3cc90af0f 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index ca3f68ce2..37e663ee5 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -7,33 +7,44 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; using NzbDrone.Common; +using System; namespace NzbDrone.Core.MediaFiles { public interface IMediaFileService { + MovieFile Add(MovieFile movieFile); + void Update(MovieFile movieFile); + void Delete(MovieFile movieFile, DeleteMediaFileReason reason); EpisodeFile Add(EpisodeFile episodeFile); void Update(EpisodeFile episodeFile); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); List GetFilesBySeries(int seriesId); + List GetFilesByMovie(int movieId); List GetFilesBySeason(int seriesId, int seasonNumber); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Series series); + List FilterExistingFiles(List files, Movie movie); EpisodeFile Get(int id); + MovieFile GetMovie(int id); List Get(IEnumerable ids); + List GetMovies(IEnumerable ids); + //List Get(IEnumerable ids); } public class MediaFileService : IMediaFileService, IHandleAsync { private readonly IEventAggregator _eventAggregator; private readonly IMediaFileRepository _mediaFileRepository; + private readonly IMovieFileRepository _movieFileRepository; private readonly Logger _logger; - public MediaFileService(IMediaFileRepository mediaFileRepository, IEventAggregator eventAggregator, Logger logger) + public MediaFileService(IMediaFileRepository mediaFileRepository, IMovieFileRepository movieFileRepository, IEventAggregator eventAggregator, Logger logger) { _mediaFileRepository = mediaFileRepository; _eventAggregator = eventAggregator; + _movieFileRepository = movieFileRepository; _logger = logger; } @@ -59,11 +70,26 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); } + public void Delete(MovieFile episodeFile, DeleteMediaFileReason reason) + { + //Little hack so we have the episodes and series attached for the event consumers + episodeFile.Movie.LazyLoad(); + episodeFile.Path = Path.Combine(episodeFile.Movie.Value.Path, episodeFile.RelativePath); + + _movieFileRepository.Delete(episodeFile); + _eventAggregator.PublishEvent(new MovieFileDeletedEvent(episodeFile, reason)); + } + public List GetFilesBySeries(int seriesId) { return _mediaFileRepository.GetFilesBySeries(seriesId); } + public List GetFilesByMovie(int movieId) + { + return _movieFileRepository.GetFilesByMovie(movieId); //TODO: Update implementation for movie files. + } + public List GetFilesBySeason(int seriesId, int seasonNumber) { return _mediaFileRepository.GetFilesBySeason(seriesId, seasonNumber); @@ -83,6 +109,15 @@ namespace NzbDrone.Core.MediaFiles return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); } + public List FilterExistingFiles(List files, Movie movie) + { + var seriesFiles = GetFilesBySeries(movie.Id).Select(f => Path.Combine(movie.Path, f.RelativePath)).ToList(); + + if (!seriesFiles.Any()) return files; + + return files.Except(seriesFiles, PathEqualityComparer.Instance).ToList(); + } + public EpisodeFile Get(int id) { return _mediaFileRepository.Get(id); @@ -93,10 +128,32 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.Get(ids).ToList(); } + public List GetMovies(IEnumerable ids) + { + return _movieFileRepository.Get(ids).ToList(); + } + public void HandleAsync(SeriesDeletedEvent message) { var files = GetFilesBySeries(message.Series.Id); _mediaFileRepository.DeleteMany(files); } + + public MovieFile Add(MovieFile episodeFile) + { + var addedFile = _movieFileRepository.Insert(episodeFile); + _eventAggregator.PublishEvent(new MovieFileAddedEvent(addedFile)); + return addedFile; + } + + public void Update(MovieFile episodeFile) + { + _movieFileRepository.Update(episodeFile); + } + + public MovieFile GetMovie(int id) + { + return _movieFileRepository.Get(id); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index b275fb03e..49eb11ca9 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -11,6 +11,8 @@ namespace NzbDrone.Core.MediaFiles public interface IMediaFileTableCleanupService { void Clean(Series series, List filesOnDisk); + + void Clean(Movie movie, List filesOnDisk); } public class MediaFileTableCleanupService : IMediaFileTableCleanupService @@ -84,5 +86,64 @@ namespace NzbDrone.Core.MediaFiles } } } + + public void Clean(Movie movie, List filesOnDisk) + { + + //TODO: Update implementation for movies. + var seriesFiles = _mediaFileService.GetFilesBySeries(movie.Id); + var episodes = _episodeService.GetEpisodeBySeries(movie.Id); + + var filesOnDiskKeys = new HashSet(filesOnDisk, PathEqualityComparer.Instance); + + foreach (var seriesFile in seriesFiles) + { + var episodeFile = seriesFile; + var episodeFilePath = Path.Combine(movie.Path, episodeFile.RelativePath); + + try + { + if (!filesOnDiskKeys.Contains(episodeFilePath)) + { + _logger.Debug("File [{0}] no longer exists on disk, removing from db", episodeFilePath); + _mediaFileService.Delete(seriesFile, DeleteMediaFileReason.MissingFromDisk); + continue; + } + + if (episodes.None(e => e.EpisodeFileId == episodeFile.Id)) + { + _logger.Debug("File [{0}] is not assigned to any episodes, removing from db", episodeFilePath); + _mediaFileService.Delete(episodeFile, DeleteMediaFileReason.NoLinkedEpisodes); + continue; + } + + // var localEpsiode = _parsingService.GetLocalEpisode(episodeFile.Path, series); + // + // if (localEpsiode == null || episodes.Count != localEpsiode.Episodes.Count) + // { + // _logger.Debug("File [{0}] parsed episodes has changed, removing from db", episodeFile.Path); + // _mediaFileService.Delete(episodeFile); + // continue; + // } + } + + catch (Exception ex) + { + var errorMessage = string.Format("Unable to cleanup EpisodeFile in DB: {0}", episodeFile.Id); + _logger.Error(ex, errorMessage); + } + } + + foreach (var e in episodes) + { + var episode = e; + + if (episode.EpisodeFileId > 0 && seriesFiles.None(f => f.Id == episode.EpisodeFileId)) + { + episode.EpisodeFileId = 0; + _episodeService.UpdateEpisode(episode); + } + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs new file mode 100644 index 000000000..9bb0f1ddd --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Marr.Data; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; + +namespace NzbDrone.Core.MediaFiles +{ + public class MovieFile : ModelBase + { + public int MovieId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public string Edition { get; set; } + public LazyLoaded Movie { get; set; } + + public override string ToString() + { + return string.Format("[{0}] {1}", Id, RelativePath); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs new file mode 100644 index 000000000..a52faed61 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MediaFiles +{ + public class MovieFileMoveResult + { + public MovieFileMoveResult() + { + OldFiles = new List(); + } + + public MovieFile MovieFile { get; set; } + public List OldFiles { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs new file mode 100644 index 000000000..cf8acd6f9 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IMoveMovieFiles + { + MovieFile MoveMovieFile(MovieFile movieFile, Movie movie); + MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie); + MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie); + } + + public class MovieFileMovingService : IMoveMovieFiles + { + private readonly IMovieService _movieService; + private readonly IUpdateMovieFileService _updateMovieFileService; + private readonly IBuildFileNames _buildFileNames; + private readonly IDiskTransferService _diskTransferService; + private readonly IDiskProvider _diskProvider; + private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public MovieFileMovingService(IMovieService movieService, + IUpdateMovieFileService updateMovieFileService, + IBuildFileNames buildFileNames, + IDiskTransferService diskTransferService, + IDiskProvider diskProvider, + IMediaFileAttributeService mediaFileAttributeService, + IEventAggregator eventAggregator, + IConfigService configService, + Logger logger) + { + _movieService = movieService; + _updateMovieFileService = updateMovieFileService; + _buildFileNames = buildFileNames; + _diskTransferService = diskTransferService; + _diskProvider = diskProvider; + _mediaFileAttributeService = mediaFileAttributeService; + _eventAggregator = eventAggregator; + _configService = configService; + _logger = logger; + } + + public MovieFile MoveMovieFile(MovieFile movieFile, Movie movie) + { + var newFileName = _buildFileNames.BuildFileName(movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(movie, newFileName, Path.GetExtension(movieFile.RelativePath)); + + EnsureMovieFolder(movieFile, movie, filePath); + + _logger.Debug("Renaming movie file: {0} to {1}", movieFile, filePath); + + return TransferFile(movieFile, movie, filePath, TransferMode.Move); + } + + public MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie) + { + var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path)); + + EnsureMovieFolder(movieFile, localMovie, filePath); + + _logger.Debug("Moving movie file: {0} to {1}", movieFile.Path, filePath); + + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Move); + } + + public MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie) + { + var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path)); + + EnsureMovieFolder(movieFile, localMovie, filePath); + + if (_configService.CopyUsingHardlinks) + { + _logger.Debug("Hardlinking movie file: {0} to {1}", movieFile.Path, filePath); + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.HardLinkOrCopy); + } + + _logger.Debug("Copying movie file: {0} to {1}", movieFile.Path, filePath); + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Copy); + } + + private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinationFilePath, TransferMode mode) + { + Ensure.That(movieFile, () => movieFile).IsNotNull(); + Ensure.That(movie,() => movie).IsNotNull(); + Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); + + var movieFilePath = movieFile.Path ?? Path.Combine(movie.Path, movieFile.RelativePath); + + if (!_diskProvider.FileExists(movieFilePath)) + { + throw new FileNotFoundException("Movie file path does not exist", movieFilePath); + } + + if (movieFilePath == destinationFilePath) + { + throw new SameFilenameException("File not moved, source and destination are the same", movieFilePath); + } + + _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); + + movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath); + + _updateMovieFileService.ChangeFileDateForFile(movieFile, movie); + + try + { + _mediaFileAttributeService.SetFolderLastWriteTime(movie.Path, movieFile.DateAdded); + } + + catch (Exception ex) + { + _logger.Warn(ex, "Unable to set last write time"); + } + + _mediaFileAttributeService.SetFilePermissions(destinationFilePath); + + return movieFile; + } + + private void EnsureMovieFolder(MovieFile movieFile, LocalMovie localMovie, string filePath) + { + EnsureMovieFolder(movieFile, localMovie.Movie, filePath); + } + + private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath) + { + var movieFolder = Path.GetDirectoryName(filePath); + var rootFolder = new OsPath(movieFolder).Directory.FullPath; + + if (!_diskProvider.FolderExists(rootFolder)) + { + throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + } + + var changed = false; + var newEvent = new MovieFolderCreatedEvent(movie, movieFile); + + if (!_diskProvider.FolderExists(movieFolder)) + { + CreateFolder(movieFolder); + newEvent.SeriesFolder = movieFolder; + changed = true; + } + + if (changed) + { + _eventAggregator.PublishEvent(newEvent); + } + } + + private void CreateFolder(string directoryName) + { + Ensure.That(directoryName, () => directoryName).IsNotNullOrWhiteSpace(); + + var parentFolder = new OsPath(directoryName).Directory.FullPath; + if (!_diskProvider.FolderExists(parentFolder)) + { + CreateFolder(parentFolder); + } + + try + { + _diskProvider.CreateFolder(directoryName); + } + catch (IOException ex) + { + _logger.Error(ex, "Unable to create directory: " + directoryName); + } + + _mediaFileAttributeService.SetFolderPermissions(directoryName); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs new file mode 100644 index 000000000..9ed89b85f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.MediaFiles +{ + public interface IMovieFileRepository : IBasicRepository + { + List GetFilesByMovie(int movieId); + List GetFilesWithoutMediaInfo(); + } + + + public class MovieFileRepository : BasicRepository, IMovieFileRepository + { + public MovieFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List GetFilesByMovie(int movieId) + { + return Query.Where(c => c.MovieId == movieId).ToList(); + } + + public List GetFilesWithoutMediaInfo() + { + return Query.Where(c => c.MediaInfo == null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/RenameMovieFilePreview.cs b/src/NzbDrone.Core/MediaFiles/RenameMovieFilePreview.cs new file mode 100644 index 000000000..12f52e42a --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/RenameMovieFilePreview.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MediaFiles +{ + public class RenameMovieFilePreview + { + public int MovieId { get; set; } + public int MovieFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs new file mode 100644 index 000000000..9a4019f56 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/RenameMovieFileService.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using NLog; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MediaFiles.Commands; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IRenameMovieFileService + { + List GetRenamePreviews(int movieId); + } + + public class RenameMovieFileService : IRenameMovieFileService, + IExecute, + IExecute + { + private readonly IMovieService _movieService; + private readonly IMediaFileService _mediaFileService; + private readonly IMoveMovieFiles _movieFileMover; + private readonly IEventAggregator _eventAggregator; + private readonly IBuildFileNames _filenameBuilder; + private readonly Logger _logger; + + public RenameMovieFileService(IMovieService movieService, + IMediaFileService mediaFileService, + IMoveMovieFiles movieFileMover, + IEventAggregator eventAggregator, + IBuildFileNames filenameBuilder, + Logger logger) + { + _movieService = movieService; + _mediaFileService = mediaFileService; + _movieFileMover = movieFileMover; + _eventAggregator = eventAggregator; + _filenameBuilder = filenameBuilder; + _logger = logger; + } + + public List GetRenamePreviews(int movieId) + { + var movie = _movieService.GetMovie(movieId); + var file = _mediaFileService.GetFilesByMovie(movieId); + + return GetPreviews(movie, file).OrderByDescending(m => m.MovieId).ToList(); //TODO: Would really like to not have these be lists + + } + + private IEnumerable GetPreviews(Movie movie, List files) + { + foreach(var file in files) + { + var movieFilePath = Path.Combine(movie.Path, file.RelativePath); + + var newName = _filenameBuilder.BuildFileName(movie, file); + var newPath = _filenameBuilder.BuildFilePath(movie, newName, Path.GetExtension(movieFilePath)); + + if(!movieFilePath.PathEquals(newPath, StringComparison.Ordinal)) + { + yield return new RenameMovieFilePreview + { + MovieId = movie.Id, + MovieFileId = file.Id, + ExistingPath = file.RelativePath, + NewPath = movie.Path.GetRelativePath(newPath) + }; + } + + } + + } + + private void RenameFiles(List movieFiles, Movie movie) + { + var renamed = new List(); + + foreach(var movieFile in movieFiles) + { + var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath); + + try + { + _logger.Debug("Renaming movie file: {0}", movieFile); + _movieFileMover.MoveMovieFile(movieFile, movie); + + _mediaFileService.Update(movieFile); + renamed.Add(movieFile); + + _logger.Debug("Renamed movie file: {0}", movieFile); + + } + catch(SameFilenameException ex) + { + _logger.Debug("File not renamed, source and destination are the same: {0}", ex.Filename); + } + catch(Exception ex) + { + _logger.Error(ex, "Failed to rename file: " + movieFilePath); + } + } + } + + public void Execute(RenameMovieFilesCommand message) + { + var movie = _movieService.GetMovie(message.MovieId); + var movieFiles = _mediaFileService.GetMovies(message.Files); + + _logger.ProgressInfo("Renaming {0} files for {1}", movieFiles.Count, movie.Title); + RenameFiles(movieFiles, movie); + _logger.ProgressInfo("Selected movie files renamed for {0}", movie.Title); + } + + public void Execute(RenameMovieCommand message) + { + _logger.Debug("Renaming all files for selected movie"); + var moviesToRename = _movieService.GetMovies(message.MovieIds); + + foreach(var movie in moviesToRename) + { + var movieFiles = _mediaFileService.GetFilesByMovie(movie.Id); + _logger.ProgressInfo("Renaming all files in movie: {0}", movie.Title); + RenameFiles(movieFiles, movie); + _logger.ProgressInfo("All movie files renamed for {0}", movie.Title); + } + + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs new file mode 100644 index 000000000..af45d8831 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Exceptron; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IUpdateMovieFileService + { + void ChangeFileDateForFile(MovieFile movieFile, Movie movie); + } + + public class UpdateMovieFileService : IUpdateMovieFileService, + IHandle + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly IMovieService _movieService; + private readonly Logger _logger; + + public UpdateMovieFileService(IDiskProvider diskProvider, + IConfigService configService, + IMovieService movieService, + Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _movieService = movieService; + _logger = logger; + } + + public void ChangeFileDateForFile(MovieFile movieFile, Movie movie) + { + ChangeFileDate(movieFile, movie); + } + + private bool ChangeFileDate(MovieFile movieFile, Movie movie) + { + var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath); + + return false; + } + + public void Handle(SeriesScannedEvent message) + { + if (_configService.FileDate == FileDateType.None) + { + return; + } + + /* var movies = _movieService.MoviesWithFiles(message.Series.Id); + + var movieFiles = new List(); + var updated = new List(); + + foreach (var group in movies.GroupBy(e => e.MovieFileId)) + { + var moviesInFile = group.Select(e => e).ToList(); + var movieFile = moviesInFile.First().MovieFile; + + movieFiles.Add(movieFile); + + if (ChangeFileDate(movieFile, message.Series, moviesInFile)) + { + updated.Add(movieFile); + } + } + + if (updated.Any()) + { + _logger.ProgressDebug("Changed file date for {0} files of {1} in {2}", updated.Count, movieFiles.Count, message.Series.Title); + } + + else + { + _logger.ProgressDebug("No file dates changed for {0}", message.Series.Title); + }*/ + } + + private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime) + { + DateTime airDate; + + if (DateTime.TryParse(fileDate + ' ' + fileTime, out airDate)) + { + // avoiding false +ve checks and set date skewing by not using UTC (Windows) + DateTime oldDateTime = _diskProvider.FileGetLastWrite(filePath); + + if (!DateTime.Equals(airDate, oldDateTime)) + { + try + { + _diskProvider.FileSetLastWriteTime(filePath, airDate); + _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldDateTime, airDate); + + return true; + } + + catch (Exception ex) + { + _logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); + } + } + } + + else + { + _logger.Debug("Could not create valid date to change file [{0}]", filePath); + } + + return false; + } + + private bool ChangeFileDateToUtcAirDate(string filePath, DateTime airDateUtc) + { + DateTime oldLastWrite = _diskProvider.FileGetLastWrite(filePath); + + if (!DateTime.Equals(airDateUtc, oldLastWrite)) + { + try + { + _diskProvider.FileSetLastWriteTime(filePath, airDateUtc); + _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDateUtc); + + return true; + } + + catch (Exception ex) + { + ex.ExceptronIgnoreOnMono(); + _logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); + } + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 95f245e3e..b8cd9f36d 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles public interface IUpgradeMediaFiles { EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); + MovieFileMoveResult UpgradeMovieFile(MovieFile movieFile, LocalMovie localMovie, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -16,22 +17,59 @@ namespace NzbDrone.Core.MediaFiles private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; private readonly IMoveEpisodeFiles _episodeFileMover; + private readonly IMoveMovieFiles _movieFileMover; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, IMoveEpisodeFiles episodeFileMover, + IMoveMovieFiles movieFileMover, IDiskProvider diskProvider, Logger logger) { _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; _episodeFileMover = episodeFileMover; + _movieFileMover = movieFileMover; _diskProvider = diskProvider; _logger = logger; } + public MovieFileMoveResult UpgradeMovieFile(MovieFile episodeFile, LocalMovie localEpisode, bool copyOnly = false) + { + var moveFileResult = new MovieFileMoveResult(); + var existingFile = localEpisode.Movie.MovieFile; + + if (existingFile.IsLoaded) + { + var file = existingFile.Value; + var episodeFilePath = Path.Combine(localEpisode.Movie.Path, file.RelativePath); + + if (_diskProvider.FileExists(episodeFilePath)) + { + _logger.Debug("Removing existing episode file: {0}", file); + _recycleBinProvider.DeleteFile(episodeFilePath); + } + + moveFileResult.OldFiles.Add(file); + _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); + } + + + + if (copyOnly) + { + moveFileResult.MovieFile = _movieFileMover.CopyMovieFile(episodeFile, localEpisode); + } + else + { + moveFileResult.MovieFile= _movieFileMover.MoveMovieFile(episodeFile, localEpisode); + } + + return moveFileResult; + } + public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) { var moveFileResult = new EpisodeFileMoveResult(); diff --git a/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs new file mode 100644 index 000000000..e5256a6dc --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/IProvideMovieInfo.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MetadataSource +{ + public interface IProvideMovieInfo + { + Movie GetMovieInfo(string ImdbId); + Movie GetMovieInfo(int TmdbId); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs new file mode 100644 index 000000000..d895075f9 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewMovie.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MetadataSource +{ + public interface ISearchForNewMovie + { + List SearchForNewMovie(string title); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ConfigurationResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ConfigurationResource.cs new file mode 100644 index 000000000..808bd9ab9 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/ConfigurationResource.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + + public class ConfigResource + { + public Images images { get; set; } + public string[] change_keys { get; set; } + } + + public class Images + { + public string base_url { get; set; } + public string secure_base_url { get; set; } + public string[] backdrop_sizes { get; set; } + public string[] logo_sizes { get; set; } + public string[] poster_sizes { get; set; } + public string[] profile_sizes { get; set; } + public string[] still_sizes { get; set; } + } + +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MovieResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MovieResource.cs new file mode 100644 index 000000000..72e3534e2 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/MovieResource.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + public class ImdbResource + { + public int v { get; set; } + public string q { get; set; } + public MovieResource[] d { get; set; } + } + + public class MovieResource + { + public string l { get; set; } + public string id { get; set; } + public string s { get; set; } + public int y { get; set; } + public string q { get; set; } + public object[] i { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs new file mode 100644 index 000000000..c0987e04f --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.MetadataSource.SkyHook.Resource +{ + + public class FindRoot + { + public MovieResult[] movie_results { get; set; } + } + public class MovieSearchRoot + { + public int page { get; set; } + public MovieResult[] results { get; set; } + public int total_results { get; set; } + public int total_pages { get; set; } + } + + public class MovieResult + { + public string poster_path { get; set; } + public bool adult { get; set; } + public string overview { get; set; } + public string release_date { get; set; } + public int?[] genre_ids { get; set; } + public int id { get; set; } + public string original_title { get; set; } + public string original_language { get; set; } + public string title { get; set; } + public string backdrop_path { get; set; } + public float popularity { get; set; } + public int vote_count { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + } + + + public class MovieResourceRoot + { + public bool adult { get; set; } + public string backdrop_path { get; set; } + public Belongs_To_Collection belongs_to_collection { get; set; } + public int budget { get; set; } + public Genre[] genres { get; set; } + public string homepage { get; set; } + public int id { get; set; } + public string imdb_id { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public string overview { get; set; } + public float popularity { get; set; } + public string poster_path { get; set; } + public Production_Companies[] production_companies { get; set; } + public Production_Countries[] production_countries { get; set; } + public string release_date { get; set; } + public long revenue { get; set; } + public int runtime { get; set; } + public Spoken_Languages[] spoken_languages { get; set; } + public string status { get; set; } + public string tagline { get; set; } + public string title { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public int vote_count { get; set; } + public AlternativeTitles alternative_titles { get; set; } + public ReleaseDatesResource release_dates { get; set; } + } + + public class ReleaseDatesResource + { + public List results { get; set; } + } + + public class ReleaseDate + { + public string certification { get; set; } + public string iso_639_1 { get; set; } + public string note { get; set; } + public string release_date { get; set; } + public int type { get; set; } + } + + public class ReleaseDates + { + public string iso_3166_1 { get; set; } + public List release_dates { get; set; } + } + + public class Belongs_To_Collection + { + public int id { get; set; } + public string name { get; set; } + public string poster_path { get; set; } + public string backdrop_path { get; set; } + } + + public class Genre + { + public int id { get; set; } + public string name { get; set; } + } + + public class Production_Companies + { + public string name { get; set; } + public int id { get; set; } + } + + public class Production_Countries + { + public string iso_3166_1 { get; set; } + public string name { get; set; } + } + + public class Spoken_Languages + { + public string iso_639_1 { get; set; } + public string name { get; set; } + } + + public class AlternativeTitles + { + public List titles { get; set; } + } + + public class Title + { + public string iso_3166_1 { get; set; } + public string title { get; set; } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 3c1ca6740..6f1534ed7 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -9,21 +9,31 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Tv; +using Newtonsoft.Json; +using System.Text.RegularExpressions; +using System.Text; namespace NzbDrone.Core.MetadataSource.SkyHook { - public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries + public class SkyHookProxy : IProvideSeriesInfo, ISearchForNewSeries, IProvideMovieInfo, ISearchForNewMovie { private readonly IHttpClient _httpClient; private readonly Logger _logger; private readonly IHttpRequestBuilderFactory _requestBuilder; + private readonly IHttpRequestBuilderFactory _movieBuilder; + private readonly ITmdbConfigService _configService; + private readonly IMovieService _movieService; - public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger) + public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, IMovieService movieService, Logger logger) { _httpClient = httpClient; _requestBuilder = requestBuilder.SkyHookTvdb; + _movieBuilder = requestBuilder.TMDB; + _configService = configService; + _movieService = movieService; _logger = logger; } @@ -57,6 +67,206 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return new Tuple<Series, List<Episode>>(series, episodes.ToList()); } + public Movie GetMovieInfo(int TmdbId) + { + var request = _movieBuilder.Create() + .SetSegment("route", "movie") + .SetSegment("id", TmdbId.ToString()) + .SetSegment("secondaryRoute", "") + .AddQueryParam("append_to_response", "alternative_titles,release_dates") + .AddQueryParam("country", "US") + .Build(); + + request.AllowAutoRedirect = true; + request.SuppressHttpError = true; + + var response = _httpClient.Get<MovieResourceRoot>(request); + + var resource = response.Resource; + + var movie = new Movie(); + + movie.TmdbId = TmdbId; + movie.ImdbId = resource.imdb_id; + movie.Title = resource.title; + movie.TitleSlug = ToUrlSlug(movie.Title); + movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title); + movie.SortTitle = Parser.Parser.NormalizeTitle(movie.Title); + movie.Overview = resource.overview; + movie.Website = resource.homepage; + if (resource.release_date.IsNotNullOrWhiteSpace()) + { + movie.InCinemas = DateTime.Parse(resource.release_date); + movie.Year = movie.InCinemas.Value.Year; + } + + movie.TitleSlug += "-" + movie.Year.ToString(); + + movie.Images.Add(_configService.GetCoverForURL(resource.poster_path, MediaCoverTypes.Poster));//TODO: Update to load image specs from tmdb page! + movie.Images.Add(_configService.GetCoverForURL(resource.backdrop_path, MediaCoverTypes.Banner)); + movie.Runtime = resource.runtime; + + foreach(Title title in resource.alternative_titles.titles) + { + movie.AlternativeTitles.Add(title.title); + } + + foreach(ReleaseDates releaseDates in resource.release_dates.results) + { + foreach(ReleaseDate releaseDate in releaseDates.release_dates) + { + if (releaseDate.type == 5 || releaseDate.type == 4) + { + if (movie.PhysicalRelease.HasValue) + { + if (movie.PhysicalRelease.Value.After(DateTime.Parse(releaseDate.release_date))) + { + movie.PhysicalRelease = DateTime.Parse(releaseDate.release_date); //Use oldest release date available. + } + } + else + { + movie.PhysicalRelease = DateTime.Parse(releaseDate.release_date); + } + } + } + } + + movie.Ratings = new Ratings(); + movie.Ratings.Votes = resource.vote_count; + movie.Ratings.Value = (decimal)resource.vote_average; + + foreach(Genre genre in resource.genres) + { + movie.Genres.Add(genre.name); + } + + if (resource.status == "Released") + { + movie.Status = MovieStatusType.Released; + } + else + { + movie.Status = MovieStatusType.Announced; + } + + return movie; + } + + public Movie GetMovieInfo(string ImdbId) + { + var request = _movieBuilder.Create() + .SetSegment("route", "find") + .SetSegment("id", ImdbId) + .SetSegment("secondaryRoute", "") + .AddQueryParam("external_source", "imdb_id") + .Build(); + + request.AllowAutoRedirect = true; + request.SuppressHttpError = true; + + var resources = _httpClient.Get<FindRoot>(request).Resource; + + return resources.movie_results.SelectList(MapMovie).FirstOrDefault(); + } + + private string StripTrailingTheFromTitle(string title) + { + if(title.EndsWith(",the")) + { + title = title.Substring(0, title.Length - 4); + } else if(title.EndsWith(", the")) + { + title = title.Substring(0, title.Length - 5); + } + return title; + } + + public List<Movie> SearchForNewMovie(string title) + { + var lowerTitle = title.ToLower(); + + var parserResult = Parser.Parser.ParseMovieTitle(title, true); + + var yearTerm = ""; + + if (parserResult != null && parserResult.MovieTitle != title) + { + //Parser found something interesting! + lowerTitle = parserResult.MovieTitle.ToLower(); + if (parserResult.Year > 1800) + { + yearTerm = parserResult.Year.ToString(); + } + + if (parserResult.ImdbId.IsNotNullOrWhiteSpace()) + { + return new List<Movie> { GetMovieInfo(parserResult.ImdbId) }; + } + } + + lowerTitle = StripTrailingTheFromTitle(lowerTitle); + + if (lowerTitle.StartsWith("imdb:") || lowerTitle.StartsWith("imdbid:")) + { + var slug = lowerTitle.Split(':')[1].Trim(); + + string imdbid = slug; + + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) + { + return new List<Movie>(); + } + + try + { + return new List<Movie> { GetMovieInfo(imdbid) }; + } + catch (SeriesNotFoundException) + { + return new List<Movie>(); + } + } + + var searchTerm = lowerTitle.Replace("_", "+").Replace(" ", "+").Replace(".", "+"); + + var firstChar = searchTerm.First(); + + var request = _movieBuilder.Create() + .SetSegment("route", "search") + .SetSegment("id", "movie") + .SetSegment("secondaryRoute", "") + .AddQueryParam("query", searchTerm) + .AddQueryParam("year", yearTerm) + .AddQueryParam("include_adult", false) + .Build(); + + request.AllowAutoRedirect = true; + request.SuppressHttpError = true; + + /*var imdbRequest = new HttpRequest("https://v2.sg.media-imdb.com/suggests/" + firstChar + "/" + searchTerm + ".json"); + + var response = _httpClient.Get(imdbRequest); + + var imdbCallback = "imdb$" + searchTerm + "("; + + var responseCleaned = response.Content.Replace(imdbCallback, "").TrimEnd(")"); + + _logger.Warn("Cleaned response: " + responseCleaned); + + ImdbResource json = JsonConvert.DeserializeObject<ImdbResource>(responseCleaned); + + _logger.Warn("Json object: " + json); + + _logger.Warn("Crash ahead.");*/ + + var response = _httpClient.Get<MovieSearchRoot>(request); + + var movieResults = response.Resource.results; + + return movieResults.SelectList(MapMovie); + } + public List<Series> SearchForNewSeries(string title) { try @@ -84,11 +294,15 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + + var httpRequest = _requestBuilder.Create() .SetSegment("route", "search") .AddQueryParam("term", title.ToLower().Trim()) .Build(); + + var httpResponse = _httpClient.Get<List<ShowResource>>(httpRequest); return httpResponse.Resource.SelectList(MapSeries); @@ -104,6 +318,51 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + private Movie MapMovie(MovieResult result) + { + var imdbMovie = new Movie(); + imdbMovie.TmdbId = result.id; + try + { + imdbMovie.SortTitle = Parser.Parser.NormalizeTitle(result.title); + imdbMovie.Title = result.title; + string titleSlug = ToUrlSlug(result.title); + imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-"); + + if (result.release_date.IsNotNullOrWhiteSpace()) + { + imdbMovie.Year = DateTime.Parse(result.release_date).Year; + } + //var slugResult = _movieService.FindByTitleSlug(imdbMovie.TitleSlug); + //if (slugResult != null) + //{ + // _logger.Debug("Movie with this title slug already exists. Adding year..."); + //} + imdbMovie.TitleSlug += "-" + imdbMovie.Year.ToString(); + + imdbMovie.Images = new List<MediaCover.MediaCover>(); + imdbMovie.Overview = result.overview; + try + { + string url = result.poster_path; + var imdbPoster = _configService.GetCoverForURL(result.poster_path, MediaCoverTypes.Poster); + imdbMovie.Images.Add(imdbPoster); + } + catch (Exception e) + { + _logger.Debug(result); + } + + return imdbMovie; + } + catch (Exception e) + { + _logger.Error(e, "Error occured while searching for new movies."); + } + + return null; + } + private static Series MapSeries(ShowResource show) { var series = new Series(); @@ -259,5 +518,29 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return MediaCoverTypes.Unknown; } } + + public static string ToUrlSlug(string value) + { + //First to lower case + value = value.ToLowerInvariant(); + + //Remove all accents + var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value); + value = Encoding.ASCII.GetString(bytes); + + //Replace spaces + value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled); + + //Remove invalid chars + value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled); + + //Trim dashes from end + value = value.Trim('-', '_'); + + //Replace double occurences of - or _ + value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled); + + return value; + } } } diff --git a/src/NzbDrone.Core/MetadataSource/TmdbConfigurationService.cs b/src/NzbDrone.Core/MetadataSource/TmdbConfigurationService.cs new file mode 100644 index 000000000..2b960f0de --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/TmdbConfigurationService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.MediaCover; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Http; +using NzbDrone.Common.Cloud; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; + +namespace NzbDrone.Core.MetadataSource +{ + public interface ITmdbConfigService + { + MediaCover.MediaCover GetCoverForURL(string url, MediaCover.MediaCoverTypes type); + } + + class TmdbConfigService : ITmdbConfigService + { + private readonly ICached<ConfigResource> _configurationCache; + private readonly IHttpClient _httpClient; + private readonly IHttpRequestBuilderFactory _tmdbBuilder; + + public TmdbConfigService(ICacheManager cacheManager, IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder) + { + _configurationCache = cacheManager.GetCache<ConfigResource>(GetType(), "configuration_cache"); + _httpClient = httpClient; + _tmdbBuilder = requestBuilder.TMDBSingle; + } + + public MediaCover.MediaCover GetCoverForURL(string url, MediaCover.MediaCoverTypes type) + { + if (_configurationCache.Count == 0) + { + RefreshCache(); + } + + var images = _configurationCache.Find("configuration").images; + + var cover = new MediaCover.MediaCover(); + cover.CoverType = type; + + var realUrl = images.base_url; + + switch (type) + { + case MediaCoverTypes.Banner: + realUrl += images.backdrop_sizes.Last(); + break; + case MediaCoverTypes.Poster: + realUrl += images.poster_sizes.Last(); + break; + default: + realUrl += "original"; + break; + } + + realUrl += url; + + cover.Url = realUrl; + + return cover; + } + + private void RefreshCache() + { + var request = _tmdbBuilder.Create().SetSegment("route", "configuration").Build(); + + var response = _httpClient.Get<ConfigResource>(request); + + if (response.Resource.images != null) + { + _configurationCache.Set("configuration", response.Resource); + } + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatistics.cs b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs new file mode 100644 index 000000000..7ea4dabdb --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public class MovieStatistics : ResultSet + { + public int MovieId { get; set; } + public string NextAiringString { get; set; } + public string PreviousAiringString { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + public List<SeasonStatistics> SeasonStatistics { get; set; } + + public DateTime? NextAiring + { + get + { + DateTime nextAiring; + + if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; + + return nextAiring; + } + } + + public DateTime? PreviousAiring + { + get + { + DateTime previousAiring; + + if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; + + return previousAiring; + } + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs new file mode 100644 index 000000000..32950944d --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsRepository + { + List<SeasonStatistics> MovieStatistics(); + List<SeasonStatistics> MovieStatistics(int movieId); + } + + public class MovieStatisticsRepository : IMovieStatisticsRepository + { + private readonly IMainDatabase _database; + + public MovieStatisticsRepository(IMainDatabase database) + { + _database = database; + } + + public List<SeasonStatistics> MovieStatistics() + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("currentDate", DateTime.UtcNow); + + var sb = new StringBuilder(); + sb.AppendLine(GetSelectClause()); + sb.AppendLine(GetEpisodeFilesJoin()); + sb.AppendLine(GetGroupByClause()); + var queryText = sb.ToString(); + + return new List<SeasonStatistics>(); + + return mapper.Query<SeasonStatistics>(queryText); + } + + public List<SeasonStatistics> MovieStatistics(int movieId) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("currentDate", DateTime.UtcNow); + mapper.AddParameter("movieId", movieId); + + var sb = new StringBuilder(); + sb.AppendLine(GetSelectClause()); + sb.AppendLine(GetEpisodeFilesJoin()); + sb.AppendLine("WHERE Episodes.MovieId = @movieId"); + sb.AppendLine(GetGroupByClause()); + var queryText = sb.ToString(); + + return new List<SeasonStatistics>(); + + return mapper.Query<SeasonStatistics>(queryText); + } + + private string GetSelectClause() + { + return @"SELECT Episodes.*, SUM(EpisodeFiles.Size) as SizeOnDisk FROM + (SELECT + Episodes.MovieId, + Episodes.SeasonNumber, + SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount, + SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, + SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, + MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString, + MAX(CASE WHEN AirDateUtc >= @currentDate OR EpisodeFileId = 0 AND Monitored = 0 THEN NULL ELSE AirDateUtc END) AS PreviousAiringString + FROM Episodes + GROUP BY Episodes.MovieId, Episodes.SeasonNumber) as Episodes"; + } + + private string GetGroupByClause() + { + return "GROUP BY Episodes.MovieId, Episodes.SeasonNumber"; + } + + private string GetEpisodeFilesJoin() + { + return @"LEFT OUTER JOIN EpisodeFiles + ON EpisodeFiles.MovieId = Episodes.MovieId + AND EpisodeFiles.SeasonNumber = Episodes.SeasonNumber"; + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs new file mode 100644 index 000000000..68dabd609 --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsService + { + List<MovieStatistics> MovieStatistics(); + MovieStatistics MovieStatistics(int movieId); + } + + public class MovieStatisticsService : IMovieStatisticsService + { + private readonly IMovieStatisticsRepository _movieStatisticsRepository; + + public MovieStatisticsService(IMovieStatisticsRepository movieStatisticsRepository) + { + _movieStatisticsRepository = movieStatisticsRepository; + } + + public List<MovieStatistics> MovieStatistics() + { + var seasonStatistics = _movieStatisticsRepository.MovieStatistics(); + + return seasonStatistics.GroupBy(s => s.MovieId).Select(s => MapMovieStatistics(s.ToList())).ToList(); + } + + public MovieStatistics MovieStatistics(int movieId) + { + var stats = _movieStatisticsRepository.MovieStatistics(movieId); + + if (stats == null || stats.Count == 0) return new MovieStatistics(); + + return MapMovieStatistics(stats); + } + + private MovieStatistics MapMovieStatistics(List<SeasonStatistics> seasonStatistics) + { + var movieStatistics = new MovieStatistics + { + SeasonStatistics = seasonStatistics, + MovieId = seasonStatistics.First().MovieId, + EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount), + EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount), + TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount), + SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk) + }; + + var nextAiring = seasonStatistics.Where(s => s.NextAiring != null) + .OrderBy(s => s.NextAiring) + .FirstOrDefault(); + + var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null) + .OrderBy(s => s.PreviousAiring) + .LastOrDefault(); + + movieStatistics.NextAiringString = nextAiring != null ? nextAiring.NextAiringString : null; + movieStatistics.PreviousAiringString = previousAiring != null ? previousAiring.PreviousAiringString : null; + + return movieStatistics; + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs b/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs new file mode 100644 index 000000000..05da073db --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/SeasonStatistics.cs @@ -0,0 +1,41 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public class SeasonStatistics : ResultSet + { + public int MovieId { get; set; } + public int SeasonNumber { get; set; } + public string NextAiringString { get; set; } + public string PreviousAiringString { get; set; } + public int EpisodeFileCount { get; set; } + public int EpisodeCount { get; set; } + public int TotalEpisodeCount { get; set; } + public long SizeOnDisk { get; set; } + + public DateTime? NextAiring + { + get + { + DateTime nextAiring; + + if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null; + + return nextAiring; + } + } + + public DateTime? PreviousAiring + { + get + { + DateTime previousAiring; + + if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null; + + return previousAiring; + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs b/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs index 86738fbcc..b1c4e3bad 100644 --- a/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs +++ b/src/NzbDrone.Core/Notifications/Boxcar/BoxcarProxy.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Notifications.Boxcar try { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings); return null; diff --git a/src/NzbDrone.Core/Notifications/Email/Email.cs b/src/NzbDrone.Core/Notifications/Email/Email.cs index 71bb9130c..27e991332 100644 --- a/src/NzbDrone.Core/Notifications/Email/Email.cs +++ b/src/NzbDrone.Core/Notifications/Email/Email.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Notifications.Email public override void OnGrab(GrabMessage grabMessage) { - const string subject = "Sonarr [TV] - Grabbed"; + const string subject = "Radarr [TV] - Grabbed"; var body = string.Format("{0} sent to queue.", grabMessage.Message); _emailService.SendEmail(Settings, subject, body); @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Notifications.Email public override void OnDownload(DownloadMessage message) { - const string subject = "Sonarr [TV] - Downloaded"; + const string subject = "Radarr [TV] - Downloaded"; var body = string.Format("{0} Downloaded and sorted.", message.Message); _emailService.SendEmail(Settings, subject, body); diff --git a/src/NzbDrone.Core/Notifications/Email/EmailService.cs b/src/NzbDrone.Core/Notifications/Email/EmailService.cs index f1469d2e9..84d1ed298 100644 --- a/src/NzbDrone.Core/Notifications/Email/EmailService.cs +++ b/src/NzbDrone.Core/Notifications/Email/EmailService.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Notifications.Email try { - SendEmail(settings, "Sonarr - Test Notification", body); + SendEmail(settings, "Radarr - Test Notification", body); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs index 73f6bc3b5..551f74e87 100644 --- a/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs +++ b/src/NzbDrone.Core/Notifications/Growl/GrowlService.cs @@ -146,7 +146,7 @@ namespace NzbDrone.Core.Notifications.Growl Register(settings.Host, settings.Port, settings.Password); const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, "TEST", settings.Host, settings.Port, settings.Password); } diff --git a/src/NzbDrone.Core/Notifications/Join/Join.cs b/src/NzbDrone.Core/Notifications/Join/Join.cs index 747a141e1..4e1f81105 100644 --- a/src/NzbDrone.Core/Notifications/Join/Join.cs +++ b/src/NzbDrone.Core/Notifications/Join/Join.cs @@ -18,14 +18,14 @@ namespace NzbDrone.Core.Notifications.Join public override void OnGrab(GrabMessage grabMessage) { - const string title = "Sonarr - Episode Grabbed"; + const string title = "Radarr - Episode Grabbed"; _proxy.SendNotification(title, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - const string title = "Sonarr - Episode Downloaded"; + const string title = "Radarr - Episode Downloaded"; _proxy.SendNotification(title, message.Message, Settings); } diff --git a/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs b/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs index 13451c912..437d10625 100644 --- a/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs +++ b/src/NzbDrone.Core/Notifications/Join/JoinProxy.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Notifications.Join public ValidationFailure Test(JoinSettings settings) { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr."; + const string body = "This is a test message from Radarr."; try { diff --git a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs index 795095c44..7c68fc306 100644 --- a/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs +++ b/src/NzbDrone.Core/Notifications/MediaBrowser/MediaBrowser.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser public override void OnGrab(GrabMessage grabMessage) { - const string title = "Sonarr - Grabbed"; + const string title = "Radarr - Grabbed"; if (Settings.Notify) { @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Notifications.MediaBrowser public override void OnDownload(DownloadMessage message) { - const string title = "Sonarr - Downloaded"; + const string title = "Radarr - Downloaded"; if (Settings.Notify) { diff --git a/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroidProxy.cs b/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroidProxy.cs index ce4d97790..8b851f24d 100644 --- a/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroidProxy.cs +++ b/src/NzbDrone.Core/Notifications/NotifyMyAndroid/NotifyMyAndroidProxy.cs @@ -69,7 +69,7 @@ namespace NzbDrone.Core.Notifications.NotifyMyAndroid try { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; Verify(settings.ApiKey); SendNotification(title, body, settings.ApiKey, (NotifyMyAndroidPriority)settings.Priority); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs b/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs index 844b3bb0a..e38e87f96 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexClient.cs @@ -18,13 +18,13 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnGrab(GrabMessage grabMessage) { - const string header = "Sonarr [TV] - Grabbed"; + const string header = "Radarr [TV] - Grabbed"; _plexClientService.Notify(Settings, header, grabMessage.Message); } public override void OnDownload(DownloadMessage message) { - const string header = "Sonarr [TV] - Downloaded"; + const string header = "Radarr [TV] - Downloaded"; _plexClientService.Notify(Settings, header, message.Message); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs b/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs index 63affad8d..e96c2c4f2 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexHomeTheater.cs @@ -23,14 +23,14 @@ namespace NzbDrone.Core.Notifications.Plex public override void OnGrab(GrabMessage grabMessage) { - const string header = "Sonarr - Grabbed"; + const string header = "Radarr - Grabbed"; Notify(Settings, header, grabMessage.Message); } public override void OnDownload(DownloadMessage message) { - const string header = "Sonarr - Downloaded"; + const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index 10b500b71..0742ca049 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Notifications.Plex { return Json.Deserialize<PlexMediaContainerLegacy>(response.Content) .Sections - .Where(d => d.Type == "show") + .Where(d => d.Type == "movie") .Select(s => new PlexSection { Id = s.Id, @@ -62,7 +62,7 @@ namespace NzbDrone.Core.Notifications.Plex return Json.Deserialize<PlexResponse<PlexSectionsContainer>>(response.Content) .MediaContainer .Sections - .Where(d => d.Type == "show") + .Where(d => d.Type == "movie") .ToList(); } diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs index 727c63e35..67b8efe23 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerService.cs @@ -98,7 +98,7 @@ namespace NzbDrone.Core.Notifications.Plex { if (version >= new Version(1, 3, 0) && version < new Version(1, 3, 1)) { - throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Sonarr", version); + throw new PlexVersionException("Found version {0}, upgrade to PMS 1.3.1 to fix library updating and then restart Radarr", version); } } diff --git a/src/NzbDrone.Core/Notifications/Prowl/ProwlService.cs b/src/NzbDrone.Core/Notifications/Prowl/ProwlService.cs index bf56dbad3..8b2a681db 100644 --- a/src/NzbDrone.Core/Notifications/Prowl/ProwlService.cs +++ b/src/NzbDrone.Core/Notifications/Prowl/ProwlService.cs @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Notifications.Prowl Verify(settings.ApiKey); const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings.ApiKey); } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs index 684ff702b..be2afe912 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBullet.cs @@ -18,14 +18,14 @@ namespace NzbDrone.Core.Notifications.PushBullet public override void OnGrab(GrabMessage grabMessage) { - const string title = "Sonarr - Episode Grabbed"; + const string title = "Radarr - Episode Grabbed"; _proxy.SendNotification(title, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - const string title = "Sonarr - Episode Downloaded"; + const string title = "Radarr - Episode Downloaded"; _proxy.SendNotification(title, message.Message, Settings); } diff --git a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs index 753a95d2b..ad3f23c9d 100644 --- a/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs +++ b/src/NzbDrone.Core/Notifications/PushBullet/PushBulletProxy.cs @@ -92,8 +92,8 @@ namespace NzbDrone.Core.Notifications.PushBullet { try { - const string title = "Sonarr - Test Notification"; - const string body = "This is a test message from Sonarr"; + const string title = "Radarr - Test Notification"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings); } diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs index 97b3215d8..44026325f 100644 --- a/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotProxy.cs @@ -65,7 +65,7 @@ namespace NzbDrone.Core.Notifications.Pushalot try { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings); } diff --git a/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs b/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs index a0fbd08e1..de3ebb1ff 100644 --- a/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs +++ b/src/NzbDrone.Core/Notifications/Pushalot/PushalotSettings.cs @@ -28,7 +28,7 @@ namespace NzbDrone.Core.Notifications.Pushalot [FieldDefinition(1, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushalotPriority))] public int Priority { get; set; } - [FieldDefinition(2, Label = "Image", Type = FieldType.Checkbox, HelpText = "Include Sonarr logo with notifications")] + [FieldDefinition(2, Label = "Image", Type = FieldType.Checkbox, HelpText = "Include Radarr logo with notifications")] public bool Image { get; set; } public bool IsValid => !string.IsNullOrWhiteSpace(AuthToken); diff --git a/src/NzbDrone.Core/Notifications/Pushover/PushoverService.cs b/src/NzbDrone.Core/Notifications/Pushover/PushoverService.cs index 940ab9ffd..9960bc18a 100644 --- a/src/NzbDrone.Core/Notifications/Pushover/PushoverService.cs +++ b/src/NzbDrone.Core/Notifications/Pushover/PushoverService.cs @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Notifications.Pushover try { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings); } diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index 498d17349..5e581a25f 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -101,7 +101,7 @@ namespace NzbDrone.Core.Notifications.Slack { try { - var message = $"Test message from Sonarr posted at {DateTime.Now}"; + var message = $"Test message from Radarr posted at {DateTime.Now}"; var payload = new SlackPayload { IconEmoji = Settings.Icon, diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs index e7259d753..81e59322a 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramService.cs @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Notifications.Telegram try { const string title = "Test Notification"; - const string body = "This is a test message from Sonarr"; + const string body = "This is a test message from Radarr"; SendNotification(title, body, settings); } diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs index 6c894b228..f6e334194 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterService.cs @@ -125,7 +125,7 @@ namespace NzbDrone.Core.Notifications.Twitter { try { - var body = "Sonarr: Test Message @ " + DateTime.Now; + var body = "Radarr: Test Message @ " + DateTime.Now; SendNotification(body, settings); } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs index c6a0c82df..08fdbfaa4 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/Xbmc.cs @@ -23,14 +23,14 @@ namespace NzbDrone.Core.Notifications.Xbmc public override void OnGrab(GrabMessage grabMessage) { - const string header = "Sonarr - Grabbed"; + const string header = "Radarr - Grabbed"; Notify(Settings, header, grabMessage.Message); } public override void OnDownload(DownloadMessage message) { - const string header = "Sonarr - Downloaded"; + const string header = "Radarr - Downloaded"; Notify(Settings, header, message.Message); UpdateAndClean(message.Series, message.OldFiles.Any()); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5d43f5d5f..e69a850e7 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -183,6 +183,18 @@ <Compile Include="Datastore\Migration\002_remove_tvrage_imdb_unique_constraint.cs" /> <Compile Include="Datastore\Migration\003_remove_clean_title_from_scene_mapping.cs" /> <Compile Include="Datastore\Migration\004_updated_history.cs" /> + <Compile Include="Datastore\Migration\118_update_movie_slug.cs" /> + <Compile Include="Datastore\Migration\117_update_movie_file.cs" /> + <Compile Include="Datastore\Migration\116_update_movie_sorttitle_again.cs" /> + <Compile Include="Datastore\Migration\115_update_movie_sorttitle.cs" /> + <Compile Include="Datastore\Migration\111_remove_bitmetv.cs" /> + <Compile Include="Datastore\Migration\112_remove_torrentleech.cs" /> + <Compile Include="Datastore\Migration\114_remove_fanzub.cs" /> + <Compile Include="Datastore\Migration\113_remove_broadcasthenet.cs" /> + <Compile Include="Datastore\Migration\108_update_schedule_interval.cs" /> + <Compile Include="Datastore\Migration\107_fix_movie_files.cs" /> + <Compile Include="Datastore\Migration\106_add_tmdb_stuff.cs" /> + <Compile Include="Datastore\Migration\105_fix_history_movieId.cs" /> <Compile Include="Datastore\Migration\005_added_eventtype_to_history.cs" /> <Compile Include="Datastore\Migration\006_add_index_to_log_time.cs" /> <Compile Include="Datastore\Migration\007_add_renameEpisodes_to_naming.cs" /> @@ -249,6 +261,7 @@ <Compile Include="Datastore\Migration\068_add_release_restrictions.cs" /> <Compile Include="Datastore\Migration\069_quality_proper.cs" /> <Compile Include="Datastore\Migration\070_delay_profile.cs" /> + <Compile Include="Datastore\Migration\104_add_moviefiles_table.cs" /> <Compile Include="Datastore\Migration\096_disable_kickass.cs" /> <Compile Include="Datastore\Migration\095_add_additional_episodes_index.cs" /> <Compile Include="Datastore\Migration\103_fix_metadata_file_extensions.cs" /> @@ -281,6 +294,8 @@ <Compile Include="Datastore\Migration\098_remove_titans_of_tv.cs"> <SubType>Code</SubType> </Compile> + <Compile Include="Datastore\Migration\110_add_physical_release_to_table.cs" /> + <Compile Include="Datastore\Migration\109_add_movie_formats_to_naming_config.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" /> @@ -457,6 +472,7 @@ <Compile Include="Download\Clients\Vuze\Vuze.cs" /> <Compile Include="Download\CompletedDownloadService.cs" /> <Compile Include="Download\DownloadEventHub.cs" /> + <Compile Include="Download\MovieGrabbedEvent.cs" /> <Compile Include="Download\TrackedDownloads\DownloadMonitoringService.cs" /> <Compile Include="Download\TrackedDownloads\TrackedDownload.cs" /> <Compile Include="Download\TrackedDownloads\TrackedDownloadService.cs" /> @@ -488,6 +504,7 @@ <Compile Include="Exceptions\BadRequestException.cs" /> <Compile Include="Exceptions\DownstreamException.cs" /> <Compile Include="Exceptions\NzbDroneClientException.cs" /> + <Compile Include="Exceptions\MovieNotFoundExceptions.cs" /> <Compile Include="Exceptions\SeriesNotFoundException.cs" /> <Compile Include="Exceptions\ReleaseDownloadException.cs" /> <Compile Include="Exceptions\StatusCodeToExceptions.cs" /> @@ -561,26 +578,27 @@ <Compile Include="Http\CloudFlare\CloudFlareHttpInterceptor.cs" /> <Compile Include="Http\HttpProxySettingsProvider.cs" /> <Compile Include="Http\TorcacheHttpInterceptor.cs" /> - <Compile Include="Indexers\BitMeTv\BitMeTv.cs" /> - <Compile Include="Indexers\BitMeTv\BitMeTvSettings.cs" /> - <Compile Include="Indexers\BitMeTv\BitMeTvRequestGenerator.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetRequestGenerator.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNet.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetSettings.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetParser.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetTorrent.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetTorrentQuery.cs" /> - <Compile Include="Indexers\BroadcastheNet\BroadcastheNetTorrents.cs" /> + <Compile Include="IndexerSearch\Definitions\MovieSearchCriteria.cs" /> + <Compile Include="IndexerSearch\MoviesSearchCommand.cs" /> + <Compile Include="IndexerSearch\MoviesSearchService.cs" /> + <Compile Include="Indexers\AwesomeHD\AwesomeHDRssParser.cs" /> <Compile Include="Indexers\DownloadProtocol.cs" /> <Compile Include="Indexers\Exceptions\ApiKeyException.cs" /> <Compile Include="Indexers\Exceptions\IndexerException.cs" /> <Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" /> <Compile Include="Indexers\Exceptions\UnsupportedFeedException.cs" /> <Compile Include="Indexers\EzrssTorrentRssParser.cs" /> - <Compile Include="Indexers\Fanzub\Fanzub.cs" /> - <Compile Include="Indexers\Fanzub\FanzubRequestGenerator.cs" /> - <Compile Include="Indexers\Fanzub\FanzubSettings.cs" /> <Compile Include="Indexers\FetchAndParseRssService.cs" /> + <Compile Include="Indexers\AwesomeHD\AwesomeHD.cs" /> + <Compile Include="Indexers\AwesomeHD\AwesomeHDApi.cs" /> + <Compile Include="Indexers\AwesomeHD\AwesomeHDRequestGenerator.cs" /> + <Compile Include="Indexers\AwesomeHD\AwesomeHDSettings.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcorn.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcornApi.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcornInfo.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcornParser.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcornRequestGenerator.cs" /> + <Compile Include="Indexers\PassThePopcorn\PassThePopcornSettings.cs" /> <Compile Include="Indexers\HDBits\HDBits.cs" /> <Compile Include="Indexers\HDBits\HDBitsApi.cs" /> <Compile Include="Indexers\HDBits\HDBitsParser.cs" /> @@ -624,6 +642,11 @@ <Compile Include="Indexers\Omgwtfnzbs\OmgwtfnzbsRssParser.cs" /> <Compile Include="Indexers\Omgwtfnzbs\OmgwtfnzbsSettings.cs" /> <Compile Include="Indexers\HttpIndexerBase.cs" /> + <Compile Include="Indexers\TorrentPotato\TorrentPotato.cs" /> + <Compile Include="Indexers\TorrentPotato\TorrentPotatoParser.cs" /> + <Compile Include="Indexers\TorrentPotato\TorrentPotatoRequestGenerator.cs" /> + <Compile Include="Indexers\TorrentPotato\TorrentPotatoResponse.cs" /> + <Compile Include="Indexers\TorrentPotato\TorrentPotatoSettings.cs" /> <Compile Include="Indexers\Rarbg\Rarbg.cs" /> <Compile Include="Indexers\Rarbg\RarbgRequestGenerator.cs" /> <Compile Include="Indexers\Rarbg\RarbgResponse.cs" /> @@ -635,9 +658,6 @@ <Compile Include="Indexers\RssSyncCommand.cs" /> <Compile Include="Indexers\RssSyncCompleteEvent.cs" /> <Compile Include="Indexers\RssSyncService.cs" /> - <Compile Include="Indexers\Torrentleech\TorrentleechRequestGenerator.cs" /> - <Compile Include="Indexers\Torrentleech\Torrentleech.cs" /> - <Compile Include="Indexers\Torrentleech\TorrentleechSettings.cs" /> <Compile Include="Indexers\TorrentRss\TorrentRssIndexer.cs" /> <Compile Include="Indexers\TorrentRss\TorrentRssIndexerParserSettings.cs" /> <Compile Include="Indexers\TorrentRss\TorrentRssIndexerRequestGenerator.cs" /> @@ -694,6 +714,22 @@ <Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" /> <Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" /> <Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" /> + <Compile Include="MediaFiles\Commands\DownloadedMovieScanCommand.cs" /> + <Compile Include="MediaFiles\Commands\RenameMovieCommand.cs" /> + <Compile Include="MediaFiles\Commands\RenameMovieFilesCommand.cs" /> + <Compile Include="MediaFiles\Commands\RescanMovieCommand.cs" /> + <Compile Include="MediaFiles\DownloadedMovieCommandService.cs" /> + <Compile Include="MediaFiles\DownloadedMovieImportService.cs" /> + <Compile Include="MediaFiles\MovieFileMovingService.cs" /> + <Compile Include="MediaFiles\Events\MovieDownloadedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieFileAddedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieFileDeletedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieFolderCreatedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieImportedEvent.cs" /> + <Compile Include="MediaFiles\MovieFileRepository.cs" /> + <Compile Include="MediaFiles\MovieFileMoveResult.cs" /> + <Compile Include="MediaFiles\MovieFile.cs" /> + <Compile Include="MediaFiles\EpisodeImport\ImportApprovedMovie.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportMode.cs" /> <Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" /> <Compile Include="MediaFiles\Commands\RenameSeriesCommand.cs" /> @@ -734,7 +770,10 @@ <Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeFolderCreatedEvent.cs" /> <Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieRenamedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieScannedEvent.cs" /> + <Compile Include="MediaFiles\Events\MovieScanSkippedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" /> <Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" /> <Compile Include="MediaFiles\FileDateType.cs" /> @@ -752,7 +791,10 @@ <Compile Include="MediaFiles\RecycleBinProvider.cs" /> <Compile Include="MediaFiles\RenameEpisodeFilePreview.cs" /> <Compile Include="MediaFiles\RenameEpisodeFileService.cs" /> + <Compile Include="MediaFiles\RenameMovieFilePreview.cs" /> + <Compile Include="MediaFiles\RenameMovieFileService.cs" /> <Compile Include="MediaFiles\SameFilenameException.cs" /> + <Compile Include="MediaFiles\UpdateMovieFileService.cs" /> <Compile Include="MediaFiles\UpdateEpisodeFileService.cs" /> <Compile Include="MediaFiles\UpgradeMediaFileService.cs" /> <Compile Include="Messaging\Commands\BackendCommandAttribute.cs" /> @@ -778,13 +820,18 @@ <Compile Include="Messaging\Events\IEventAggregator.cs" /> <Compile Include="Messaging\Events\IHandle.cs" /> <Compile Include="Messaging\IProcessMessage.cs" /> + <Compile Include="MetadataSource\IProvideMovieInfo.cs" /> + <Compile Include="MetadataSource\ISearchForNewMovie.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ActorResource.cs" /> + <Compile Include="MetadataSource\SkyHook\Resource\ConfigurationResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\EpisodeResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ImageResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\RatingResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\SeasonResource.cs" /> + <Compile Include="MetadataSource\SkyHook\Resource\MovieResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\ShowResource.cs" /> <Compile Include="MetadataSource\SkyHook\Resource\TimeOfDayResource.cs" /> + <Compile Include="MetadataSource\SkyHook\Resource\TMDBResources.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxy.cs" /> <Compile Include="MetadataSource\SearchSeriesComparer.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookException.cs" /> @@ -809,6 +856,7 @@ <Compile Include="Extras\Metadata\MetadataType.cs" /> <Compile Include="MetadataSource\IProvideSeriesInfo.cs" /> <Compile Include="MetadataSource\ISearchForNewSeries.cs" /> + <Compile Include="MetadataSource\TmdbConfigurationService.cs" /> <Compile Include="Notifications\Join\JoinAuthException.cs" /> <Compile Include="Notifications\Join\JoinInvalidDeviceException.cs" /> <Compile Include="Notifications\Join\JoinResponseModel.cs" /> @@ -864,6 +912,9 @@ <Compile Include="Parser\IsoLanguage.cs" /> <Compile Include="Parser\IsoLanguages.cs" /> <Compile Include="Parser\LanguageParser.cs" /> + <Compile Include="Parser\Model\LocalMovie.cs" /> + <Compile Include="Parser\Model\ParsedMovieInfo.cs" /> + <Compile Include="Parser\Model\RemoteMovie.cs" /> <Compile Include="Profiles\Delay\DelayProfile.cs" /> <Compile Include="Profiles\Delay\DelayProfileService.cs" /> <Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" /> @@ -1021,6 +1072,10 @@ </Compile> <Compile Include="RootFolders\UnmappedFolder.cs" /> <Compile Include="Security.cs" /> + <Compile Include="MovieStats\SeasonStatistics.cs" /> + <Compile Include="MovieStats\MovieStatistics.cs" /> + <Compile Include="MovieStats\MovieStatisticsRepository.cs" /> + <Compile Include="MovieStats\MovieStatisticsService.cs" /> <Compile Include="SeriesStats\SeasonStatistics.cs" /> <Compile Include="SeriesStats\SeriesStatistics.cs" /> <Compile Include="SeriesStats\SeriesStatisticsRepository.cs" /> @@ -1045,6 +1100,7 @@ <Compile Include="Tv\Actor.cs" /> <Compile Include="Tv\AddSeriesOptions.cs" /> <Compile Include="Tv\Commands\MoveSeriesCommand.cs" /> + <Compile Include="Tv\Commands\RefreshMovieCommand.cs" /> <Compile Include="Tv\Commands\RefreshSeriesCommand.cs" /> <Compile Include="Tv\Episode.cs" /> <Compile Include="Tv\EpisodeAddedService.cs" /> @@ -1055,29 +1111,44 @@ </Compile> <Compile Include="Tv\EpisodeService.cs" /> <Compile Include="Tv\Events\EpisodeInfoRefreshedEvent.cs" /> + <Compile Include="Tv\Events\MovieAddedEvent.cs" /> <Compile Include="Tv\Events\SeriesAddedEvent.cs" /> + <Compile Include="Tv\Events\MovieDeletedEvent.cs" /> <Compile Include="Tv\Events\SeriesDeletedEvent.cs" /> + <Compile Include="Tv\Events\MovieEditedEvent.cs" /> <Compile Include="Tv\Events\SeriesEditedEvent.cs" /> <Compile Include="Tv\Events\SeriesMovedEvent.cs" /> + <Compile Include="Tv\Events\MovieRefreshStartingEvent.cs" /> <Compile Include="Tv\Events\SeriesRefreshStartingEvent.cs" /> + <Compile Include="Tv\Events\MovieUpdateEvent.cs" /> <Compile Include="Tv\Events\SeriesUpdatedEvent.cs" /> <Compile Include="Tv\MonitoringOptions.cs" /> <Compile Include="Tv\MoveSeriesService.cs" /> <Compile Include="Tv\Ratings.cs" /> <Compile Include="Tv\RefreshEpisodeService.cs" /> + <Compile Include="Tv\RefreshMovieService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\Season.cs" /> + <Compile Include="Tv\Movie.cs" /> <Compile Include="Tv\Series.cs" /> + <Compile Include="Tv\MovieAddedHandler.cs" /> <Compile Include="Tv\SeriesAddedHandler.cs" /> + <Compile Include="Tv\MovieRepository.cs" /> + <Compile Include="Tv\MovieEditedService.cs" /> + <Compile Include="Tv\MovieScannedHandler.cs" /> <Compile Include="Tv\SeriesScannedHandler.cs" /> <Compile Include="Tv\SeriesEditedService.cs" /> <Compile Include="Tv\SeriesRepository.cs" /> + <Compile Include="Tv\MovieService.cs" /> <Compile Include="Tv\SeriesService.cs"> <SubType>Code</SubType> </Compile> + <Compile Include="Tv\MovieStatusType.cs" /> <Compile Include="Tv\SeriesStatusType.cs" /> + <Compile Include="Tv\MovieTitleNormalizer.cs" /> <Compile Include="Tv\SeriesTitleNormalizer.cs" /> <Compile Include="Tv\SeriesTypes.cs" /> + <Compile Include="Tv\ShouldRefreshMovie.cs" /> <Compile Include="Tv\ShouldRefreshSeries.cs" /> <Compile Include="Update\Commands\ApplicationUpdateCommand.cs" /> <Compile Include="Update\InstallUpdateService.cs" /> @@ -1104,6 +1175,9 @@ <Compile Include="Validation\Paths\FolderWritableValidator.cs" /> <Compile Include="Validation\Paths\PathExistsValidator.cs" /> <Compile Include="Validation\Paths\PathValidator.cs" /> + <Compile Include="Validation\Paths\MoviePathValidation.cs" /> + <Compile Include="Validation\Paths\MovieAncestorValidator.cs" /> + <Compile Include="Validation\Paths\MovieExistsValidator.cs" /> <Compile Include="Validation\Paths\StartupFolderValidator.cs" /> <Compile Include="Validation\Paths\RootFolderValidator.cs" /> <Compile Include="Validation\Paths\SeriesAncestorValidator.cs" /> diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 31cbd53ef..25399e6de 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -17,11 +17,14 @@ namespace NzbDrone.Core.Organizer public interface IBuildFileNames { string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); + string BuildFilePath(Movie movie, string fileName, string extension); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); string BuildSeasonPath(Series series, int seasonNumber); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); } public class FileNameBuilder : IBuildFileNames @@ -55,6 +58,9 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex MovieTitleRegex = new Regex(@"(?<token>\{(?:Movie)(?<separator>[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); @@ -73,6 +79,7 @@ namespace NzbDrone.Core.Organizer { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; + //_movieFormatCache = cacheManager.GetCache<MovieFormat>(GetType(), "movieFormat"); _episodeFormatCache = cacheManager.GetCache<EpisodeFormat[]>(GetType(), "episodeFormat"); _absoluteEpisodeFormatCache = cacheManager.GetCache<AbsoluteEpisodeFormat[]>(GetType(), "absoluteEpisodeFormat"); _logger = logger; @@ -136,6 +143,34 @@ namespace NzbDrone.Core.Organizer return fileName; } + public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(movieFile); + } + + //TODO: Update namingConfig for Movies! + var pattern = namingConfig.StandardMovieFormat; + var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year + AddQualityTokens(tokenHandlers, movie, movieFile); + AddMediaInfoTokens(tokenHandlers, movieFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); @@ -145,6 +180,15 @@ namespace NzbDrone.Core.Organizer return Path.Combine(path, fileName + extension); } + public string BuildFilePath(Movie movie, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = movie.Path; + + return Path.Combine(path, fileName + extension); + } + public string BuildSeasonPath(Series series, int seasonNumber) { var path = series.Path; @@ -170,6 +214,8 @@ namespace NzbDrone.Core.Organizer public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) { + return new BasicNamingConfig(); //For now let's be lazy + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); if (episodeFormat == null) @@ -243,6 +289,21 @@ namespace NzbDrone.Core.Organizer return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); } + public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) + { + if(namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); + + return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); + } + public static string CleanTitle(string title) { title = title.Replace("&", "and"); @@ -269,7 +330,9 @@ namespace NzbDrone.Core.Organizer public static string CleanFolderName(string name) { name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); - return name.Trim(' ', '.'); + name = name.Trim(' ', '.'); + + return CleanFileName(name); } private void AddSeriesTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Series series) @@ -402,6 +465,17 @@ namespace NzbDrone.Core.Organizer return pattern; } + private void AddMovieTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Movie movie) + { + tokenHandlers["{Movie Title}"] = m => movie.Title; + tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); + } + + private void AddReleaseDateTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int releaseYear) + { + tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? + } + private void AddSeasonTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, int seasonNumber) { tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); @@ -441,6 +515,18 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Quality Real}"] = m => qualityReal; } + private void AddQualityTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Movie movie, MovieFile movieFile) + { + var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(movie, movieFile.Quality); + var qualityReal = GetQualityReal(movie, movieFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, EpisodeFile episodeFile) { if (episodeFile.MediaInfo == null) return; @@ -545,6 +631,110 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); } + private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile) + { + if (movieFile.MediaInfo == null) return; + + string videoCodec; + switch (movieFile.MediaInfo.VideoCodec) + { + case "AVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = movieFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (movieFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (movieFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = movieFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? + movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + private string GetLanguagesToken(string mediaInfoLanguages) { List<string> tokens = new List<string>(); @@ -724,6 +914,16 @@ namespace NzbDrone.Core.Organizer return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); } + private string GetQualityProper(Movie movie, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + return "Proper"; + } + + return String.Empty; + } + private string GetQualityProper(Series series, QualityModel quality) { if (quality.Revision.Version > 1) @@ -749,6 +949,16 @@ namespace NzbDrone.Core.Organizer return string.Empty; } + private string GetQualityReal(Movie movie, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + private string GetOriginalTitle(EpisodeFile episodeFile) { if (episodeFile.SceneName.IsNullOrWhiteSpace()) @@ -768,6 +978,26 @@ namespace NzbDrone.Core.Organizer return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); } + + private string GetOriginalTitle(MovieFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(MovieFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } } internal sealed class TokenMatch diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 966061fb3..654179b52 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -3,6 +3,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.MediaFiles.MediaInfo; +using System; namespace NzbDrone.Core.Organizer { @@ -13,8 +14,10 @@ namespace NzbDrone.Core.Organizer SampleResult GetDailySample(NamingConfig nameSpec); SampleResult GetAnimeSample(NamingConfig nameSpec); SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec); + SampleResult GetMovieSample(NamingConfig nameSpec); string GetSeriesFolderSample(NamingConfig nameSpec); string GetSeasonFolderSample(NamingConfig nameSpec); + string GetMovieFolderSample(NamingConfig nameSpec); } public class FileNameSampleService : IFilenameSampleService @@ -34,10 +37,19 @@ namespace NzbDrone.Core.Organizer private static EpisodeFile _animeEpisodeFile; private static EpisodeFile _animeMultiEpisodeFile; + private static MovieFile _movieFile; + private static Movie _movie; + public FileNameSampleService(IBuildFileNames buildFileNames) { _buildFileNames = buildFileNames; + _movie = new Movie + { + Title = "Movie Title", + Year = 2010 + }; + _standardSeries = new Series { SeriesType = SeriesTypes.Standard, @@ -106,6 +118,15 @@ namespace NzbDrone.Core.Organizer Subtitles = "Japanese/English" }; + _movieFile = new MovieFile + { + Quality = new QualityModel(Quality.Bluray1080p, new Revision(2)), + RelativePath = "Movie.Title.2010.1080p.BluRay.DTS.x264-EVOLVE.mkv", + SceneName = "Movie.Title.2010.1080p.BluRay.DTS.x264-EVOLVE", + ReleaseGroup = "RlsGrp", + MediaInfo = mediaInfo + }; + _singleEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p, new Revision(2)), @@ -217,6 +238,16 @@ namespace NzbDrone.Core.Organizer return result; } + public SampleResult GetMovieSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + FileName = BuildSample(_movie, _movieFile, nameSpec), + }; + + return result; + } + public string GetSeriesFolderSample(NamingConfig nameSpec) { return _buildFileNames.GetSeriesFolder(_standardSeries, nameSpec); @@ -227,6 +258,11 @@ namespace NzbDrone.Core.Organizer return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec); } + public string GetMovieFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetMovieFolder(_movie, nameSpec); + } + private string BuildSample(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) { try @@ -238,5 +274,17 @@ namespace NzbDrone.Core.Organizer return string.Empty; } } + + private string BuildSample(Movie movie, MovieFile movieFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildFileName(movie, movieFile, nameSpec); + } + catch (NamingFormatException) + { + return string.Empty; + } + } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 930b8a044..9ecfa6b42 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -41,6 +41,18 @@ namespace NzbDrone.Core.Organizer ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number"); } + + public static IRuleBuilderOptions<T, string> ValidMovieFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.MovieTitleRegex)).WithMessage("Must contain movie title"); + } + + public static IRuleBuilderOptions<T, string> ValidMovieFormat<T>(this IRuleBuilder<T, string> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.MovieTitleRegex)).WithMessage("Must contain movie title"); + } } public class ValidStandardEpisodeFormatValidator : PropertyValidator @@ -55,6 +67,8 @@ namespace NzbDrone.Core.Organizer { var value = context.PropertyValue as string; + return true; + if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && !FileNameValidation.OriginalTokenRegex.IsMatch(value)) { @@ -77,6 +91,8 @@ namespace NzbDrone.Core.Organizer { var value = context.PropertyValue as string; + return true; + if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && !FileNameBuilder.AirDateRegex.IsMatch(value) && !FileNameValidation.OriginalTokenRegex.IsMatch(value)) @@ -100,6 +116,8 @@ namespace NzbDrone.Core.Organizer { var value = context.PropertyValue as string; + return true; + if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && !FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) && !FileNameValidation.OriginalTokenRegex.IsMatch(value)) diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..697f72bbb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -11,12 +11,26 @@ namespace NzbDrone.Core.Organizer ValidationFailure ValidateStandardFilename(SampleResult sampleResult); ValidationFailure ValidateDailyFilename(SampleResult sampleResult); ValidationFailure ValidateAnimeFilename(SampleResult sampleResult); + ValidationFailure ValidateMovieFilename(SampleResult sampleResult); } public class FileNameValidationService : IFilenameValidationService { private const string ERROR_MESSAGE = "Produces invalid file names"; + public ValidationFailure ValidateMovieFilename(SampleResult sampleResult) + { + var validationFailure = new ValidationFailure("MovieFormat", ERROR_MESSAGE); + var parsedMovieInfo = Parser.Parser.ParseMovieTitle(sampleResult.FileName); + + if(parsedMovieInfo == null) + { + return validationFailure; + } + + return null; + } + public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 5de62a090..a617d5e61 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Organizer { @@ -13,7 +13,9 @@ namespace NzbDrone.Core.Organizer DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", SeriesFolderFormat = "{Series Title}", - SeasonFolderFormat = "Season {season}" + SeasonFolderFormat = "Season {season}", + MovieFolderFormat = "{Movie Title} ({Release Year})", + StandardMovieFormat = "{Movie Title} ({Release Year}) {Quality Full}", }; public bool RenameEpisodes { get; set; } @@ -24,5 +26,7 @@ namespace NzbDrone.Core.Organizer public string AnimeEpisodeFormat { get; set; } public string SeriesFolderFormat { get; set; } public string SeasonFolderFormat { get; set; } + public string StandardMovieFormat { get; set; } + public string MovieFolderFormat { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalMovie.cs b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs new file mode 100644 index 000000000..5ae9c481d --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; + +namespace NzbDrone.Core.Parser.Model +{ + public class LocalMovie + { + public LocalMovie() + { + } + + public string Path { get; set; } + public long Size { get; set; } + public ParsedMovieInfo ParsedMovieInfo { get; set; } + public Movie Movie { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public bool ExistingFile { get; set; } + + + public override string ToString() + { + return Path; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs new file mode 100644 index 000000000..87938423f --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/ParsedMovieInfo.cs @@ -0,0 +1,32 @@ +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Parser.Model +{ + public class ParsedMovieInfo + { + public string MovieTitle { get; set; } + public SeriesTitleInfo MovieTitleInfo { get; set; } + public QualityModel Quality { get; set; } + //public int SeasonNumber { get; set; } + public Language Language { get; set; } + //public bool FullSeason { get; set; } + //public bool Special { get; set; } + public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } + public string Edition { get; set;} + public int Year { get; set; } + public string ImdbId { get; set; } + + public ParsedMovieInfo() + { + + } + + public override string ToString() + { + return string.Format("{0} - {1} {2}", MovieTitle, MovieTitleInfo.Year, Quality); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 7c1680196..6f2812a7c 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Parser.Model public DownloadProtocol DownloadProtocol { get; set; } public int TvdbId { get; set; } public int TvRageId { get; set; } + public int ImdbId { get; set; } public DateTime PublishDate { get; set; } public string Origin { get; set; } @@ -82,6 +83,7 @@ namespace NzbDrone.Core.Parser.Model stringBuilder.AppendLine("DownloadProtocol: " + DownloadProtocol ?? "Empty"); stringBuilder.AppendLine("TvdbId: " + TvdbId ?? "Empty"); stringBuilder.AppendLine("TvRageId: " + TvRageId ?? "Empty"); + stringBuilder.AppendLine("ImdbId: " + ImdbId ?? "Empty"); stringBuilder.AppendLine("PublishDate: " + PublishDate ?? "Empty"); return stringBuilder.ToString(); default: diff --git a/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs new file mode 100644 index 000000000..1e6f5f5cc --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/RemoteMovie.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Parser.Model +{ + public class RemoteMovie + { + public ReleaseInfo Release { get; set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do. + public ParsedMovieInfo ParsedMovieInfo { get; set; } + public Movie Movie { get; set; } + public bool DownloadAllowed { get; set; } + + public override string ToString() + { + return Release.Title; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs b/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs index 59aab44a0..dbbf7f1e9 100644 --- a/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/TorrentInfo.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Parser.Model public string InfoHash { get; set; } public int? Seeders { get; set; } public int? Peers { get; set; } + public bool Freeleech { get; set; } public static int? GetSeeders(ReleaseInfo release) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 9b8759bd1..d133f4e86 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -15,6 +15,32 @@ namespace NzbDrone.Core.Parser { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser)); + private static readonly Regex[] ReportMovieTitleRegex = new[] + { + //Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.Special.Edition.2011 + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)\[!]))*(?<edition>(\.?((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final|Extended|Rogue|Special|Despecialized).(Cut|Edition|Version)|Extended|Uncensored|Remastered|Unrated|Uncut|IMAX)))\.(?<year>(19|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition //TODO: Seems to slow down parsing heavily! + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)(?<edition>((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final|Extended|Rogue|Special|Despecialized).(Cut|Edition|Version)|Extended|Uncensored|Remastered|Unrated|Uncut|IMAX))", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + //Normal movie format, e.g: Mission.Impossible.3.2011 + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //PassThePopcorn Torrent names: Star.Wars[PassThePopcorn] + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(\[\w *\])))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //That did not work? Maybe some tool uses [] for years. Who would do that? + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + }; + + private static readonly Regex[] ReportMovieTitleFolderRegex = new[] + { + //When year comes first. + new Regex(@"^(?:(?:[-_\W](?<![)!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?<title>.+?)?$") + }; + private static readonly Regex[] ReportTitleRegex = new[] { //Anime - Absolute Episode Number + Title + Season+Episode @@ -26,6 +52,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?:\W*S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Matches Movie name with AirYear + new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) new Regex(@"^(?:S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -236,7 +266,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|720[ip]|1080[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)\s*", + private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{9})", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex SimpleTitleRegex = new Regex(@"(?:480[ip]|576[ip]|720[ip]|1080[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)\s*", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex WebsitePrefixRegex = new Regex(@"^\[\s*[a-z]+(\.[a-z]+)+\s*\][- ]*", @@ -294,8 +326,148 @@ namespace NzbDrone.Core.Parser return result; } + public static ParsedMovieInfo ParseMoviePath(string path) + { + var fileInfo = new FileInfo(path); + + var result = ParseMovieTitle(fileInfo.Name, true); + + if (result == null) + { + Logger.Debug("Attempting to parse episode info using directory and file names. {0}", fileInfo.Directory.Name); + result = ParseMovieTitle(fileInfo.Directory.Name + " " + fileInfo.Name); + } + + if (result == null) + { + Logger.Debug("Attempting to parse episode info using directory name. {0}", fileInfo.Directory.Name); + result = ParseMovieTitle(fileInfo.Directory.Name + fileInfo.Extension); + } + + return result; + + } + + public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false) + { + + ParsedMovieInfo realResult = null; + try + { + if (!ValidateBeforeParsing(title)) return null; + + title = title.Replace(" ", "."); //TODO: Determine if this breaks something. However, it shouldn't. + + Logger.Debug("Parsing string '{0}'", title); + + if (ReversedTitleRegex.IsMatch(title)) + { + var titleWithoutExtension = RemoveFileExtension(title).ToCharArray(); + Array.Reverse(titleWithoutExtension); + + title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length); + + Logger.Debug("Reversed name detected. Converted to '{0}'", title); + } + + var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty); + + simpleTitle = RemoveFileExtension(simpleTitle); + + // TODO: Quick fix stripping [url] - prefixes. + simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty); + + simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty); + + var allRegexes = ReportMovieTitleRegex.ToList(); + + if (isDir) + { + allRegexes.AddRange(ReportMovieTitleFolderRegex); + } + + foreach (var regex in allRegexes) + { + var match = regex.Matches(simpleTitle); + + if (match.Count != 0) + { + Logger.Trace(regex); + try + { + var result = ParseMovieMatchCollection(match); + + if (result != null) + { + + result.Language = LanguageParser.ParseLanguage(title); + Logger.Debug("Language parsed: {0}", result.Language); + + result.Quality = QualityParser.ParseQuality(title); + Logger.Debug("Quality parsed: {0}", result.Quality); + + result.ReleaseGroup = ParseReleaseGroup(title); + + result.ImdbId = ParseImdbId(title); + + var subGroup = GetSubGroup(match); + if (!subGroup.IsNullOrWhiteSpace()) + { + result.ReleaseGroup = subGroup; + } + + Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup); + + result.ReleaseHash = GetReleaseHash(match); + if (!result.ReleaseHash.IsNullOrWhiteSpace()) + { + Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); + } + + realResult = result; + + return result; + } + } + catch (InvalidDateException ex) + { + Logger.Debug(ex, ex.Message); + break; + } + } + } + } + catch (Exception e) + { + if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc")) + Logger.Error(e, "An error has occurred while trying to parse " + title); + } + + Logger.Debug("Unable to parse {0}", title); + return realResult; + } + + public static string ParseImdbId(string title) + { + var match = ReportImdbId.Match(title); + if (match.Success) + { + if (match.Groups["imdbid"].Value != null) + { + if (match.Groups["imdbid"].Length == 11) + { + return match.Groups["imdbid"].Value; + } + } + } + + return ""; + } + public static ParsedEpisodeInfo ParseTitle(string title) { + + ParsedEpisodeInfo realResult = null; try { if (!ValidateBeforeParsing(title)) return null; @@ -342,6 +514,8 @@ namespace NzbDrone.Core.Parser } } + + foreach (var regex in ReportTitleRegex) { var match = regex.Matches(simpleTitle); @@ -383,6 +557,8 @@ namespace NzbDrone.Core.Parser Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash); } + realResult = result; + return result; } } @@ -401,7 +577,7 @@ namespace NzbDrone.Core.Parser } Logger.Debug("Unable to parse {0}", title); - return null; + return realResult; } public static string ParseSeriesName(string title) @@ -518,6 +694,31 @@ namespace NzbDrone.Core.Parser return seriesTitleInfo; } + private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection) + { + var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' '); + seriesName = RequestInfoRegex.Replace(seriesName, "").Trim(' '); + + int airYear; + int.TryParse(matchCollection[0].Groups["year"].Value, out airYear); + + ParsedMovieInfo result; + + result = new ParsedMovieInfo { Year = airYear }; + + if (matchCollection[0].Groups["edition"].Success) + { + result.Edition = matchCollection[0].Groups["edition"].Value.Replace(".", " "); + } + + result.MovieTitle = seriesName; + result.MovieTitleInfo = GetSeriesTitleInfo(result.MovieTitle); + + Logger.Debug("Movie Parsed. {0}", result); + + return result; + } + private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection) { var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' '); @@ -525,6 +726,7 @@ namespace NzbDrone.Core.Parser int airYear; int.TryParse(matchCollection[0].Groups["airyear"].Value, out airYear); + //int.TryParse(matchCollection[0].Groups["year"].Value, out airYear); ParsedEpisodeInfo result; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 4ab4fd4c7..9df110a88 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -15,9 +15,13 @@ namespace NzbDrone.Core.Parser { LocalEpisode GetLocalEpisode(string filename, Series series); LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); + LocalMovie GetLocalMovie(string filename, Movie movie); + LocalMovie GetLocalMovie(string filename, Movie movie, ParsedMovieInfo folderInfo, bool sceneSource); Series GetSeries(string title); + Movie GetMovie(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds); + RemoteMovie Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null); List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); } @@ -27,16 +31,33 @@ namespace NzbDrone.Core.Parser private readonly IEpisodeService _episodeService; private readonly ISeriesService _seriesService; private readonly ISceneMappingService _sceneMappingService; + private readonly IMovieService _movieService; private readonly Logger _logger; + private readonly Dictionary<string, string> romanNumeralsMapper = new Dictionary<string, string> + { + { "1", "I"}, + { "2", "II"}, + { "3", "III"}, + { "4", "IV"}, + { "5", "V"}, + { "6", "VI"}, + { "7", "VII"}, + { "8", "VII"}, + { "9", "IX"}, + { "10", "X"}, + + }; //If a movie has more than 10 parts fuck 'em. public ParsingService(IEpisodeService episodeService, ISeriesService seriesService, ISceneMappingService sceneMappingService, + IMovieService movieService, Logger logger) { _episodeService = episodeService; _seriesService = seriesService; _sceneMappingService = sceneMappingService; + _movieService = movieService; _logger = logger; } @@ -94,6 +115,46 @@ namespace NzbDrone.Core.Parser }; } + public LocalMovie GetLocalMovie(string filename, Movie movie) + { + return GetLocalMovie(filename, movie, null, false); + } + + public LocalMovie GetLocalMovie(string filename, Movie movie, ParsedMovieInfo folderInfo, bool sceneSource) + { + ParsedMovieInfo parsedMovieInfo; + + if (folderInfo != null) + { + parsedMovieInfo = folderInfo.JsonClone(); + parsedMovieInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename)); + } + + else + { + parsedMovieInfo = Parser.ParseMoviePath(filename); + } + + if (parsedMovieInfo == null) + { + if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) + { + _logger.Warn("Unable to parse movie info from path {0}", filename); + } + + return null; + } + + return new LocalMovie + { + Movie = movie, + Quality = parsedMovieInfo.Quality, + Path = filename, + ParsedMovieInfo = parsedMovieInfo, + ExistingFile = movie.Path.IsParentPath(filename) + }; + } + public Series GetSeries(string title) { var parsedEpisodeInfo = Parser.ParseTitle(title); @@ -114,6 +175,31 @@ namespace NzbDrone.Core.Parser return series; } + public Movie GetMovie(string title) + { + var parsedEpisodeInfo = Parser.ParseMovieTitle(title); + + if (parsedEpisodeInfo == null) + { + return _movieService.FindByTitle(title); + } + + var series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle); + + if (series == null) + { + series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.MovieTitleInfo.Year); + } + + if (series == null) + { + series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle.Replace("DC", "").Trim()); + } + + return series; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) { var remoteEpisode = new RemoteEpisode @@ -134,6 +220,25 @@ namespace NzbDrone.Core.Parser return remoteEpisode; } + public RemoteMovie Map(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null) + { + var remoteEpisode = new RemoteMovie + { + ParsedMovieInfo = parsedEpisodeInfo, + }; + + var movie = GetMovie(parsedEpisodeInfo, imdbId, searchCriteria); + + if (movie == null) + { + return remoteEpisode; + } + + remoteEpisode.Movie = movie; + + return remoteEpisode; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds) { return new RemoteEpisode @@ -248,14 +353,82 @@ namespace NzbDrone.Core.Parser return null; } + private Movie GetMovie(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + var possibleTitles = new List<string>(); + + possibleTitles.Add(searchCriteria.Movie.CleanTitle); + + foreach (string altTitle in searchCriteria.Movie.AlternativeTitles) + { + possibleTitles.Add(altTitle.CleanSeriesTitle()); + } + + foreach (string title in possibleTitles) + { + if (title == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle()) + { + return searchCriteria.Movie; + } + + foreach (KeyValuePair<string, string> entry in romanNumeralsMapper) + { + string num = entry.Key; + string roman = entry.Value.ToLower(); + + if (title.Replace(num, roman) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle()) + { + return searchCriteria.Movie; + } + + if (title.Replace(roman, num) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle()) + { + return searchCriteria.Movie; + } + } + } + + } + + Movie movie = null; + + if (searchCriteria == null) + { + + movie = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle); //Todo: same as above! + + return movie; + } + + + + if (movie == null && imdbId.IsNotNullOrWhiteSpace()) + { + //TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import + movie = _movieService.FindByImdbId(imdbId); + } + + if (movie == null) + { + _logger.Debug("No matching movie {0}", parsedEpisodeInfo.MovieTitle); + return null; + } + + return movie; + } + private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria) { Series series = null; + /*var localEpisode = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + var sceneMappingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle); - if (sceneMappingTvdbId.HasValue) + if (localEpisode != null) { - if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMappingTvdbId.Value) + if (searchCriteria != null && searchCriteria.Series.TvdbId == localEpisode.TvdbId) { return searchCriteria.Series; } @@ -269,7 +442,7 @@ namespace NzbDrone.Core.Parser } return series; - } + }*/ //This is only to find scene mapping should not be necessary for movies. if (searchCriteria != null) { diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 7154cd3fd..696b979fb 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -28,6 +28,8 @@ namespace NzbDrone.Core.Parser )\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private static readonly Regex HardcodedSubsRegex = new Regex(@"\b(?<hcsub>(\w+SUB))|(?<hc>(HC))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>RawHD|1080i[-_. ]HDTV|Raw[-_. ]HD|MPEG[-_. ]?2)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -59,6 +61,19 @@ namespace NzbDrone.Core.Parser name = name.Trim(); var normalizedName = name.Replace('_', ' ').Trim().ToLower(); var result = ParseQualityModifiers(name, normalizedName); + var subMatch = HardcodedSubsRegex.Matches(normalizedName).OfType<Match>().LastOrDefault(); + + if (subMatch != null && subMatch.Success) + { + if (subMatch.Groups["hcsub"].Success) + { + result.HardcodedSubs = subMatch.Groups["hcsub"].Value; + } + else if (subMatch.Groups["hc"].Success) + { + result.HardcodedSubs = "Generic Hardcoded Subs"; + } + } if (RawHDRegex.IsMatch(normalizedName)) { @@ -69,6 +84,7 @@ namespace NzbDrone.Core.Parser var sourceMatch = SourceRegex.Matches(normalizedName).OfType<Match>().LastOrDefault(); var resolution = ParseResolution(normalizedName); var codecRegex = CodecRegex.Match(normalizedName); + if (sourceMatch != null && sourceMatch.Success) { @@ -201,6 +217,8 @@ namespace NzbDrone.Core.Parser } } + + //Anime Bluray matching if (AnimeBlurayRegex.Match(normalizedName).Success) diff --git a/src/NzbDrone.Core/Properties/AssemblyInfo.cs b/src/NzbDrone.Core/Properties/AssemblyInfo.cs index 4593d015a..7ddb4d5ec 100644 --- a/src/NzbDrone.Core/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Core/Properties/AssemblyInfo.cs @@ -11,6 +11,4 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("3C29FEF7-4B07-49ED-822E-1C29DC49BFAB")] -[assembly: AssemblyVersion("10.0.0.*")] - [assembly: InternalsVisibleTo("NzbDrone.Core.Test")] diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 6476e5766..2230b7320 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Qualities new QualityDefinition(Quality.WEBDL720p) { Weight = 8, MinSize = 0, MaxSize = 100 }, new QualityDefinition(Quality.Bluray720p) { Weight = 9, MinSize = 0, MaxSize = 100 }, new QualityDefinition(Quality.WEBDL1080p) { Weight = 10, MinSize = 0, MaxSize = 100 }, - new QualityDefinition(Quality.Bluray1080p) { Weight = 11, MinSize = 0, MaxSize = 100 }, + new QualityDefinition(Quality.Bluray1080p) { Weight = 11, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.HDTV2160p) { Weight = 12, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.WEBDL2160p) { Weight = 13, MinSize = 0, MaxSize = null }, new QualityDefinition(Quality.Bluray2160p) { Weight = 14, MinSize = 0, MaxSize = null }, diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index a483d22c2..2ecc3cb6f 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Qualities { public Quality Quality { get; set; } public Revision Revision { get; set; } + public string HardcodedSubs { get; set; } [JsonIgnore] public QualitySource QualitySource { get; set; } diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 7164a17ae..851c404e2 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Queue { public Series Series { get; set; } public Episode Episode { get; set; } + public Movie Movie { get; set; } public QualityModel Quality { get; set; } public decimal Size { get; set; } public string Title { get; set; } @@ -24,6 +25,7 @@ namespace NzbDrone.Core.Queue public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } public string DownloadId { get; set; } public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } public DownloadProtocol Protocol { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 264645ed8..f7390ee6b 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -44,19 +44,49 @@ namespace NzbDrone.Core.Queue private IEnumerable<Queue> MapQueue(TrackedDownload trackedDownload) { - if (trackedDownload.RemoteEpisode.Episodes != null && trackedDownload.RemoteEpisode.Episodes.Any()) + if (trackedDownload.RemoteEpisode != null && trackedDownload.RemoteEpisode.Episodes != null && trackedDownload.RemoteEpisode.Episodes.Any()) { foreach (var episode in trackedDownload.RemoteEpisode.Episodes) { yield return MapEpisode(trackedDownload, episode); } } - else + else if (trackedDownload.RemoteMovie != null && trackedDownload.RemoteMovie.Movie != null) { - // FIXME: Present queue items with unknown series/episodes + yield return MapMovie(trackedDownload, trackedDownload.RemoteMovie.Movie); } } + private Queue MapMovie(TrackedDownload trackedDownload, Movie movie) + { + var queue = new Queue + { + Id = HashConverter.GetHashInt31(string.Format("trackedDownload-{0}", trackedDownload.DownloadItem.DownloadId)), + Series = null, + Episode = null, + Quality = trackedDownload.RemoteMovie.ParsedMovieInfo.Quality, + Title = trackedDownload.DownloadItem.Title, + Size = trackedDownload.DownloadItem.TotalSize, + Sizeleft = trackedDownload.DownloadItem.RemainingSize, + Timeleft = trackedDownload.DownloadItem.RemainingTime, + Status = trackedDownload.DownloadItem.Status.ToString(), + TrackedDownloadStatus = trackedDownload.Status.ToString(), + StatusMessages = trackedDownload.StatusMessages.ToList(), + RemoteEpisode = trackedDownload.RemoteEpisode, + RemoteMovie = trackedDownload.RemoteMovie, + DownloadId = trackedDownload.DownloadItem.DownloadId, + Protocol = trackedDownload.Protocol, + Movie = movie + }; + + if (queue.Timeleft.HasValue) + { + queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value); + } + + return queue; + } + private Queue MapEpisode(TrackedDownload trackedDownload, Episode episode) { var queue = new Queue diff --git a/src/NzbDrone.Core/RootFolders/RootFolderService.cs b/src/NzbDrone.Core/RootFolders/RootFolderService.cs index f1a1145e9..bf4d06972 100644 --- a/src/NzbDrone.Core/RootFolders/RootFolderService.cs +++ b/src/NzbDrone.Core/RootFolders/RootFolderService.cs @@ -149,7 +149,11 @@ namespace NzbDrone.Core.RootFolders foreach (string unmappedFolder in unmappedFolders) { var di = new DirectoryInfo(unmappedFolder.Normalize()); - results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName }); + if (!di.Attributes.HasFlag(FileAttributes.System) && !di.Attributes.HasFlag(FileAttributes.Hidden)) + { + results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName }); + } + } var setToRemove = SpecialFolders; diff --git a/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs b/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs new file mode 100644 index 000000000..08c7b2e72 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Commands/RefreshMovieCommand.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Tv.Commands +{ + public class RefreshMovieCommand : Command + { + public int? MovieId { get; set; } + + public RefreshMovieCommand() + { + } + + public RefreshMovieCommand(int? movieId) + { + MovieId = movieId; + } + + public override bool SendUpdatesToClient => true; + + public override bool UpdateScheduledTask => !MovieId.HasValue; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs new file mode 100644 index 000000000..1559d3716 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieAddedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieAddedEvent(Movie movie) + { + Movie = movie; + } + } +} diff --git a/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs new file mode 100644 index 000000000..6c56ef1d2 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieDeletedEvent : IEvent + { + public Movie Movie { get; private set; } + public bool DeleteFiles { get; private set; } + + public MovieDeletedEvent(Movie movie, bool deleteFiles) + { + Movie = movie; + DeleteFiles = deleteFiles; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs new file mode 100644 index 000000000..8b4b5c5f3 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieEditedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieEditedEvent : IEvent + { + public Movie Movie { get; private set; } + public Movie OldMovie { get; private set; } + + public MovieEditedEvent(Movie movie, Movie oldMovie) + { + Movie = movie; + OldMovie = oldMovie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs new file mode 100644 index 000000000..201b5f9bb --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieRefreshStartingEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieRefreshStartingEvent : IEvent + { + public bool ManualTrigger { get; set; } + + public MovieRefreshStartingEvent(bool manualTrigger) + { + ManualTrigger = manualTrigger; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs b/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs new file mode 100644 index 000000000..bae4d3e1d --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/MovieUpdateEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class MovieUpdatedEvent : IEvent + { + public Movie Movie { get; private set; } + + public MovieUpdatedEvent(Movie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/Movie.cs b/src/NzbDrone.Core/Tv/Movie.cs new file mode 100644 index 000000000..eadff4151 --- /dev/null +++ b/src/NzbDrone.Core/Tv/Movie.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Tv +{ + public class Movie : ModelBase + { + public Movie() + { + Images = new List<MediaCover.MediaCover>(); + Genres = new List<string>(); + Actors = new List<Actor>(); + Tags = new HashSet<int>(); + AlternativeTitles = new List<string>(); + } + public int TmdbId { get; set; } + public string ImdbId { get; set; } + public string Title { get; set; } + public string CleanTitle { get; set; } + public string SortTitle { get; set; } + public MovieStatusType Status { get; set; } + public string Overview { get; set; } + public bool Monitored { get; set; } + public int ProfileId { get; set; } + public DateTime? LastInfoSync { get; set; } + public int Runtime { get; set; } + public List<MediaCover.MediaCover> Images { get; set; } + public string TitleSlug { get; set; } + public string Website { get; set; } + public string Path { get; set; } + public int Year { get; set; } + public Ratings Ratings { get; set; } + public List<string> Genres { get; set; } + public List<Actor> Actors { get; set; } + public string Certification { get; set; } + public string RootFolderPath { get; set; } + public DateTime Added { get; set; } + public DateTime? InCinemas { get; set; } + public DateTime? PhysicalRelease { get; set; } + public LazyLoaded<Profile> Profile { get; set; } + public HashSet<int> Tags { get; set; } + public AddMovieOptions AddOptions { get; set; } + public LazyLoaded<MovieFile> MovieFile { get; set; } + public int MovieFileId { get; set; } + public List<string> AlternativeTitles { get; set; } + + public bool HasFile => MovieFileId > 0; + + public override string ToString() + { + return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe()); + } + } + + public class AddMovieOptions : MonitoringOptions + { + public bool SearchForMovie { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieAddedHandler.cs b/src/NzbDrone.Core/Tv/MovieAddedHandler.cs new file mode 100644 index 000000000..765446ee1 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieAddedHandler.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieAddedHandler : IHandle<MovieAddedEvent> + { + private readonly IManageCommandQueue _commandQueueManager; + + public MovieAddedHandler(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(MovieAddedEvent message) + { + _commandQueueManager.Push(new RefreshMovieCommand(message.Movie.Id)); + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieEditedService.cs b/src/NzbDrone.Core/Tv/MovieEditedService.cs new file mode 100644 index 000000000..5f011c5ba --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieEditedService.cs @@ -0,0 +1,25 @@ +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieEditedService : IHandle<MovieEditedEvent> + { + private readonly IManageCommandQueue _commandQueueManager; + + public MovieEditedService(IManageCommandQueue commandQueueManager) + { + _commandQueueManager = commandQueueManager; + } + + public void Handle(MovieEditedEvent message) + { + if (message.Movie.ImdbId != message.OldMovie.ImdbId) + { + _commandQueueManager.Push(new RefreshMovieCommand(message.Movie.Id)); //Probably not needed, as metadata should stay the same. + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs new file mode 100644 index 000000000..42bcddf48 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -0,0 +1,123 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.Tv +{ + public interface IMovieRepository : IBasicRepository<Movie> + { + bool MoviePathExists(string path); + Movie FindByTitle(string cleanTitle); + Movie FindByTitle(string cleanTitle, int year); + Movie FindByImdbId(string imdbid); + Movie FindByTitleSlug(string slug); + List<Movie> GetMoviesByFileId(int fileId); + void SetFileId(int fileId, int movieId); + } + + public class MovieRepository : BasicRepository<Movie>, IMovieRepository + { + private readonly Dictionary<string, string> romanNumeralsMapper = new Dictionary<string, string> + { + { "1", "I"}, + { "2", "II"}, + { "3", "III"}, + { "4", "IV"}, + { "5", "V"}, + { "6", "VI"}, + { "7", "VII"}, + { "8", "VII"}, + { "9", "IX"}, + { "10", "X"}, + + }; //If a movie has more than 10 parts fuck 'em. + + public MovieRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public bool MoviePathExists(string path) + { + return Query.Where(c => c.Path == path).Any(); + } + + public Movie FindByTitle(string cleanTitle) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + var cleanRoman = cleanTitle; + + var cleanNum = cleanTitle; + + foreach (KeyValuePair<string, string> entry in romanNumeralsMapper) + { + string num = entry.Key; + string roman = entry.Value.ToLower(); + + cleanRoman = cleanRoman.Replace(num, roman); + + cleanNum = cleanNum.Replace(roman, num); + } + + var result = Query.Where(s => s.CleanTitle == cleanTitle).FirstOrDefault(); + + if (result == null) + { + result = Query.Where(s => s.CleanTitle == cleanNum).OrWhere(s => s.CleanTitle == cleanRoman).FirstOrDefault(); + + if (result == null) + { + var movies = this.All(); + + result = movies.Where(m => m.AlternativeTitles.Any(t => Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanTitle || + Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanRoman || + Parser.Parser.CleanSeriesTitle(t.ToLower()) == cleanNum)).FirstOrDefault(); + + return result; + } + else + { + return result; + } + + } + else + { + return result; + } + } + + public Movie FindByTitle(string cleanTitle, int year) + { + cleanTitle = cleanTitle.ToLowerInvariant(); + + return Query.Where(s => s.CleanTitle == cleanTitle) + .AndWhere(s => s.Year == year) + .SingleOrDefault(); + } + + public Movie FindByImdbId(string imdbid) + { + return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault(); + } + + public List<Movie> GetMoviesByFileId(int fileId) + { + return Query.Where(m => m.MovieFileId == fileId).ToList(); + } + + public void SetFileId(int fileId, int episodeId) + { + SetFields(new Movie { Id = episodeId, MovieFileId = fileId }, movie => movie.MovieFileId); + } + + public Movie FindByTitleSlug(string slug) + { + return Query.Where(m => m.TitleSlug == slug).FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieScannedHandler.cs b/src/NzbDrone.Core/Tv/MovieScannedHandler.cs new file mode 100644 index 000000000..151ef0559 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieScannedHandler.cs @@ -0,0 +1,57 @@ +using NLog; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieScannedHandler : IHandle<MovieScannedEvent>, + IHandle<MovieScanSkippedEvent> + { + + private readonly IMovieService _movieService; + private readonly IManageCommandQueue _commandQueueManager; + + private readonly Logger _logger; + + public MovieScannedHandler( IMovieService movieService, + IManageCommandQueue commandQueueManager, + Logger logger) + { + _movieService = movieService; + _commandQueueManager = commandQueueManager; + _logger = logger; + } + + private void HandleScanEvents(Movie movie) + { + if (movie.AddOptions == null) + { + //_episodeAddedService.SearchForRecentlyAdded(movie.Id); + return; + } + + _logger.Info("[{0}] was recently added, performing post-add actions", movie.Title); + //_episodeMonitoredService.SetEpisodeMonitoredStatus(movie, movie.AddOptions); + + if (movie.AddOptions.SearchForMovie) + { + _commandQueueManager.Push(new MoviesSearchCommand { MovieId = movie.Id}); + } + + movie.AddOptions = null; + _movieService.RemoveAddOptions(movie); + } + + public void Handle(MovieScannedEvent message) + { + HandleScanEvents(message.Movie); + } + + public void Handle(MovieScanSkippedEvent message) + { + HandleScanEvents(message.Movie); + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs new file mode 100644 index 000000000..ac9c132db --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; + +namespace NzbDrone.Core.Tv +{ + public interface IMovieService + { + Movie GetMovie(int movieId); + List<Movie> GetMovies(IEnumerable<int> movieIds); + Movie AddMovie(Movie newMovie); + Movie FindByImdbId(string imdbid); + Movie FindByTitle(string title); + Movie FindByTitle(string title, int year); + Movie FindByTitleInexact(string title); + Movie FindByTitleSlug(string slug); + Movie GetMovieByFileId(int fileId); + void DeleteMovie(int movieId, bool deleteFiles); + List<Movie> GetAllMovies(); + Movie UpdateMovie(Movie movie); + List<Movie> UpdateMovie(List<Movie> movie); + bool MoviePathExists(string folder); + void RemoveAddOptions(Movie movie); + } + + public class MovieService : IMovieService, IHandle<MovieFileAddedEvent>, + IHandle<MovieFileDeletedEvent> + { + private readonly IMovieRepository _movieRepository; + private readonly IEventAggregator _eventAggregator; + private readonly IBuildFileNames _fileNameBuilder; + private readonly Logger _logger; + + public MovieService(IMovieRepository movieRepository, + IEventAggregator eventAggregator, + ISceneMappingService sceneMappingService, + IEpisodeService episodeService, + IBuildFileNames fileNameBuilder, + Logger logger) + { + _movieRepository = movieRepository; + _eventAggregator = eventAggregator; + _fileNameBuilder = fileNameBuilder; + _logger = logger; + } + + public Movie GetMovie(int movieId) + { + return _movieRepository.Get(movieId); + } + + public List<Movie> GetMovies(IEnumerable<int> movieIds) + { + return _movieRepository.Get(movieIds).ToList(); + } + + public Movie AddMovie(Movie newMovie) + { + Ensure.That(newMovie, () => newMovie).IsNotNull(); + + if (string.IsNullOrWhiteSpace(newMovie.Path)) + { + var folderName = _fileNameBuilder.GetMovieFolder(newMovie); + newMovie.Path = Path.Combine(newMovie.RootFolderPath, folderName); + } + + _logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path); + + newMovie.CleanTitle = newMovie.Title.CleanSeriesTitle(); + newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.TmdbId); + newMovie.Added = DateTime.UtcNow; + + _movieRepository.Insert(newMovie); + _eventAggregator.PublishEvent(new MovieAddedEvent(GetMovie(newMovie.Id))); + + return newMovie; + } + + public Movie FindByTitle(string title) + { + return _movieRepository.FindByTitle(title.CleanSeriesTitle()); + } + + public Movie FindByImdbId(string imdbid) + { + return _movieRepository.FindByImdbId(imdbid); + } + + public Movie FindByTitleInexact(string title) + { + // find any movie clean title within the provided release title + string cleanTitle = title.CleanSeriesTitle(); + var list = _movieRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList(); + if (!list.Any()) + { + // no movie matched + return null; + } + if (list.Count == 1) + { + // return the first movie if there is only one + return list.Single(); + } + // build ordered list of movie by position in the search string + var query = + list.Select(movie => new + { + position = cleanTitle.IndexOf(movie.CleanTitle), + length = movie.CleanTitle.Length, + movie = movie + }) + .Where(s => (s.position>=0)) + .ToList() + .OrderBy(s => s.position) + .ThenByDescending(s => s.length) + .ToList(); + + // get the leftmost movie that is the longest + // movie are usually the first thing in release title, so we select the leftmost and longest match + var match = query.First().movie; + + _logger.Debug("Multiple movie matched {0} from title {1}", match.Title, title); + foreach (var entry in list) + { + _logger.Debug("Multiple movie match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle); + } + + return match; + } + + public Movie FindByTitle(string title, int year) + { + return _movieRepository.FindByTitle(title.CleanSeriesTitle(), year); + } + + public void DeleteMovie(int movieId, bool deleteFiles) + { + var movie = _movieRepository.Get(movieId); + _movieRepository.Delete(movieId); + _eventAggregator.PublishEvent(new MovieDeletedEvent(movie, deleteFiles)); + } + + public List<Movie> GetAllMovies() + { + return _movieRepository.All().ToList(); + } + + public Movie UpdateMovie(Movie movie) + { + var storedMovie = GetMovie(movie.Id); + + var updatedMovie = _movieRepository.Update(movie); + _eventAggregator.PublishEvent(new MovieEditedEvent(updatedMovie, storedMovie)); + + return updatedMovie; + } + + public List<Movie> UpdateMovie(List<Movie> movie) + { + _logger.Debug("Updating {0} movie", movie.Count); + foreach (var s in movie) + { + _logger.Trace("Updating: {0}", s.Title); + if (!s.RootFolderPath.IsNullOrWhiteSpace()) + { + var folderName = new DirectoryInfo(s.Path).Name; + s.Path = Path.Combine(s.RootFolderPath, folderName); + _logger.Trace("Changing path for {0} to {1}", s.Title, s.Path); + } + + else + { + _logger.Trace("Not changing path for: {0}", s.Title); + } + } + + _movieRepository.UpdateMany(movie); + _logger.Debug("{0} movie updated", movie.Count); + + return movie; + } + + public bool MoviePathExists(string folder) + { + return _movieRepository.MoviePathExists(folder); + } + + public void RemoveAddOptions(Movie movie) + { + _movieRepository.SetFields(movie, s => s.AddOptions); + } + + public void Handle(MovieFileAddedEvent message) + { + _movieRepository.SetFileId(message.MovieFile.Id, message.MovieFile.Movie.Value.Id); + _logger.Debug("Linking [{0}] > [{1}]", message.MovieFile.RelativePath, message.MovieFile.Movie.Value); + } + + public void Handle(MovieFileDeletedEvent message) + { + var movie = _movieRepository.GetMoviesByFileId(message.MovieFile.Id).First(); + movie.MovieFileId = 0; + + UpdateMovie(movie); + } + + public Movie GetMovieByFileId(int fileId) + { + return _movieRepository.GetMoviesByFileId(fileId).First(); + } + + public Movie FindByTitleSlug(string slug) + { + return _movieRepository.FindByTitleSlug(slug); + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieStatusType.cs b/src/NzbDrone.Core/Tv/MovieStatusType.cs new file mode 100644 index 000000000..9c0bdbed9 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieStatusType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Tv +{ + public enum MovieStatusType + { + TBA = 0, //Nothing yet announced, only rumors, but still IMDb page + Announced = 1, //AirDate is announced + Released = 2 //Has at least one PreDB release + } +} diff --git a/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs b/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs new file mode 100644 index 000000000..c82dd014d --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieTitleNormalizer.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Tv +{ + public static class MovieTitleNormalizer + { + private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string> + { + { 999999999, "a to z" }, + }; + + public static string Normalize(string title, int tmdbid) + { + if (PreComputedTitles.ContainsKey(tmdbid)) + { + return PreComputedTitles[tmdbid]; + } + + return Parser.Parser.NormalizeTitle(title).ToLower(); + } + } +} diff --git a/src/NzbDrone.Core/Tv/RefreshMovieService.cs b/src/NzbDrone.Core/Tv/RefreshMovieService.cs new file mode 100644 index 000000000..a0b4a56a9 --- /dev/null +++ b/src/NzbDrone.Core/Tv/RefreshMovieService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +//using NzbDrone.Core.DataAugmentation.DailyMovie; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Tv.Commands; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.Tv +{ + public class RefreshMovieService : IExecute<RefreshMovieCommand> + { + private readonly IProvideMovieInfo _movieInfo; + private readonly IMovieService _movieService; + private readonly IRefreshEpisodeService _refreshEpisodeService; + private readonly IEventAggregator _eventAggregator; + private readonly IDiskScanService _diskScanService; + private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed; + private readonly Logger _logger; + + public RefreshMovieService(IProvideMovieInfo movieInfo, + IMovieService movieService, + IRefreshEpisodeService refreshEpisodeService, + IEventAggregator eventAggregator, + IDiskScanService diskScanService, + ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed, + Logger logger) + { + _movieInfo = movieInfo; + _movieService = movieService; + _refreshEpisodeService = refreshEpisodeService; + _eventAggregator = eventAggregator; + _diskScanService = diskScanService; + _checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed; + _logger = logger; + } + + private void RefreshMovieInfo(Movie movie) + { + _logger.ProgressInfo("Updating Info for {0}", movie.Title); + + Movie movieInfo; + + try + { + movieInfo = _movieInfo.GetMovieInfo(movie.TmdbId); + } + catch (MovieNotFoundException) + { + _logger.Error("Movie '{0}' (imdbid {1}) was not found, it may have been removed from TheTVDB.", movie.Title, movie.ImdbId); + return; + } + + if (movie.TmdbId != movieInfo.TmdbId) + { + _logger.Warn("Movie '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", movie.Title, movie.TmdbId, movieInfo.Title, movieInfo.TmdbId); + movie.TmdbId = movieInfo.TmdbId; + } + + movie.Title = movieInfo.Title; + movie.TitleSlug = movieInfo.TitleSlug; + movie.ImdbId = movieInfo.ImdbId; + movie.Overview = movieInfo.Overview; + movie.Status = movieInfo.Status; + movie.CleanTitle = movieInfo.CleanTitle; + movie.SortTitle = movieInfo.SortTitle; + movie.LastInfoSync = DateTime.UtcNow; + movie.Runtime = movieInfo.Runtime; + movie.Images = movieInfo.Images; + movie.Ratings = movieInfo.Ratings; + movie.Actors = movieInfo.Actors; + movie.Genres = movieInfo.Genres; + movie.Certification = movieInfo.Certification; + movie.InCinemas = movieInfo.InCinemas; + movie.Website = movieInfo.Website; + movie.AlternativeTitles = movieInfo.AlternativeTitles; + movie.Year = movieInfo.Year; + movie.PhysicalRelease = movieInfo.PhysicalRelease; + + try + { + movie.Path = new DirectoryInfo(movie.Path).FullName; + movie.Path = movie.Path.GetActualCasing(); + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't update movie path for " + movie.Path); + } + + _movieService.UpdateMovie(movie); + + _logger.Debug("Finished movie refresh for {0}", movie.Title); + _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); + } + + public void Execute(RefreshMovieCommand message) + { + _eventAggregator.PublishEvent(new MovieRefreshStartingEvent(message.Trigger == CommandTrigger.Manual)); + + if (message.MovieId.HasValue) + { + var movie = _movieService.GetMovie(message.MovieId.Value); + RefreshMovieInfo(movie); + } + else + { + var allMovie = _movieService.GetAllMovies().OrderBy(c => c.SortTitle).ToList(); + + foreach (var movie in allMovie) + { + if (message.Trigger == CommandTrigger.Manual || _checkIfMovieShouldBeRefreshed.ShouldRefresh(movie)) + { + try + { + RefreshMovieInfo(movie); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't refresh info for {0}".Inject(movie)); + } + } + + else + { + try + { + _logger.Info("Skipping refresh of movie: {0}", movie.Title); + _diskScanService.Scan(movie); + } + catch (Exception e) + { + _logger.Error(e, "Couldn't rescan movie {0}".Inject(movie)); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs new file mode 100644 index 000000000..4b5d650eb --- /dev/null +++ b/src/NzbDrone.Core/Tv/ShouldRefreshMovie.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using NLog; + +namespace NzbDrone.Core.Tv +{ + public interface ICheckIfMovieShouldBeRefreshed + { + bool ShouldRefresh(Movie movie); + } + + public class ShouldRefreshMovie : ICheckIfMovieShouldBeRefreshed + { + private readonly IEpisodeService _episodeService; + private readonly Logger _logger; + + public ShouldRefreshMovie(IEpisodeService episodeService, Logger logger) + { + _episodeService = episodeService; + _logger = logger; + } + + public bool ShouldRefresh(Movie movie) + { + if (movie.LastInfoSync < DateTime.UtcNow.AddDays(-30)) + { + _logger.Trace("Movie {0} last updated more than 30 days ago, should refresh.", movie.Title); + return true; + } + + if (movie.LastInfoSync >= DateTime.UtcNow.AddHours(-6)) + { + _logger.Trace("Movie {0} last updated less than 6 hours ago, should not be refreshed.", movie.Title); + return false; + } + + if (movie.Status != MovieStatusType.TBA) + { + _logger.Trace("Movie {0} is announced or released, should refresh.", movie.Title); //We probably have to change this. + return true; + } + + _logger.Trace("Movie {0} ended long ago, should not be refreshed.", movie.Title); + return false; + } + } +} diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 5911a9a13..8656ffef8 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -6,6 +6,6 @@ namespace NzbDrone.Core.Update.Commands { public override bool SendUpdatesToClient => true; - public override string CompletionMessage => "Restarting Sonarr to apply updates"; + public override string CompletionMessage => "Restarting Radarr to apply updates"; } } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index e5ef7fa30..e42a075cd 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -129,7 +129,7 @@ namespace NzbDrone.Core.Update _diskTransferService.TransferFolder(_appFolderInfo.GetUpdateClientFolder(), updateSandboxFolder, TransferMode.Move, false); _logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath()); - _logger.ProgressInfo("Sonarr will restart shortly."); + _logger.ProgressInfo("Radarr will restart shortly."); _processProvider.Start(_appFolderInfo.GetUpdateClientExePath(), GetUpdaterArgs(updateSandboxFolder)); } @@ -178,8 +178,9 @@ namespace NzbDrone.Core.Update { var processId = _processProvider.GetCurrentProcess().Id.ToString(); var executingApplication = _runtimeInfo.ExecutingApplication; - - return string.Join(" ", processId, updateSandboxFolder.TrimEnd(Path.DirectorySeparatorChar).WrapInQuotes(), executingApplication.WrapInQuotes(), _startupContext.PreservedArguments); + var args = string.Join(" ", processId, updateSandboxFolder.TrimEnd(Path.DirectorySeparatorChar).WrapInQuotes(), executingApplication.WrapInQuotes(), _startupContext.PreservedArguments); + _logger.Info("Updater Arguments: " + args); + return args; } private void EnsureAppDataSafety() @@ -187,7 +188,7 @@ namespace NzbDrone.Core.Update if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) { - throw new UpdateFailedException("Your Sonarr configuration '{0}' is being stored in application folder '{1}' which will cause data lost during the upgrade. Please remove any symlinks or redirects before trying again.", _appFolderInfo.AppDataFolder, _appFolderInfo.StartUpFolder); + throw new UpdateFailedException("Your Radarr configuration '{0}' is being stored in application folder '{1}' which will cause data lost during the upgrade. Please remove any symlinks or redirects before trying again.", _appFolderInfo.AppDataFolder, _appFolderInfo.StartUpFolder); } } diff --git a/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs new file mode 100644 index 000000000..d694d00b4 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MovieAncestorValidator.cs @@ -0,0 +1,25 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MovieAncestorValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MovieAncestorValidator(IMovieService seriesService) + : base("Path is an ancestor of an existing path") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return !_seriesService.GetAllMovies().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs new file mode 100644 index 000000000..5aec9a132 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs @@ -0,0 +1,26 @@ +using System; +using FluentValidation.Validators; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MovieExistsValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MovieExistsValidator(IMovieService seriesService) + : base("This series has already been added") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + int tmdbId = (int)context.PropertyValue; + + return (!_seriesService.GetAllMovies().Exists(s => s.TmdbId == tmdbId)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs b/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs new file mode 100644 index 000000000..690bd59f2 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/MoviePathValidation.cs @@ -0,0 +1,27 @@ +using FluentValidation.Validators; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class MoviePathValidator : PropertyValidator + { + private readonly IMovieService _seriesService; + + public MoviePathValidator(IMovieService seriesService) + : base("Path is already configured for another series") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + dynamic instance = context.ParentContext.InstanceToValidate; + var instanceId = (int)instance.Id; + + return (!_seriesService.GetAllMovies().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs index 794e9edff..9518a6d9e 100644 --- a/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/FirewallAdapter.cs @@ -5,7 +5,7 @@ using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Host.AccessControl +namespace Radarr.Host.AccessControl { public interface IFirewallAdapter { diff --git a/src/NzbDrone.Host/AccessControl/NetshProvider.cs b/src/NzbDrone.Host/AccessControl/NetshProvider.cs index 88bcd880c..cca3fc8c5 100644 --- a/src/NzbDrone.Host/AccessControl/NetshProvider.cs +++ b/src/NzbDrone.Host/AccessControl/NetshProvider.cs @@ -2,7 +2,7 @@ using NLog; using NzbDrone.Common.Processes; -namespace NzbDrone.Host.AccessControl +namespace Radarr.Host.AccessControl { public interface INetshProvider { diff --git a/src/NzbDrone.Host/AccessControl/SslAdapter.cs b/src/NzbDrone.Host/AccessControl/SslAdapter.cs index 12784ba87..ed9c3aa95 100644 --- a/src/NzbDrone.Host/AccessControl/SslAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/SslAdapter.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Core.Configuration; -namespace NzbDrone.Host.AccessControl +namespace Radarr.Host.AccessControl { public interface ISslAdapter { diff --git a/src/NzbDrone.Host/AccessControl/UrlAcl.cs b/src/NzbDrone.Host/AccessControl/UrlAcl.cs index 51af167a6..8ff7e9602 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAcl.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAcl.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Host.AccessControl +namespace Radarr.Host.AccessControl { public class UrlAcl { diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index 9493dd276..7c61f4320 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; -namespace NzbDrone.Host.AccessControl +namespace Radarr.Host.AccessControl { public interface IUrlAclAdapter { diff --git a/src/NzbDrone.Host/ApplicationModes.cs b/src/NzbDrone.Host/ApplicationModes.cs index aa425948c..3495d8688 100644 --- a/src/NzbDrone.Host/ApplicationModes.cs +++ b/src/NzbDrone.Host/ApplicationModes.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Host +namespace Radarr.Host { public enum ApplicationModes { diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index fdd3c3683..e7235683b 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -5,9 +5,9 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Host.Owin; +using Radarr.Host.Owin; -namespace NzbDrone.Host +namespace Radarr.Host { public interface INzbDroneServiceFactory { diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 24a151eeb..5fdb487de 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -10,7 +10,7 @@ using NzbDrone.Common.Security; using NzbDrone.Core.Datastore; using NzbDrone.Core.Instrumentation; -namespace NzbDrone.Host +namespace Radarr.Host { public static class Bootstrap { @@ -24,7 +24,7 @@ namespace NzbDrone.Host SecurityProtocolPolicy.Register(); X509CertificateValidationPolicy.Register(); - Logger.Info("Starting Sonarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); + Logger.Info("Starting Radarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); if (!PlatformValidation.IsValidate(userAlert)) { diff --git a/src/NzbDrone.Host/BrowserService.cs b/src/NzbDrone.Host/BrowserService.cs index 1867421cf..cf0a3a313 100644 --- a/src/NzbDrone.Host/BrowserService.cs +++ b/src/NzbDrone.Host/BrowserService.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; using NzbDrone.Core.Configuration; -namespace NzbDrone.Host +namespace Radarr.Host { public interface IBrowserService { diff --git a/src/NzbDrone.Host/IUserAlert.cs b/src/NzbDrone.Host/IUserAlert.cs index 04db62985..f0ea05ae4 100644 --- a/src/NzbDrone.Host/IUserAlert.cs +++ b/src/NzbDrone.Host/IUserAlert.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Host +namespace Radarr.Host { public interface IUserAlert { diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index ca31bb723..a82d3d836 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -6,7 +6,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.SignalR; -namespace NzbDrone.Host +namespace Radarr.Host { public class MainAppContainerBuilder : ContainerBuilderBase { @@ -14,7 +14,7 @@ namespace NzbDrone.Host { var assemblies = new List<string> { - "NzbDrone.Host", + "Radarr.Host", "NzbDrone.Common", "NzbDrone.Core", "NzbDrone.Api", diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index fa9b7bf42..d42d1f820 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -8,8 +8,8 @@ <ProjectGuid>{95C11A9E-56ED-456A-8447-2C89C1139266}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>NzbDrone.Host</RootNamespace> - <AssemblyName>NzbDrone.Host</AssemblyName> + <RootNamespace>Radarr.Host</RootNamespace> + <AssemblyName>Radarr.Host</AssemblyName> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <TargetFrameworkProfile> @@ -56,6 +56,9 @@ <PropertyGroup> <RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent> </PropertyGroup> + <PropertyGroup> + <ApplicationIcon>Radarr.ico</ApplicationIcon> + </PropertyGroup> <ItemGroup> <Reference Include="Microsoft.Owin, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> @@ -134,9 +137,6 @@ <None Include="app.config" /> <None Include="packages.config" /> </ItemGroup> - <ItemGroup> - <None Include="NzbDrone.ico" /> - </ItemGroup> <ItemGroup> <BootstrapperPackage Include=".NETFramework,Version=v4.0"> <Visible>False</Visible> @@ -185,7 +185,9 @@ <Name>NzbDrone.SignalR</Name> </ProjectReference> </ItemGroup> - <ItemGroup /> + <ItemGroup> + <Content Include="Radarr.ico" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PreBuildEvent> diff --git a/src/NzbDrone.Host/NzbDrone.ico b/src/NzbDrone.Host/NzbDrone.ico deleted file mode 100644 index 1922557d6..000000000 Binary files a/src/NzbDrone.Host/NzbDrone.ico and /dev/null differ diff --git a/src/NzbDrone.Host/Owin/IHostController.cs b/src/NzbDrone.Host/Owin/IHostController.cs index 130b48d4b..74d534b9d 100644 --- a/src/NzbDrone.Host/Owin/IHostController.cs +++ b/src/NzbDrone.Host/Owin/IHostController.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public interface IHostController { diff --git a/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs index 1b5e8ce5b..ee33d0df0 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/IOwinMiddleWare.cs @@ -1,6 +1,6 @@ using Owin; -namespace NzbDrone.Host.Owin.MiddleWare +namespace Radarr.Host.Owin.MiddleWare { public interface IOwinMiddleWare { diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs index 89f664864..7b826168b 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/NancyMiddleWare.cs @@ -2,7 +2,7 @@ using Nancy.Owin; using Owin; -namespace NzbDrone.Host.Owin.MiddleWare +namespace Radarr.Host.Owin.MiddleWare { public class NancyMiddleWare : IOwinMiddleWare { diff --git a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs index a74d9b1d3..a46e357ae 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/NzbDroneVersionMiddleWare.cs @@ -4,7 +4,7 @@ using Microsoft.Owin; using NzbDrone.Common.EnvironmentInfo; using Owin; -namespace NzbDrone.Host.Owin.MiddleWare +namespace Radarr.Host.Owin.MiddleWare { public class NzbDroneVersionMiddleWare : IOwinMiddleWare { diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs index 0df60a326..fa9fe158a 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Composition; using NzbDrone.SignalR; using Owin; -namespace NzbDrone.Host.Owin.MiddleWare +namespace Radarr.Host.Owin.MiddleWare { public class SignalRMiddleWare : IOwinMiddleWare { diff --git a/src/NzbDrone.Host/Owin/NlogTextWriter.cs b/src/NzbDrone.Host/Owin/NlogTextWriter.cs index 2d04acf1a..b57e26b92 100644 --- a/src/NzbDrone.Host/Owin/NlogTextWriter.cs +++ b/src/NzbDrone.Host/Owin/NlogTextWriter.cs @@ -2,7 +2,7 @@ using System.Text; using NLog; -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public class NlogTextWriter : TextWriter { diff --git a/src/NzbDrone.Host/Owin/OwinHostController.cs b/src/NzbDrone.Host/Owin/OwinHostController.cs index 82357c24c..a2000974b 100644 --- a/src/NzbDrone.Host/Owin/OwinHostController.cs +++ b/src/NzbDrone.Host/Owin/OwinHostController.cs @@ -1,9 +1,9 @@ using System; using NLog; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Host.AccessControl; +using Radarr.Host.AccessControl; -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public class OwinHostController : IHostController { diff --git a/src/NzbDrone.Host/Owin/OwinServiceProvider.cs b/src/NzbDrone.Host/Owin/OwinServiceProvider.cs index c0676cd24..4dd08a2ea 100644 --- a/src/NzbDrone.Host/Owin/OwinServiceProvider.cs +++ b/src/NzbDrone.Host/Owin/OwinServiceProvider.cs @@ -9,10 +9,10 @@ using Microsoft.Owin.Hosting.Services; using Microsoft.Owin.Hosting.Tracing; using NLog; using NzbDrone.Core.Configuration; -using NzbDrone.Host.Owin.MiddleWare; +using Radarr.Host.Owin.MiddleWare; using Owin; -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public interface IOwinAppFactory { diff --git a/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs b/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs index 6dc0e57ee..b195ba969 100644 --- a/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs +++ b/src/NzbDrone.Host/Owin/OwinTraceOutputFactory.cs @@ -2,7 +2,7 @@ using Microsoft.Owin.Hosting.Tracing; using NLog; -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public class OwinTraceOutputFactory : ITraceOutputFactory { diff --git a/src/NzbDrone.Host/Owin/PortInUseException.cs b/src/NzbDrone.Host/Owin/PortInUseException.cs index 5c6d7a542..5946bc61a 100644 --- a/src/NzbDrone.Host/Owin/PortInUseException.cs +++ b/src/NzbDrone.Host/Owin/PortInUseException.cs @@ -1,7 +1,7 @@ using System; using NzbDrone.Common.Exceptions; -namespace NzbDrone.Host.Owin +namespace Radarr.Host.Owin { public class PortInUseException : NzbDroneException { diff --git a/src/NzbDrone.Host/PlatformValidation.cs b/src/NzbDrone.Host/PlatformValidation.cs index a4dce7bc8..2082ee814 100644 --- a/src/NzbDrone.Host/PlatformValidation.cs +++ b/src/NzbDrone.Host/PlatformValidation.cs @@ -5,7 +5,7 @@ using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; -namespace NzbDrone.Host +namespace Radarr.Host { public static class PlatformValidation { diff --git a/src/NzbDrone.Host/Properties/AssemblyInfo.cs b/src/NzbDrone.Host/Properties/AssemblyInfo.cs index dd667bbdd..88458fff9 100644 --- a/src/NzbDrone.Host/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Host/Properties/AssemblyInfo.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.exe")] +[assembly: AssemblyTitle("Radarr.exe")] [assembly: Guid("C2172AF4-F9A6-4D91-BAEE-C2E4EE680613")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Host/Radarr.ico b/src/NzbDrone.Host/Radarr.ico new file mode 100644 index 000000000..6f0a8b50e Binary files /dev/null and b/src/NzbDrone.Host/Radarr.ico differ diff --git a/src/NzbDrone.Host/Router.cs b/src/NzbDrone.Host/Router.cs index 72d1c8f67..8009ccb70 100644 --- a/src/NzbDrone.Host/Router.cs +++ b/src/NzbDrone.Host/Router.cs @@ -1,7 +1,7 @@ using NLog; using NzbDrone.Common; -namespace NzbDrone.Host +namespace Radarr.Host { public class Router { diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index 75b8bb13e..8aa3a15dd 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -4,7 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common.Processes; -namespace NzbDrone.Host +namespace Radarr.Host { public interface ISingleInstancePolicy { @@ -31,7 +31,7 @@ namespace NzbDrone.Host { if (IsAlreadyRunning()) { - _logger.Warn("Another instance of Sonarr is already running."); + _logger.Warn("Another instance of Radarr is already running."); _browserService.LaunchWebUI(); throw new TerminateApplicationException("Another instance is already running"); } diff --git a/src/NzbDrone.Host/SpinService.cs b/src/NzbDrone.Host/SpinService.cs index e2c4e6933..ae35590fd 100644 --- a/src/NzbDrone.Host/SpinService.cs +++ b/src/NzbDrone.Host/SpinService.cs @@ -4,7 +4,7 @@ using NLog.Common; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; -namespace NzbDrone.Host +namespace Radarr.Host { public interface IWaitForExit { diff --git a/src/NzbDrone.Host/TerminateApplicationException.cs b/src/NzbDrone.Host/TerminateApplicationException.cs index 734fb65d2..0c65345c3 100644 --- a/src/NzbDrone.Host/TerminateApplicationException.cs +++ b/src/NzbDrone.Host/TerminateApplicationException.cs @@ -1,6 +1,6 @@ using System; -namespace NzbDrone.Host +namespace Radarr.Host { public class TerminateApplicationException : ApplicationException { diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index bd36562c8..94defd951 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test public override string SeriesRootFolder => GetTempDirectory("SeriesRootFolder"); - protected override string RootUrl => "http://localhost:8989/"; + protected override string RootUrl => "http://localhost:7878/"; protected override string ApiKey => _runner.ApiKey; diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index cf6593d04..e8be666bf 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -159,7 +159,7 @@ namespace NzbDrone.Integration.Test protected void ConnectSignalR() { _signalRReceived = new List<SignalRMessage>(); - _signalrConnection = new Connection("http://localhost:8989/signalr"); + _signalrConnection = new Connection("http://localhost:7878/signalr"); _signalrConnection.Start(new LongPollingTransport()).ContinueWith(task => { if (task.IsFaulted) diff --git a/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs index 5183f6f7e..fd12f9363 100644 --- a/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Integration.Test/Properties/AssemblyInfo.cs @@ -20,5 +20,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("8a49cb1d-87ac-42f9-a582-607365a6bd79")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs index 8d91461ae..883df114e 100644 --- a/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Libraries.Test/Properties/AssemblyInfo.cs @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("32ec29e2-40ba-4050-917d-e295d85d4969")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs index 012007b52..d2eaab331 100644 --- a/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Mono.Test/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("45299d3c-34ff-48ca-9093-de2f037c38ac")] -[assembly: AssemblyVersion("10.0.0.*")] +[assembly: AssemblyVersion("0.1.0.*")] diff --git a/src/NzbDrone.Mono/Properties/AssemblyInfo.cs b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs index f78631ed8..e211b8ca2 100644 --- a/src/NzbDrone.Mono/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Mono/Properties/AssemblyInfo.cs @@ -20,5 +20,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("01493ea5-494f-43bf-be18-8ae4d0708fc6")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs b/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs index 7d5972415..1f4a44bd9 100644 --- a/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.SignalR/Properties/AssemblyInfo.cs @@ -6,5 +6,3 @@ using System.Runtime.InteropServices; // associated with an assembly. [assembly: AssemblyTitle("NzbDrone.SignalR")] [assembly: Guid("98bd985a-4f23-4201-8ed3-f6f3d7f2a5fe")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 2f1d8e3f5..d844ea8f7 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -22,26 +22,26 @@ namespace NzbDrone.Test.Common public string AppData { get; private set; } public string ApiKey { get; private set; } - public NzbDroneRunner(Logger logger, int port = 8989) + public NzbDroneRunner(Logger logger, int port = 7878) { _processProvider = new ProcessProvider(logger); - _restClient = new RestClient("http://localhost:8989/api"); + _restClient = new RestClient("http://localhost:7878/api"); } public void Start() { AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + DateTime.Now.Ticks); - var nzbdroneConsoleExe = "NzbDrone.Console.exe"; + var nzbdroneConsoleExe = "Radarr.Console.exe"; if (OsInfo.IsNotWindows) { - nzbdroneConsoleExe = "NzbDrone.exe"; + nzbdroneConsoleExe = "Radarr.exe"; } if (BuildInfo.IsDebug) { - Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "..\\..\\..\\..\\..\\_output\\NzbDrone.Console.exe")); + Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "..\\..\\..\\..\\..\\_output\\Radarr.Console.exe")); } else { diff --git a/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs b/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs index d82d940d5..b1487e507 100644 --- a/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Test.Common/Properties/AssemblyInfo.cs @@ -21,4 +21,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("f3e91f6e-d01d-4f20-8255-147cc10f04e3")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs b/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs index d2e93dadf..a0ce8907e 100644 --- a/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Test.Dummy/Properties/AssemblyInfo.cs @@ -20,5 +20,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("7b773a86-574d-48c3-9e89-6f2e0dff714b")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Update.Test/ProgramFixture.cs b/src/NzbDrone.Update.Test/ProgramFixture.cs index 5d9b7243a..fb3ba1338 100644 --- a/src/NzbDrone.Update.Test/ProgramFixture.cs +++ b/src/NzbDrone.Update.Test/ProgramFixture.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Update.Test [Test] public void should_call_update_with_correct_path() { - var ProcessPath = @"C:\NzbDrone\nzbdrone.exe".AsOsAgnostic(); + var ProcessPath = @"C:\NzbDrone\radarr.exe".AsOsAgnostic(); Mocker.GetMock<IProcessProvider>().Setup(c => c.GetProcessById(12)) .Returns(new ProcessInfo() { StartPath = ProcessPath }); diff --git a/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs index 35dc227d7..a3c1d1ca6 100644 --- a/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Update.Test/Properties/AssemblyInfo.cs @@ -20,5 +20,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("b323e212-2d04-4c7f-9097-c356749ace4d")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Update.Test/StartNzbDroneService.cs b/src/NzbDrone.Update.Test/StartNzbDroneService.cs index 4cb97c91d..d8f8a5dd9 100644 --- a/src/NzbDrone.Update.Test/StartNzbDroneService.cs +++ b/src/NzbDrone.Update.Test/StartNzbDroneService.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Update.Test Subject.Start(AppType.Service, targetFolder); - Mocker.GetMock<IProcessProvider>().Verify(c => c.SpawnNewProcess("c:\\NzbDrone\\NzbDrone.Console.exe", "/" + StartupContext.NO_BROWSER, null), Times.Once()); + Mocker.GetMock<IProcessProvider>().Verify(c => c.SpawnNewProcess("c:\\NzbDrone\\Radarr.Console.exe", "/" + StartupContext.NO_BROWSER, null), Times.Once()); ExceptionVerification.ExpectedWarns(1); } diff --git a/src/NzbDrone.Update.Test/UpdateProviderStartFixture.cs b/src/NzbDrone.Update.Test/UpdateProviderStartFixture.cs index 6e6456b59..a5a852951 100644 --- a/src/NzbDrone.Update.Test/UpdateProviderStartFixture.cs +++ b/src/NzbDrone.Update.Test/UpdateProviderStartFixture.cs @@ -232,7 +232,7 @@ namespace NzbDrone.Update.Test .Verify(c => c.Start(It.IsAny<string>()), Times.Never()); Mocker.GetMock<IProcessProvider>() - .Verify(c => c.Start(TARGET_FOLDER + "NzbDrone.exe"), Times.Once()); + .Verify(c => c.Start(TARGET_FOLDER + "radarr.exe"), Times.Once()); } diff --git a/src/NzbDrone.Update/NzbDrone.Update.csproj b/src/NzbDrone.Update/NzbDrone.Update.csproj index 2fa4f4bc5..a6721db7a 100644 --- a/src/NzbDrone.Update/NzbDrone.Update.csproj +++ b/src/NzbDrone.Update/NzbDrone.Update.csproj @@ -9,7 +9,7 @@ <OutputType>WinExe</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone.Update</RootNamespace> - <AssemblyName>NzbDrone.Update</AssemblyName> + <AssemblyName>Radarr.Update</AssemblyName> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> <TargetFrameworkProfile> </TargetFrameworkProfile> diff --git a/src/NzbDrone.Update/Properties/AssemblyInfo.cs b/src/NzbDrone.Update/Properties/AssemblyInfo.cs index 5a577baf3..b337f0025 100644 --- a/src/NzbDrone.Update/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Update/Properties/AssemblyInfo.cs @@ -8,5 +8,3 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("e4560a3d-8053-4d57-a260-bfe52f4cc357")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index bad208032..82b1ed116 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Update var startupArgument = new StartupContext(args); NzbDroneLogger.Register(startupArgument, true, true); - Logger.Info("Starting Sonarr Update Client"); + Logger.Info("Starting Radarr Update Client"); _container = UpdateContainerBuilder.Build(startupArgument); @@ -66,9 +66,9 @@ namespace NzbDrone.Update } var startupContext = new UpdateStartupContext - { - ProcessId = ParseProcessId(args[0]) - }; + { + ProcessId = ParseProcessId(args[0]) + }; if (OsInfo.IsNotWindows) { diff --git a/src/NzbDrone.Update/UpdateContainerBuilder.cs b/src/NzbDrone.Update/UpdateContainerBuilder.cs index 2af2a5adc..aeaa130ad 100644 --- a/src/NzbDrone.Update/UpdateContainerBuilder.cs +++ b/src/NzbDrone.Update/UpdateContainerBuilder.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Update { var assemblies = new List<string> { - "NzbDrone.Update", + "Radarr.Update", "NzbDrone.Common" }; diff --git a/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs index d27190f17..2494e43bc 100644 --- a/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs +++ b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Update.UpdateEngine { try { - var targetExecutable = Path.Combine(targetFolder, "NzbDrone.exe"); + var targetExecutable = Path.Combine(targetFolder, "Radarr.exe"); if (File.Exists(targetExecutable)) { diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index 9c2866330..dcd8921f8 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -80,7 +80,7 @@ namespace NzbDrone.Update.UpdateEngine public void Start(string installationFolder, int processId) { _logger.Info("Installation Folder: {0}", installationFolder); - _logger.Info("Updating Sonarr from version {0} to version {1}", _detectExistingVersion.GetExistingVersion(installationFolder), BuildInfo.Version); + _logger.Info("Updating Radarr from version {0} to version {1}", _detectExistingVersion.GetExistingVersion(installationFolder), BuildInfo.Version); Verify(installationFolder, processId); @@ -103,7 +103,7 @@ namespace NzbDrone.Update.UpdateEngine { if (_processProvider.Exists(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) || _processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) { - _logger.Error("Sonarr was restarted prematurely by external process."); + _logger.Error("Radarr was restarted prematurely by external process."); return; } } @@ -146,7 +146,7 @@ namespace NzbDrone.Update.UpdateEngine if (_processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) { - _logger.Info("Sonarr was restarted by external process."); + _logger.Info("Radarr was restarted by external process."); break; } } diff --git a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs index 0a1bc9147..470f28a7a 100644 --- a/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs +++ b/src/NzbDrone.Update/UpdateEngine/StartNzbDrone.cs @@ -62,12 +62,12 @@ namespace NzbDrone.Update.UpdateEngine private void StartWinform(string installationFolder) { - Start(installationFolder, "NzbDrone.exe"); + Start(installationFolder, "Radarr.exe"); } private void StartConsole(string installationFolder) { - Start(installationFolder, "NzbDrone.Console.exe"); + Start(installationFolder, "Radarr.Console.exe"); } private void Start(string installationFolder, string fileName) diff --git a/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs index c881ae54e..cf513e568 100644 --- a/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Windows.Test/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("372cb8dc-5cdf-4fe4-9e1d-725827889bc7")] -[assembly: AssemblyVersion("10.0.0.*")] +[assembly: AssemblyVersion("0.1.0.*")] diff --git a/src/NzbDrone.Windows/Properties/AssemblyInfo.cs b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs index bbeee6014..5edd3dee8 100644 --- a/src/NzbDrone.Windows/Properties/AssemblyInfo.cs +++ b/src/NzbDrone.Windows/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("cea28fa9-43d0-4682-99f2-d364377adbdf")] -[assembly: AssemblyVersion("10.0.0.*")] +[assembly: AssemblyVersion("0.1.0.*")] diff --git a/src/NzbDrone/MessageBoxUserAlert.cs b/src/NzbDrone/MessageBoxUserAlert.cs index 1b5686864..da371ccda 100644 --- a/src/NzbDrone/MessageBoxUserAlert.cs +++ b/src/NzbDrone/MessageBoxUserAlert.cs @@ -1,5 +1,5 @@ using System.Windows.Forms; -using NzbDrone.Host; +using Radarr.Host; namespace NzbDrone { diff --git a/src/NzbDrone/NzbDrone.csproj b/src/NzbDrone/NzbDrone.csproj index d36a0dfc9..3425b3049 100644 --- a/src/NzbDrone/NzbDrone.csproj +++ b/src/NzbDrone/NzbDrone.csproj @@ -9,7 +9,7 @@ <OutputType>WinExe</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>NzbDrone</RootNamespace> - <AssemblyName>NzbDrone</AssemblyName> + <AssemblyName>Radarr</AssemblyName> <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <TargetFrameworkProfile> @@ -54,7 +54,7 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup> - <ApplicationIcon>..\NzbDrone.Host\NzbDrone.ico</ApplicationIcon> + <ApplicationIcon>Resources\Radarr.ico</ApplicationIcon> </PropertyGroup> <PropertyGroup> <StartupObject>NzbDrone.WindowsApp</StartupObject> @@ -147,6 +147,7 @@ <EmbeddedResource Include="Properties\Resources.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Resources.Designer.cs</LastGenOutput> + <SubType>Designer</SubType> </EmbeddedResource> </ItemGroup> <ItemGroup> @@ -155,6 +156,10 @@ </None> <None Include="packages.config" /> </ItemGroup> + <ItemGroup> + <Content Include="Radarr.ico" /> + <None Include="Resources\Radarr.ico" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PreBuildEvent> diff --git a/src/NzbDrone/Properties/AssemblyInfo.cs b/src/NzbDrone/Properties/AssemblyInfo.cs index c1bca6872..1f92e8654 100644 --- a/src/NzbDrone/Properties/AssemblyInfo.cs +++ b/src/NzbDrone/Properties/AssemblyInfo.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("NzbDrone.exe")] +[assembly: AssemblyTitle("Radarr.exe")] [assembly: Guid("67AADCD9-89AA-4D95-8281-3193740E70E5")] -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/NzbDrone/Properties/Resources.Designer.cs b/src/NzbDrone/Properties/Resources.Designer.cs index 65584111d..f01f723db 100644 --- a/src/NzbDrone/Properties/Resources.Designer.cs +++ b/src/NzbDrone/Properties/Resources.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.32559 +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -9,6 +9,9 @@ //------------------------------------------------------------------------------ namespace NzbDrone.Properties { + using System; + + /// <summary> /// A strongly-typed resource class, for looking up localized strings, etc. /// </summary> @@ -60,9 +63,9 @@ namespace NzbDrone.Properties { /// <summary> /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// </summary> - internal static System.Drawing.Icon NzbDroneIcon { + internal static System.Drawing.Icon Radarr { get { - object obj = ResourceManager.GetObject("NzbDroneIcon", resourceCulture); + object obj = ResourceManager.GetObject("Radarr", resourceCulture); return ((System.Drawing.Icon)(obj)); } } diff --git a/src/NzbDrone/Properties/Resources.resx b/src/NzbDrone/Properties/Resources.resx index 408bab357..5d89d1917 100644 --- a/src/NzbDrone/Properties/Resources.resx +++ b/src/NzbDrone/Properties/Resources.resx @@ -118,7 +118,7 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> - <data name="NzbDroneIcon" type="System.Resources.ResXFileRef, System.Windows.Forms"> - <value>..\..\NzbDrone.Host\NzbDrone.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> + <data name="Radarr" type="System.Resources.ResXFileRef, System.Windows.Forms"> + <value>..\Resources\Radarr.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> </data> </root> \ No newline at end of file diff --git a/src/NzbDrone/Radarr.ico b/src/NzbDrone/Radarr.ico new file mode 100644 index 000000000..6f0a8b50e Binary files /dev/null and b/src/NzbDrone/Radarr.ico differ diff --git a/src/NzbDrone/Resources/Radarr.ico b/src/NzbDrone/Resources/Radarr.ico new file mode 100644 index 000000000..4903af4b1 Binary files /dev/null and b/src/NzbDrone/Resources/Radarr.ico differ diff --git a/src/NzbDrone/Resources/Thumbs.db b/src/NzbDrone/Resources/Thumbs.db new file mode 100644 index 000000000..32f10f9f8 Binary files /dev/null and b/src/NzbDrone/Resources/Thumbs.db differ diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 6325593e1..c4415d496 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -4,7 +4,7 @@ using System.Windows.Forms; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Processes; -using NzbDrone.Host; +using Radarr.Host; namespace NzbDrone.SysTray { @@ -38,8 +38,8 @@ namespace NzbDrone.SysTray _trayMenu.MenuItems.Add("-"); _trayMenu.MenuItems.Add("Exit", OnExit); - _trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version); - _trayIcon.Icon = Properties.Resources.NzbDroneIcon; + _trayIcon.Text = string.Format("Radarr - {0}", BuildInfo.Version); + _trayIcon.Icon = Properties.Resources.Radarr; _trayIcon.ContextMenu = _trayMenu; _trayIcon.Visible = true; diff --git a/src/NzbDrone/WindowsApp.cs b/src/NzbDrone/WindowsApp.cs index b99f3d134..8cd0fdc6f 100644 --- a/src/NzbDrone/WindowsApp.cs +++ b/src/NzbDrone/WindowsApp.cs @@ -3,7 +3,7 @@ using System.Windows.Forms; using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation; -using NzbDrone.Host; +using Radarr.Host; using NzbDrone.SysTray; namespace NzbDrone diff --git a/src/Radarr.ico b/src/Radarr.ico new file mode 100644 index 000000000..4903af4b1 Binary files /dev/null and b/src/Radarr.ico differ diff --git a/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs b/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs index 63a2e4bc0..cf198ba29 100644 --- a/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs +++ b/src/ServiceHelpers/ServiceInstall/Properties/AssemblyInfo.cs @@ -5,6 +5,4 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("13976baa-e5ba-42b2-8ad7-8d568b68a53b")] - -[assembly: AssemblyVersion("10.0.0.*")] +[assembly: Guid("13976baa-e5ba-42b2-8ad7-8d568b68a53b")] \ No newline at end of file diff --git a/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs b/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs index 78e881170..5e8880978 100644 --- a/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs +++ b/src/ServiceHelpers/ServiceInstall/ServiceHelper.cs @@ -8,7 +8,7 @@ namespace ServiceInstall { public static class ServiceHelper { - private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "NzbDrone.Console.exe"); + private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Radarr.Console.exe"); private static bool IsAnAdministrator() { @@ -20,7 +20,7 @@ namespace ServiceInstall { if (!File.Exists(NzbDroneExe)) { - Console.WriteLine("Unable to find NzbDrone.Console.exe in the current directory."); + Console.WriteLine("Unable to find Radarr.Console.exe in the current directory."); return; } diff --git a/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs b/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs index c5e087a13..c4ed0f142 100644 --- a/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs +++ b/src/ServiceHelpers/ServiceUninstall/Properties/AssemblyInfo.cs @@ -3,5 +3,3 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("UninstallService")] [assembly: Guid("0a964b21-9de9-40b3-9378-0474fd5f21a8")] - -[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs b/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs index e5fedb19e..1a046b1b3 100644 --- a/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs +++ b/src/ServiceHelpers/ServiceUninstall/ServiceHelper.cs @@ -8,7 +8,7 @@ namespace ServiceUninstall { public static class ServiceHelper { - private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "NzbDrone.Console.exe"); + private static string NzbDroneExe => Path.Combine(new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName, "Radarr.Console.exe"); private static bool IsAnAdministrator() { @@ -20,7 +20,7 @@ namespace ServiceUninstall { if (!File.Exists(NzbDroneExe)) { - Console.WriteLine("Unable to find NzbDrone.exe in the current directory."); + Console.WriteLine("Unable to find Radarr.exe in the current directory."); return; } diff --git a/src/Thumbs.db b/src/Thumbs.db new file mode 100644 index 000000000..f63c67336 Binary files /dev/null and b/src/Thumbs.db differ diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayout.js b/src/UI/Activity/History/Details/HistoryDetailsLayout.js index 5654a3e72..42f583ddb 100644 --- a/src/UI/Activity/History/Details/HistoryDetailsLayout.js +++ b/src/UI/Activity/History/Details/HistoryDetailsLayout.js @@ -32,4 +32,4 @@ module.exports = Marionette.Layout.extend({ vent.trigger(vent.Commands.CloseModalCommand); } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs b/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs index 89a757660..090c46978 100644 --- a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs +++ b/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs @@ -91,7 +91,7 @@ {{/if_eq}} {{#if_eq reason compare="MissingFromDisk"}} - Sonarr was unable to find the file on disk so it was removed + Radarr was unable to find the file on disk so it was removed {{/if_eq}} {{#if_eq reason compare="Upgrade"}} diff --git a/src/UI/Activity/History/HistoryCollection.js b/src/UI/Activity/History/HistoryCollection.js index 3bd564309..8c82b7988 100644 --- a/src/UI/Activity/History/HistoryCollection.js +++ b/src/UI/Activity/History/HistoryCollection.js @@ -50,16 +50,20 @@ var Collection = PageableCollection.extend({ }, sortMappings : { - 'series' : { sortKey : 'series.sortTitle' } + 'movie' : { sortKey : 'movie.title' } }, initialize : function(options) { delete this.queryParams.episodeId; + delete this.queryParams.movieId; if (options) { if (options.episodeId) { this.queryParams.episodeId = options.episodeId; } + if (options.movieId) { + this.queryParams.movieId = options.movieId; + } } }, @@ -80,4 +84,4 @@ Collection = AsFilteredCollection.call(Collection); Collection = AsSortedCollection.call(Collection); Collection = AsPersistedStateCollection.call(Collection); -module.exports = Collection; \ No newline at end of file +module.exports = Collection; diff --git a/src/UI/Activity/History/HistoryLayout.js b/src/UI/Activity/History/HistoryLayout.js index ae7e4c93e..9f9c88f2a 100644 --- a/src/UI/Activity/History/HistoryLayout.js +++ b/src/UI/Activity/History/HistoryLayout.js @@ -2,7 +2,7 @@ var Marionette = require('marionette'); var Backgrid = require('backgrid'); var HistoryCollection = require('./HistoryCollection'); var EventTypeCell = require('../../Cells/EventTypeCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); +var MovieTitleCell = require('../../Cells/MovieTitleHistoryCell'); var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); var HistoryQualityCell = require('./HistoryQualityCell'); @@ -29,11 +29,11 @@ module.exports = Marionette.Layout.extend({ cellValue : 'this' }, { - name : 'series', - label : 'Series', - cell : SeriesTitleCell + name : 'movies', + label : 'Movie Title', + cell : MovieTitleCell, }, - { + /*{ name : 'episode', label : 'Episode', cell : EpisodeNumberCell, @@ -44,7 +44,7 @@ module.exports = Marionette.Layout.extend({ label : 'Episode Title', cell : EpisodeTitleCell, sortable : false - }, + },*/ { name : 'this', label : 'Quality', diff --git a/src/UI/Activity/History/HistoryModel.js b/src/UI/Activity/History/HistoryModel.js index f8ec8c538..967b7ba22 100644 --- a/src/UI/Activity/History/HistoryModel.js +++ b/src/UI/Activity/History/HistoryModel.js @@ -1,12 +1,20 @@ var Backbone = require('backbone'); var SeriesModel = require('../../Series/SeriesModel'); var EpisodeModel = require('../../Series/EpisodeModel'); +var MovieModel = require('../../Movies/MovieModel'); module.exports = Backbone.Model.extend({ parse : function(model) { - model.series = new SeriesModel(model.series); - model.episode = new EpisodeModel(model.episode); - model.episode.set('series', model.series); + if (model.series) { + model.series = new SeriesModel(model.series); + model.episode = new EpisodeModel(model.episode); + model.episode.set('series', model.series); + } + + if (model.movie) { + model.movie = new MovieModel(model.movie); + } + return model; } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/History/HistoryQualityCell.js b/src/UI/Activity/History/HistoryQualityCell.js index c65aa042b..f779c714e 100644 --- a/src/UI/Activity/History/HistoryQualityCell.js +++ b/src/UI/Activity/History/HistoryQualityCell.js @@ -27,4 +27,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Activity/Queue/QueueCollection.js b/src/UI/Activity/Queue/QueueCollection.js index 474cafe6b..bd3aa065e 100644 --- a/src/UI/Activity/Queue/QueueCollection.js +++ b/src/UI/Activity/Queue/QueueCollection.js @@ -35,6 +35,14 @@ var QueueCollection = PageableCollection.extend({ } }, + movie : { + sortValue : function(model, attr) { + var movie = model.get(attr); + + return movie.get('sortTitle'); + } + }, + episode : { sortValue : function(model, attr) { var episode = model.get('episode'); @@ -84,4 +92,4 @@ QueueCollection = AsPageableCollection.call(QueueCollection); var collection = new QueueCollection().bindSignalR(); collection.fetch(); -module.exports = collection; \ No newline at end of file +module.exports = collection; diff --git a/src/UI/Activity/Queue/QueueLayout.js b/src/UI/Activity/Queue/QueueLayout.js index 462c6a568..4416cb07b 100644 --- a/src/UI/Activity/Queue/QueueLayout.js +++ b/src/UI/Activity/Queue/QueueLayout.js @@ -1,7 +1,7 @@ var Marionette = require('marionette'); var Backgrid = require('backgrid'); var QueueCollection = require('./QueueCollection'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); +var SeriesTitleCell = require('../../Cells/MovieTitleCell'); var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); var QualityCell = require('../../Cells/QualityCell'); @@ -28,11 +28,11 @@ module.exports = Marionette.Layout.extend({ cellValue : 'this' }, { - name : 'series', - label : 'Series', + name : 'movie', + label : 'Movie', cell : SeriesTitleCell }, - { + /*{ name : 'episode', label : 'Episode', cell : EpisodeNumberCell @@ -42,7 +42,7 @@ module.exports = Marionette.Layout.extend({ label : 'Episode Title', cell : EpisodeTitleCell, cellValue : 'episode' - }, + },*/ { name : 'quality', label : 'Quality', diff --git a/src/UI/Activity/Queue/QueueModel.js b/src/UI/Activity/Queue/QueueModel.js index f8ec8c538..e9b3fb045 100644 --- a/src/UI/Activity/Queue/QueueModel.js +++ b/src/UI/Activity/Queue/QueueModel.js @@ -1,12 +1,14 @@ var Backbone = require('backbone'); var SeriesModel = require('../../Series/SeriesModel'); var EpisodeModel = require('../../Series/EpisodeModel'); +var MovieModel = require('../../Movies/MovieModel'); module.exports = Backbone.Model.extend({ parse : function(model) { model.series = new SeriesModel(model.series); model.episode = new EpisodeModel(model.episode); model.episode.set('series', model.series); + model.movie = new MovieModel(model.movie); return model; } -}); \ No newline at end of file +}); diff --git a/src/UI/AddMovies/AddMoviesCollection.js b/src/UI/AddMovies/AddMoviesCollection.js new file mode 100644 index 000000000..f133d4fbb --- /dev/null +++ b/src/UI/AddMovies/AddMoviesCollection.js @@ -0,0 +1,22 @@ +var Backbone = require('backbone'); +var MovieModel = require('../Movies/MovieModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/movies/lookup', + model : MovieModel, + + parse : function(response) { + var self = this; + + _.each(response, function(model) { + model.id = undefined; + + if (self.unmappedFolderModel) { + model.path = self.unmappedFolderModel.get('folder').path; + } + }); + + return response; + } +}); diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js new file mode 100644 index 000000000..3dd299959 --- /dev/null +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -0,0 +1,53 @@ +var vent = require('vent'); +var AppLayout = require('../AppLayout'); +var Marionette = require('marionette'); +var RootFolderLayout = require('./RootFolders/RootFolderLayout'); +var ExistingMoviesCollectionView = require('./Existing/AddExistingMovieCollectionView'); +var AddMoviesView = require('./AddMoviesView'); +var ProfileCollection = require('../Profile/ProfileCollection'); +var RootFolderCollection = require('./RootFolders/RootFolderCollection'); +require('../Movies/MoviesCollection'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/AddMoviesLayoutTemplate', + + regions : { + workspace : '#add-movies-workspace' + }, + + events : { + 'click .x-import' : '_importMovies', + 'click .x-add-new' : '_addMovies' + }, + + attributes : { + id : 'add-movies-screen' + }, + + initialize : function() { + ProfileCollection.fetch(); + RootFolderCollection.fetch().done(function() { + RootFolderCollection.synced = true; + }); + }, + + onShow : function() { + this.workspace.show(new AddMoviesView()); + }, + + _folderSelected : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + //TODO: Fix this shit. + this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); + }, + + _importMovies : function() { + this.rootFolderLayout = new RootFolderLayout(); + this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); + AppLayout.modalRegion.show(this.rootFolderLayout); + }, + + _addMovies : function() { + this.workspace.show(new AddMoviesView()); + } +}); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs new file mode 100644 index 000000000..9eccf4d91 --- /dev/null +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -0,0 +1,16 @@ +<div class="row"> + <div class="col-md-12"> + <div class="btn-group add-movies-btn-group btn-group-lg btn-block"> + <button type="button" class="btn btn-default col-md-10 col-xs-8 add-movies-import-btn x-import"> + <i class="icon-sonarr-hdd"/> + Import existing movies on disk + </button> + <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button> + </div> + </div> +</div> +<div class="row"> + <div class="col-md-12"> + <div id="add-movies-workspace"></div> + </div> +</div> diff --git a/src/UI/AddMovies/AddMoviesView.js b/src/UI/AddMovies/AddMoviesView.js new file mode 100644 index 000000000..ca47d5368 --- /dev/null +++ b/src/UI/AddMovies/AddMoviesView.js @@ -0,0 +1,185 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var AddMoviesCollection = require('./AddMoviesCollection'); +var SearchResultCollectionView = require('./SearchResultCollectionView'); +var EmptyView = require('./EmptyView'); +var NotFoundView = require('./NotFoundView'); +var ErrorView = require('./ErrorView'); +var LoadingView = require('../Shared/LoadingView'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/AddMoviesViewTemplate', + + regions : { + searchResult : '#search-result' + }, + + ui : { + moviesSearch : '.x-movies-search', + searchBar : '.x-search-bar', + loadMore : '.x-load-more' + }, + + events : { + 'click .x-load-more' : '_onLoadMore' + }, + + initialize : function(options) { + console.log(options); + this.isExisting = options.isExisting; + this.collection = new AddMoviesCollection(); + + if (this.isExisting) { + this.collection.unmappedFolderModel = this.model; + } + + if (this.isExisting) { + this.className = 'existing-movies'; + } else { + this.className = 'new-movies'; + } + + this.listenTo(vent, vent.Events.MoviesAdded, this._onMoviesAdded); + this.listenTo(this.collection, 'sync', this._showResults); + + this.resultCollectionView = new SearchResultCollectionView({ + collection : this.collection, + isExisting : this.isExisting + }); + + this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); + }, + + onRender : function() { + var self = this; + + this.$el.addClass(this.className); + + this.ui.moviesSearch.keyup(function(e) { + + if (_.contains([ + 9, + 16, + 17, + 18, + 19, + 20, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 91, + 92, + 93 + ], e.keyCode)) { + return; + } + + self._abortExistingSearch(); + self.throttledSearch({ + term : self.ui.moviesSearch.val() + }); + }); + + this._clearResults(); + + if (this.isExisting) { + this.ui.searchBar.hide(); + } + }, + + onShow : function() { + this.ui.moviesSearch.focus(); + }, + + search : function(options) { + var self = this; + + this.collection.reset(); + + if (!options.term || options.term === this.collection.term) { + return Marionette.$.Deferred().resolve(); + } + + this.searchResult.show(new LoadingView()); + this.collection.term = options.term; + this.currentSearchPromise = this.collection.fetch({ + data : { term : options.term } + }); + + this.currentSearchPromise.fail(function() { + self._showError(); + }); + + return this.currentSearchPromise; + }, + + _onMoviesAdded : function(options) { + if (this.isExisting && options.movie.get('path') === this.model.get('folder').path) { + this.close(); + } + + else if (!this.isExisting) { + this.resultCollectionView.setExisting(options.movie.get('tmdbId')); + /*this.collection.term = ''; + this.collection.reset(); + this._clearResults(); + this.ui.moviesSearch.val(''); + this.ui.moviesSearch.focus();*/ //TODO: Maybe add option wheter to clear search result. + } + }, + + _onLoadMore : function() { + var showingAll = this.resultCollectionView.showMore(); + this.ui.searchBar.show(); + + if (showingAll) { + this.ui.loadMore.hide(); + } + }, + + _clearResults : function() { + + if (!this.isExisting) { + this.searchResult.show(new EmptyView()); + } else { + this.searchResult.close(); + } + }, + + _showResults : function() { + if (!this.isClosed) { + if (this.collection.length === 0) { + this.ui.searchBar.show(); + this.searchResult.show(new NotFoundView({ term : this.collection.term })); + } else { + this.searchResult.show(this.resultCollectionView); + if (!this.showingAll && this.isExisting) { + this.ui.loadMore.show(); + } + } + } + }, + + _abortExistingSearch : function() { + if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { + console.log('aborting previous pending search request.'); + this.currentSearchPromise.abort(); + } else { + this._clearResults(); + } + }, + + _showError : function() { + if (!this.isClosed) { + this.ui.searchBar.show(); + this.searchResult.show(new ErrorView({ term : this.collection.term })); + this.collection.term = ''; + } + } +}); diff --git a/src/UI/AddMovies/AddMoviesViewTemplate.hbs b/src/UI/AddMovies/AddMoviesViewTemplate.hbs new file mode 100644 index 000000000..9f9e0660c --- /dev/null +++ b/src/UI/AddMovies/AddMoviesViewTemplate.hbs @@ -0,0 +1,24 @@ +{{#if folder.path}} +<div class="unmapped-folder-path"> + <div class="col-md-12"> + {{folder.path}} + </div> +</div>{{/if}} +<div class="x-search-bar"> + <div class="input-group input-group-lg add-movies-search"> + <span class="input-group-addon"><i class="icon-sonarr-search"/></span> + + {{#if folder}} + <input type="text" class="form-control x-movies-search" value="{{folder.name}}"> + {{else}} + <input type="text" class="form-control x-movies-search" placeholder="Start typing the name of the movie you want to add ..."> + {{/if}} + </div> +</div> +<div class="row"> + <div id="search-result" class="result-list col-md-12"/> +</div> +<div class="btn btn-block text-center new-movies-loadmore x-load-more" style="display: none;"> + <i class="icon-sonarr-load-more"/> + more +</div> diff --git a/src/UI/AddMovies/EmptyView.js b/src/UI/AddMovies/EmptyView.js new file mode 100644 index 000000000..19cdc7bff --- /dev/null +++ b/src/UI/AddMovies/EmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/EmptyViewTemplate' +}); diff --git a/src/UI/AddMovies/EmptyViewTemplate.hbs b/src/UI/AddMovies/EmptyViewTemplate.hbs new file mode 100644 index 000000000..681bd1933 --- /dev/null +++ b/src/UI/AddMovies/EmptyViewTemplate.hbs @@ -0,0 +1,3 @@ +<div class="text-center hint col-md-12"> + <span>You can also search by imdbid using the imdb: prefixes.</span> +</div> diff --git a/src/UI/AddMovies/ErrorView.js b/src/UI/AddMovies/ErrorView.js new file mode 100644 index 000000000..f953834db --- /dev/null +++ b/src/UI/AddMovies/ErrorView.js @@ -0,0 +1,13 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/ErrorViewTemplate', + + initialize : function(options) { + this.options = options; + }, + + templateHelpers : function() { + return this.options; + } +}); diff --git a/src/UI/AddMovies/ErrorViewTemplate.hbs b/src/UI/AddMovies/ErrorViewTemplate.hbs new file mode 100644 index 000000000..511d29952 --- /dev/null +++ b/src/UI/AddMovies/ErrorViewTemplate.hbs @@ -0,0 +1,7 @@ +<div class="text-center col-md-12"> + <h3> + There was an error searching for '{{term}}'. + </h3> + + If the movie title contains non-alphanumeric characters try removing them, otherwise try your search again later. +</div> diff --git a/src/UI/AddMovies/Existing/AddExistingMovieCollectionView.js b/src/UI/AddMovies/Existing/AddExistingMovieCollectionView.js new file mode 100644 index 000000000..1e3d38b6a --- /dev/null +++ b/src/UI/AddMovies/Existing/AddExistingMovieCollectionView.js @@ -0,0 +1,51 @@ +var Marionette = require('marionette'); +var AddMoviesView = require('../AddMoviesView'); +var UnmappedFolderCollection = require('./UnmappedFolderCollection'); + +module.exports = Marionette.CompositeView.extend({ + itemView : AddMoviesView, + itemViewContainer : '.x-loading-folders', + template : 'AddMovies/Existing/AddExistingMovieCollectionViewTemplate', + + ui : { + loadingFolders : '.x-loading-folders' + }, + + initialize : function() { + this.collection = new UnmappedFolderCollection(); + this.collection.importItems(this.model); + }, + + showCollection : function() { + this._showAndSearch(0); + }, + + appendHtml : function(collectionView, itemView, index) { + collectionView.ui.loadingFolders.before(itemView.el); + }, + + _showAndSearch : function(index) { + var self = this; + var model = this.collection.at(index); + + if (model) { + var currentIndex = index; + var folderName = model.get('folder').name; + this.addItemView(model, this.getItemView(), index); + this.children.findByModel(model).search({ term : folderName }).always(function() { + if (!self.isClosed) { + self._showAndSearch(currentIndex + 1); + } + }); + } + + else { + this.ui.loadingFolders.hide(); + } + }, + + itemViewOptions : { + isExisting : true + } + +}); \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/AddExistingMovieCollectionViewTemplate.hbs b/src/UI/AddMovies/Existing/AddExistingMovieCollectionViewTemplate.hbs new file mode 100644 index 000000000..0928c0f38 --- /dev/null +++ b/src/UI/AddMovies/Existing/AddExistingMovieCollectionViewTemplate.hbs @@ -0,0 +1,5 @@ +<div class="x-existing-folders"> + <div class="loading-folders x-loading-folders"> + Loading search results from TheTVDB for your movies, this may take a few minutes. + </div> +</div> \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/UnmappedFolderCollection.js b/src/UI/AddMovies/Existing/UnmappedFolderCollection.js new file mode 100644 index 000000000..bd2a83f49 --- /dev/null +++ b/src/UI/AddMovies/Existing/UnmappedFolderCollection.js @@ -0,0 +1,20 @@ +var Backbone = require('backbone'); +var UnmappedFolderModel = require('./UnmappedFolderModel'); +var _ = require('underscore'); + +module.exports = Backbone.Collection.extend({ + model : UnmappedFolderModel, + + importItems : function(rootFolderModel) { + + this.reset(); + var rootFolder = rootFolderModel; + + _.each(rootFolderModel.get('unmappedFolders'), function(folder) { + this.push(new UnmappedFolderModel({ + rootFolder : rootFolder, + folder : folder + })); + }, this); + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/Existing/UnmappedFolderModel.js b/src/UI/AddMovies/Existing/UnmappedFolderModel.js new file mode 100644 index 000000000..3986a5948 --- /dev/null +++ b/src/UI/AddMovies/Existing/UnmappedFolderModel.js @@ -0,0 +1,3 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/AddMovies/MonitoringTooltipTemplate.hbs b/src/UI/AddMovies/MonitoringTooltipTemplate.hbs new file mode 100644 index 000000000..66ad7d77f --- /dev/null +++ b/src/UI/AddMovies/MonitoringTooltipTemplate.hbs @@ -0,0 +1,6 @@ +<dl class="monitor-tooltip-contents"> + <dt>Yes</dt> + <dd>Monitor for new releases</dd> + <dt>No</dt> + <dd>Do not monitor for new releases</dd> +</dl> \ No newline at end of file diff --git a/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs b/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs new file mode 100644 index 000000000..d63e9f60b --- /dev/null +++ b/src/UI/AddMovies/MoviesTypeSelectionPartial.hbs @@ -0,0 +1,3 @@ +<select class="form-control col-md-2 x-movie-type" name="movieType"> + <option value="standard">Standard</option> +</select> diff --git a/src/UI/AddMovies/NotFoundView.js b/src/UI/AddMovies/NotFoundView.js new file mode 100644 index 000000000..928a17392 --- /dev/null +++ b/src/UI/AddMovies/NotFoundView.js @@ -0,0 +1,13 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/NotFoundViewTemplate', + + initialize : function(options) { + this.options = options; + }, + + templateHelpers : function() { + return this.options; + } +}); diff --git a/src/UI/AddMovies/NotFoundViewTemplate.hbs b/src/UI/AddMovies/NotFoundViewTemplate.hbs new file mode 100644 index 000000000..7cc6eb25a --- /dev/null +++ b/src/UI/AddMovies/NotFoundViewTemplate.hbs @@ -0,0 +1,7 @@ +<div class="text-center col-md-12"> + <h3> + Sorry. We couldn't find any movies matching '{{term}}' + </h3> + <a href="https://github.com/NzbDrone/NzbDrone/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">[UPDATE LINK] Why can't I find my movie?</a> + +</div> diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollection.js b/src/UI/AddMovies/RootFolders/RootFolderCollection.js new file mode 100644 index 000000000..81050c19d --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollection.js @@ -0,0 +1,10 @@ +var Backbone = require('backbone'); +var RootFolderModel = require('./RootFolderModel'); +require('../../Mixins/backbone.signalr.mixin'); + +var RootFolderCollection = Backbone.Collection.extend({ + url : window.NzbDrone.ApiRoot + '/rootfolder', + model : RootFolderModel +}); + +module.exports = new RootFolderCollection(); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js new file mode 100644 index 000000000..f0704f342 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var RootFolderItemView = require('./RootFolderItemView'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/RootFolders/RootFolderCollectionViewTemplate', + itemViewContainer : '.x-root-folders', + itemView : RootFolderItemView +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs new file mode 100644 index 000000000..70755bbca --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderCollectionViewTemplate.hbs @@ -0,0 +1,13 @@ +<table class="table table-hover"> + <thead> + <tr> + <th class="col-md-10 "> + Path + </th> + <th class="col-md-3"> + Free Space + </th> + </tr> + </thead> + <tbody class="x-root-folders"></tbody> +</table> \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderItemView.js b/src/UI/AddMovies/RootFolders/RootFolderItemView.js new file mode 100644 index 000000000..7397f4e94 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderItemView.js @@ -0,0 +1,28 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'AddMovies/RootFolders/RootFolderItemViewTemplate', + className : 'recent-folder', + tagName : 'tr', + + initialize : function() { + this.listenTo(this.model, 'change', this.render); + }, + + events : { + 'click .x-delete' : 'removeFolder', + 'click .x-folder' : 'folderSelected' + }, + + removeFolder : function() { + var self = this; + + this.model.destroy().success(function() { + self.close(); + }); + }, + + folderSelected : function() { + this.trigger('folderSelected', this.model); + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs new file mode 100644 index 000000000..2203e1efd --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderItemViewTemplate.hbs @@ -0,0 +1,9 @@ +<td class="col-md-10 x-folder folder-path"> + {{path}} +</td> +<td class="col-md-3 x-folder folder-free-space"> + <span>{{Bytes freeSpace}}</span> +</td> +<td class="col-md-1"> + <i class="icon-sonarr-delete x-delete"></i> +</td> diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayout.js b/src/UI/AddMovies/RootFolders/RootFolderLayout.js new file mode 100644 index 000000000..f48890076 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderLayout.js @@ -0,0 +1,77 @@ +var Marionette = require('marionette'); +var RootFolderCollectionView = require('./RootFolderCollectionView'); +var RootFolderCollection = require('./RootFolderCollection'); +var RootFolderModel = require('./RootFolderModel'); +var LoadingView = require('../../Shared/LoadingView'); +var AsValidatedView = require('../../Mixins/AsValidatedView'); +require('../../Mixins/FileBrowser'); + +var Layout = Marionette.Layout.extend({ + template : 'AddMovies/RootFolders/RootFolderLayoutTemplate', + + ui : { + pathInput : '.x-path' + }, + + regions : { + currentDirs : '#current-dirs' + }, + + events : { + 'click .x-add' : '_addFolder', + 'keydown .x-path input' : '_keydown' + }, + + initialize : function() { + this.collection = RootFolderCollection; + this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); + + this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); + }, + + onShow : function() { + this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs); + this.currentDirs.show(new LoadingView()); + + if (RootFolderCollection.synced) { + this._showCurrentDirs(); + } + + this.ui.pathInput.fileBrowser(); + }, + + _onFolderSelected : function(options) { + this.trigger('folderSelected', options); + }, + + _addFolder : function() { + var self = this; + + var newDir = new RootFolderModel({ + Path : this.ui.pathInput.val() + }); + + this.bindToModelValidation(newDir); + + newDir.save().done(function() { + RootFolderCollection.add(newDir); + self.trigger('folderSelected', { model : newDir }); + }); + }, + + _showCurrentDirs : function() { + this.currentDirs.show(this.rootfolderListView); + }, + + _keydown : function(e) { + if (e.keyCode !== 13) { + return; + } + + this._addFolder(); + } +}); + +var Layout = AsValidatedView.apply(Layout); + +module.exports = Layout; \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs new file mode 100644 index 000000000..1d6eae265 --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderLayoutTemplate.hbs @@ -0,0 +1,36 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Select Folder</h3> + </div> + <div class="modal-body root-folders-modal"> + <div class="validation-errors"></div> + <div class="alert alert-info">Enter the path that contains some or all of your movies, you will be able to choose which movies you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> + + <div class="row"> + <div class="form-group"> + + <div class="col-md-12"> + + <div class="input-group"> + <span class="input-group-addon"> <i class="icon-sonarr-folder-open"></i></span> + <input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your movies"> + <span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-sonarr-ok"/></button></span> + </div> + </div> + </div> + </div> + + <div class="row root-folders"> + <div class="col-md-12"> + {{#if items}} + <h4>Recent Folders</h4> + {{/if}} + <div id="current-dirs" class="root-folders-list"></div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">Close</button> + </div> +</div> diff --git a/src/UI/AddMovies/RootFolders/RootFolderModel.js b/src/UI/AddMovies/RootFolders/RootFolderModel.js new file mode 100644 index 000000000..28681768b --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderModel.js @@ -0,0 +1,8 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/rootfolder', + defaults : { + freeSpace : 0 + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs b/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs new file mode 100644 index 000000000..56729b0dd --- /dev/null +++ b/src/UI/AddMovies/RootFolders/RootFolderSelectionPartial.hbs @@ -0,0 +1,11 @@ +<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath"> + {{#if this}} + {{#each this}} + <option value="{{id}}">{{path}}</option> + {{/each}} + {{else}} + <option value="">Select Path</option> + {{/if}} + <option value="addNew">Add a different path</option> +</select> + diff --git a/src/UI/AddMovies/SearchResultCollectionView.js b/src/UI/AddMovies/SearchResultCollectionView.js new file mode 100644 index 000000000..02fe1fa41 --- /dev/null +++ b/src/UI/AddMovies/SearchResultCollectionView.js @@ -0,0 +1,41 @@ +var Marionette = require('marionette'); +var SearchResultView = require('./SearchResultView'); + +module.exports = Marionette.CollectionView.extend({ + itemView : SearchResultView, + + initialize : function(options) { + this.isExisting = options.isExisting; + this.showing = 1; + }, + + showAll : function() { + this.showingAll = true; + this.render(); + }, + + showMore : function() { + this.showing += 5; + this.render(); + + return this.showing >= this.collection.length; + }, + + setExisting : function(tmdbid) { + var movies = this.collection.where({ tmdbId : tmdbid }); + console.warn(movies); + //debugger; + if (movies.length > 0) { + this.children.findByModel(movies[0])._configureTemplateHelpers(); + //this.children.findByModel(movies[0])._configureTemplateHelpers(); + this.children.findByModel(movies[0]).render(); + //this.templateHelpers.existing = existingMovies[0].toJSON(); + } + }, + + appendHtml : function(collectionView, itemView, index) { + if (!this.isExisting || index < this.showing || index === 0) { + collectionView.$el.append(itemView.el); + } + } +}); diff --git a/src/UI/AddMovies/SearchResultView.js b/src/UI/AddMovies/SearchResultView.js new file mode 100644 index 000000000..f563e6f66 --- /dev/null +++ b/src/UI/AddMovies/SearchResultView.js @@ -0,0 +1,242 @@ +var _ = require('underscore'); +var vent = require('vent'); +var AppLayout = require('../AppLayout'); +var Backbone = require('backbone'); +var Marionette = require('marionette'); +var Profiles = require('../Profile/ProfileCollection'); +var RootFolders = require('./RootFolders/RootFolderCollection'); +var RootFolderLayout = require('./RootFolders/RootFolderLayout'); +var MoviesCollection = require('../Movies/MoviesCollection'); +var Config = require('../Config'); +var Messenger = require('../Shared/Messenger'); +var AsValidatedView = require('../Mixins/AsValidatedView'); + +require('jquery.dotdotdot'); + +var view = Marionette.ItemView.extend({ + + template : 'AddMovies/SearchResultViewTemplate', + + ui : { + profile : '.x-profile', + rootFolder : '.x-root-folder', + seasonFolder : '.x-season-folder', + monitor : '.x-monitor', + monitorTooltip : '.x-monitor-tooltip', + addButton : '.x-add', + addSearchButton : '.x-add-search', + overview : '.x-overview' + }, + + events : { + 'click .x-add' : '_addWithoutSearch', + 'click .x-add-search' : '_addAndSearch', + 'change .x-profile' : '_profileChanged', + 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-season-folder' : '_seasonFolderChanged', + 'change .x-monitor' : '_monitorChanged' + }, + + initialize : function() { + + if (!this.model) { + throw 'model is required'; + } + + console.log(this.route); + + this.templateHelpers = {}; + this._configureTemplateHelpers(); + + this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); + this.listenTo(this.model, 'change', this.render); + this.listenTo(RootFolders, 'all', this._rootFoldersUpdated); + }, + + onRender : function() { + + var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); + var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true); + var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'all'); + + if (Profiles.get(defaultProfile)) { + this.ui.profile.val(defaultProfile); + } + + if (RootFolders.get(defaultRoot)) { + this.ui.rootFolder.val(defaultRoot); + } + + this.ui.seasonFolder.prop('checked', useSeasonFolder); + this.ui.monitor.val(defaultMonitorEpisodes); + + //TODO: make this work via onRender, FM? + //works with onShow, but stops working after the first render + this.ui.overview.dotdotdot({ + height : 120 + }); + + this.templateFunction = Marionette.TemplateCache.get('AddMovies/MonitoringTooltipTemplate'); + var content = this.templateFunction(); + + this.ui.monitorTooltip.popover({ + content : content, + html : true, + trigger : 'hover', + title : 'Movie Monitoring Options', + placement : 'right', + container : this.$el + }); + }, + + _configureTemplateHelpers : function() { + var existingMovies = MoviesCollection.where({ tmdbId : this.model.get('tmdbId') }); + console.log(existingMovies); + if (existingMovies.length > 0) { + this.templateHelpers.existing = existingMovies[0].toJSON(); + } + + this.templateHelpers.profiles = Profiles.toJSON(); + console.log(this.model); + console.log(this.templateHelpers.existing); + if (!this.model.get('isExisting')) { + this.templateHelpers.rootFolders = RootFolders.toJSON(); + } + }, + + _onConfigUpdated : function(options) { + if (options.key === Config.Keys.DefaultProfileId) { + this.ui.profile.val(options.value); + } + + else if (options.key === Config.Keys.DefaultRootFolderId) { + this.ui.rootFolder.val(options.value); + } + + else if (options.key === Config.Keys.UseSeasonFolder) { + this.ui.seasonFolder.prop('checked', options.value); + } + + else if (options.key === Config.Keys.MonitorEpisodes) { + this.ui.monitor.val(options.value); + } + }, + + _profileChanged : function() { + Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val()); + }, + + _seasonFolderChanged : function() { + Config.setValue(Config.Keys.UseSeasonFolder, this.ui.seasonFolder.prop('checked')); + }, + + _rootFolderChanged : function() { + var rootFolderValue = this.ui.rootFolder.val(); + if (rootFolderValue === 'addNew') { + var rootFolderLayout = new RootFolderLayout(); + this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); + AppLayout.modalRegion.show(rootFolderLayout); + } else { + Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); + } + }, + + _monitorChanged : function() { + Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val()); + }, + + _setRootFolder : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.ui.rootFolder.val(options.model.id); + this._rootFolderChanged(); + }, + + _addWithoutSearch : function() { + this._addMovies(false); + }, + + _addAndSearch : function() { + this._addMovies(true); + }, + + _addMovies : function(searchForMovie) { + var addButton = this.ui.addButton; + var addSearchButton = this.ui.addSearchButton; + + addButton.addClass('disabled'); + addSearchButton.addClass('disabled'); + + var profile = this.ui.profile.val(); + var rootFolderPath = this.ui.rootFolder.children(':selected').text(); + var monitor = this.ui.monitor.val(); + + var options = this._getAddMoviesOptions(); + options.searchForMovie = searchForMovie; + console.warn(searchForMovie); + + this.model.set({ + profileId : profile, + rootFolderPath : rootFolderPath, + addOptions : options, + monitored : (monitor === 'all' ? true : false) + }, { silent : true }); + + var self = this; + var promise = this.model.save(); + + console.log(this.model.save); + console.log(promise); + + if (searchForMovie) { + this.ui.addSearchButton.spinForPromise(promise); + } + + else { + this.ui.addButton.spinForPromise(promise); + } + + promise.always(function() { + addButton.removeClass('disabled'); + addSearchButton.removeClass('disabled'); + }); + + promise.done(function() { + MoviesCollection.add(self.model); + + self.close(); + + Messenger.show({ + message : 'Added: ' + self.model.get('title'), + actions : { + goToSeries : { + label : 'Go to Movie', + action : function() { + Backbone.history.navigate('/movies/' + self.model.get('titleSlug'), { trigger : true }); + } + } + }, + hideAfter : 8, + hideOnNavigate : true + }); + + vent.trigger(vent.Events.MoviesAdded, { movie : self.model }); + }); + }, + + _rootFoldersUpdated : function() { + this._configureTemplateHelpers(); + this.render(); + }, + + _getAddMoviesOptions : function() { + return { + ignoreEpisodesWithFiles : false, + ignoreEpisodesWithoutFiles : false + }; + } +}); + +AsValidatedView.apply(view); + +module.exports = view; diff --git a/src/UI/AddMovies/SearchResultViewTemplate.hbs b/src/UI/AddMovies/SearchResultViewTemplate.hbs new file mode 100644 index 000000000..fbcb54777 --- /dev/null +++ b/src/UI/AddMovies/SearchResultViewTemplate.hbs @@ -0,0 +1,105 @@ +<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}"> + <div class="row"> + <div class="col-md-2"> + <a href="{{tmdbUrl}}" target="_blank"> + {{#if remotePoster}} + {{remotePoster}} + {{else}} + {{poster}} + {{/if}} + </a> + </div> + <div class="col-md-10"> + <div class="row"> + <div class="col-md-12"> + <h2 class="movies-title"> + {{titleWithYear}} + + <span class="labels"> + <span class="label label-default">{{network}}</span> + {{#unless_eq status compare="announced"}} + <span class="label label-danger">Released</span> <!-- TODO: Better handling of cases here! --> + {{/unless_eq}} + </span> + </h2> + </div> + </div> + <div class="row new-movies-overview x-overview"> + <div class="col-md-12 overview-internal"> + {{overview}} + </div> + </div> + <div class="row"> + {{#unless existing}} + {{#unless path}} + <div class="form-group col-md-4"> + <label>Path</label> + {{> RootFolderSelectionPartial rootFolders}} + </div> + {{/unless}} + + <div class="form-group col-md-2"> + <label>Monitor <i class="icon-sonarr-form-info monitor-tooltip x-monitor-tooltip"></i></label> + <select class="form-control col-md-2 x-monitor"> + <option value="all">Yes</option> + <!-- <option value="missing">Missing</option> --> + <option value="none">No</option> + </select> + </div> + + <div class="form-group col-md-2"> + <label>Profile</label> + {{> ProfileSelectionPartial profiles}} + </div> + + {{!--<div class="form-group col-md-2"> + <label>Season Folders</label> + + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" class="x-season-folder"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + <div class="btn btn-primary slide-button"/> + </label> + </div> + </div>--}} + {{/unless}} + + {{#unless existing}} + {{#if title}} + <div class="form-group col-md-2"> + <!--Uncomment if we need to add even more controls to add Movies--> + <label style="visibility: hidden">Add</label> + <div class="btn-group"> + <button class="btn btn-success add x-add" title="Add"> + <i class="icon-sonarr-add"></i> + </button> + + <button class="btn btn-success add x-add-search" title="Add and Search for movie"> + <i class="icon-sonarr-search"></i> + </button> + </div> + </div> + {{else}} + <label style="visibility: hidden">Add</label> + <div class="col-md-2" title="Movies require an English title"> + <button class="btn add-movies disabled"> + Add + </button> + </div> + {{/if}} + {{else}} + <label style="visibility: hidden">Add</label> + <div class="col-md-2 col-md-offset-10"> + <a class="btn btn-default" href="{{route}}"> + Already Exists + </a> + </div> + {{/unless}} + </div> + </div> + </div> +</div> diff --git a/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs b/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs new file mode 100644 index 000000000..e5623e33a --- /dev/null +++ b/src/UI/AddMovies/StartingSeasonSelectionPartial.hbs @@ -0,0 +1,13 @@ +<select class="form-control col-md-2 starting-season x-starting-season"> + + + {{#each this}} + {{#if_eq seasonNumber compare="0"}} + <option value="{{seasonNumber}}">Specials</option> + {{else}} + <option value="{{seasonNumber}}">Season {{seasonNumber}}</option> + {{/if_eq}} + {{/each}} + + <option value="5000000">None</option> +</select> diff --git a/src/UI/AddMovies/addMovies.less b/src/UI/AddMovies/addMovies.less new file mode 100644 index 000000000..e1eaad2c8 --- /dev/null +++ b/src/UI/AddMovies/addMovies.less @@ -0,0 +1,177 @@ +@import "../Shared/Styles/card.less"; +@import "../Shared/Styles/clickable.less"; + +#add-movies-screen { + .existing-movies { + + .card(); + margin : 30px 0px; + + .unmapped-folder-path { + padding: 20px; + margin-left : 0px; + font-weight : 100; + font-size : 25px; + text-align : center; + } + + .new-movies-loadmore { + font-size : 30px; + font-weight : 300; + padding-top : 10px; + padding-bottom : 10px; + } + } + + .new-movies { + .search-item { + .card(); + margin : 40px 0px; + } + } + + .add-movies-search { + margin-top : 20px; + margin-bottom : 20px; + } + + .search-item { + + padding-bottom : 20px; + + .btn-group{ + display: table; + } + + .movies-title { + margin-top : 5px; + + .labels { + margin-left : 10px; + + .label { + font-size : 12px; + vertical-align : middle; + } + } + + .year { + font-style : italic; + color : #aaaaaa; + } + } + + .new-movies-overview { + overflow : hidden; + height : 103px; + + .overview-internal { + overflow : hidden; + height : 80px; + } + } + + .movies-poster { + min-width : 138px; + min-height : 203px; + max-width : 138px; + max-height : 203px; + margin : 10px; + } + + a { + color : #343434; + } + + a:hover { + text-decoration : none; + } + + select { + font-size : 14px; + } + + .checkbox { + margin-top : 0px; + } + + .add { + i { + &:before { + color : #ffffff; + } + } + } + + .monitor-tooltip { + margin-left : 5px; + } + } + + .loading-folders { + margin : 30px 0px; + text-align: center; + } + + .hint { + color : #999999; + font-style : italic; + } + + .monitor-tooltip-contents { + padding-bottom : 0px; + + dd { + padding-bottom : 8px; + } + } +} + +li.add-new { + .clickable; + + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: rgb(51, 51, 51); + white-space: nowrap; +} + +li.add-new:hover { + text-decoration: none; + color: rgb(255, 255, 255); + background-color: rgb(0, 129, 194); +} + +.root-folders-modal { + overflow : visible; + + .root-folders-list { + overflow-y : auto; + max-height : 300px; + + i { + .clickable(); + } + } + + .validation-errors { + display : none; + } + + .input-group { + .form-control { + background-color : white; + } + } + + .root-folders { + margin-top : 20px; + } + + .recent-folder { + .clickable(); + } +} diff --git a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs index ab6e5e6c0..097a7ed75 100644 --- a/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesLayoutTemplate.hbs @@ -5,7 +5,7 @@ <i class="icon-sonarr-hdd"/> Import existing series on disk </button> - <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Series</button> + <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-sonarr-active hidden-xs"></i> Add New Movie</button> </div> </div> </div> @@ -14,4 +14,3 @@ <div id="add-series-workspace"></div> </div> </div> - diff --git a/src/UI/AddSeries/AddSeriesViewTemplate.hbs b/src/UI/AddSeries/AddSeriesViewTemplate.hbs index 18ed2ffb3..8048f48ae 100644 --- a/src/UI/AddSeries/AddSeriesViewTemplate.hbs +++ b/src/UI/AddSeries/AddSeriesViewTemplate.hbs @@ -11,7 +11,7 @@ {{#if folder}} <input type="text" class="form-control x-series-search" value="{{folder.name}}"> {{else}} - <input type="text" class="form-control x-series-search" placeholder="Start typing the name of series you want to add ..."> + <input type="text" class="form-control x-series-search" placeholder="Start typing the name of the movie you want to add ..."> {{/if}} </div> </div> diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.hbs b/src/UI/Calendar/CalendarFeedViewTemplate.hbs index c192c740d..8d8e3c7be 100644 --- a/src/UI/Calendar/CalendarFeedViewTemplate.hbs +++ b/src/UI/Calendar/CalendarFeedViewTemplate.hbs @@ -1,7 +1,7 @@ <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Sonarr Calendar feed</h3> + <h3>Radarr Calendar feed</h3> </div> <div class="modal-body edit-series-modal"> <div class="form-horizontal"> diff --git a/src/UI/Cells/DownloadedQualityCell.js b/src/UI/Cells/DownloadedQualityCell.js new file mode 100644 index 000000000..1a7d9c354 --- /dev/null +++ b/src/UI/Cells/DownloadedQualityCell.js @@ -0,0 +1,28 @@ +var Backgrid = require('backgrid'); +var ProfileCollection = require('../Profile/ProfileCollection'); +var _ = require('underscore'); + +module.exports = Backgrid.Cell.extend({ + className : 'profile-cell', + + _originalInit : Backgrid.Cell.prototype.initialize, + + initialize : function () { + this._originalInit.apply(this, arguments); + + this.listenTo(ProfileCollection, 'sync', this.render); + }, + + render : function() { + + this.$el.empty(); + if (this.model.get("movieFile")) { + var profileId = this.model.get("movieFile").quality.quality.id; + this.$el.html(this.model.get("movieFile").quality.quality.name); + + } + + + return this; + } +}); diff --git a/src/UI/Cells/EditionCell.js b/src/UI/Cells/EditionCell.js new file mode 100644 index 000000000..c110807f5 --- /dev/null +++ b/src/UI/Cells/EditionCell.js @@ -0,0 +1,41 @@ +var Backgrid = require('backgrid'); +var Marionette = require('marionette'); +require('bootstrap'); + +module.exports = Backgrid.Cell.extend({ + className : 'edition-cell', + //template : 'Cells/EditionCellTemplate', + + render : function() { + + var edition = this.model.get(this.column.get('name')); + if (!edition) { + return this; + } + var cut = false; + + if (edition.toLowerCase().contains("cut")) { + cut = true; + } + + //this.templateFunction = Marionette.TemplateCache.get(this.template); + + //var html = this.templateFunction(edition); + if (cut) { + this.$el.html('<i class="icon-sonarr-form-cut"/ title="{0}">'.format(edition)); + } else { + this.$el.html('<i class="icon-sonarr-form-special"/ title="{0}">'.format(edition)); + } + + /*this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : this.column.get('title'), + placement : 'left', + container : this.$el + });*/ + + return this; + } +}); diff --git a/src/UI/Cells/EditionCellTemplate.hbs b/src/UI/Cells/EditionCellTemplate.hbs new file mode 100644 index 000000000..9b4f43449 --- /dev/null +++ b/src/UI/Cells/EditionCellTemplate.hbs @@ -0,0 +1,5 @@ +<ul> + <li> + {{this}} + </li> +</ul> diff --git a/src/UI/Cells/EpisodeActionsCell.js b/src/UI/Cells/EpisodeActionsCell.js index 383942d34..0a8d20211 100644 --- a/src/UI/Cells/EpisodeActionsCell.js +++ b/src/UI/Cells/EpisodeActionsCell.js @@ -35,10 +35,11 @@ module.exports = NzbDroneCell.extend({ }, _manualSearch : function() { + console.warn(this.cellValue); vent.trigger(vent.Commands.ShowEpisodeDetails, { episode : this.cellValue, hideSeriesLink : true, openingTab : 'search' }); } -}); \ No newline at end of file +}); diff --git a/src/UI/Cells/EpisodeProgressCell.js b/src/UI/Cells/EpisodeProgressCell.js index 6208040c4..84f39cb8a 100644 --- a/src/UI/Cells/EpisodeProgressCell.js +++ b/src/UI/Cells/EpisodeProgressCell.js @@ -25,4 +25,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Cells/EventTypeCell.js b/src/UI/Cells/EventTypeCell.js index 4ca9a85ae..5426c7efb 100644 --- a/src/UI/Cells/EventTypeCell.js +++ b/src/UI/Cells/EventTypeCell.js @@ -13,23 +13,23 @@ module.exports = NzbDroneCell.extend({ switch (this.cellValue.get('eventType')) { case 'grabbed': icon = 'icon-sonarr-downloading'; - toolTip = 'Episode grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer); + toolTip = 'Movie grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer); break; case 'seriesFolderImported': icon = 'icon-sonarr-hdd'; - toolTip = 'Existing episode file added to library'; + toolTip = 'Existing movie file added to library'; break; case 'downloadFolderImported': icon = 'icon-sonarr-imported'; - toolTip = 'Episode downloaded successfully and picked up from download client'; + toolTip = 'Movie downloaded successfully and picked up from download client'; break; case 'downloadFailed': icon = 'icon-sonarr-download-failed'; - toolTip = 'Episode download failed'; + toolTip = 'Movie download failed'; break; case 'episodeFileDeleted': icon = 'icon-sonarr-deleted'; - toolTip = 'Episode file deleted'; + toolTip = 'Movie file deleted'; break; default: icon = 'icon-sonarr-unknown'; @@ -41,4 +41,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Cells/FileTitleCell.js b/src/UI/Cells/FileTitleCell.js new file mode 100644 index 000000000..372ee07c4 --- /dev/null +++ b/src/UI/Cells/FileTitleCell.js @@ -0,0 +1,15 @@ +var NzbDroneCell = require('./NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'file-title-cell', + + render : function() { + this.$el.empty(); + + var title = this.model.get('relativePath'); + this.$el.html(title); + + + return this; + } +}); diff --git a/src/UI/Cells/InCinemasCell.js b/src/UI/Cells/InCinemasCell.js new file mode 100644 index 000000000..9f1cb2d57 --- /dev/null +++ b/src/UI/Cells/InCinemasCell.js @@ -0,0 +1,22 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'in-cinemas-cell', + + render : function() { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + this.$el.html(""); + + if (this.model.get("inCinemas")) { + var cinemasDate = new Date(this.model.get("inCinemas")); + var year = cinemasDate.getFullYear(); + var month = monthNames[cinemasDate.getMonth()]; + this.$el.html(month + " " + year); //Hack, but somehow handlebar helper does not work. + } + + return this; + } +}); diff --git a/src/UI/Cells/MediaInfoCell.js b/src/UI/Cells/MediaInfoCell.js new file mode 100644 index 000000000..ed42380a3 --- /dev/null +++ b/src/UI/Cells/MediaInfoCell.js @@ -0,0 +1,23 @@ +var NzbDroneCell = require('./NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'release-title-cell', + + render : function() { + this.$el.empty(); + + var info = this.model.get('mediaInfo'); + if (info) { + var runtime = info.runTime; + if (runtime) { + runtime = runtime.split(".")[0]; + } + var video = "{0} ({1}x{2}) ({3})".format(info.videoCodec, info.width, info.height, runtime); + var audio = "{0} ({1})".format(info.audioFormat, info.audioLanguages); + this.$el.html(video + " " + audio); + } + + + return this; + } +}); diff --git a/src/UI/Cells/MovieActionCell.js b/src/UI/Cells/MovieActionCell.js new file mode 100644 index 000000000..e2d18cdea --- /dev/null +++ b/src/UI/Cells/MovieActionCell.js @@ -0,0 +1,45 @@ +var vent = require('vent'); +var NzbDroneCell = require('./NzbDroneCell'); +var CommandController = require('../Commands/CommandController'); + +module.exports = NzbDroneCell.extend({ + className : 'series-actions-cell', + + ui : { + refresh : '.x-refresh' + }, + + events : { + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' + }, + + render : function() { + this.$el.empty(); + + this.$el.html('<i class="icon-sonarr-refresh x-refresh hidden-xs" title="" data-original-title="Update movie info and scan disk"></i> ' + + '<i class="icon-sonarr-edit x-edit" title="" data-original-title="Edit Movie"></i>'); + + CommandController.bindToCommand({ + element : this.$el.find('.x-refresh'), + command : { + name : 'refreshMovie', + movieId : this.model.get('id') + } + }); + + this.delegateEvents(); + return this; + }, + + _editSeries : function() { + vent.trigger(vent.Commands.EditMovieCommand, { movie : this.model }); + }, + + _refreshSeries : function() { + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', + movieId : this.model.id + }); + } +}); diff --git a/src/UI/Cells/MovieDownloadStatusCell.js b/src/UI/Cells/MovieDownloadStatusCell.js new file mode 100644 index 000000000..7cb154374 --- /dev/null +++ b/src/UI/Cells/MovieDownloadStatusCell.js @@ -0,0 +1,10 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'movie-title-cell', + template : 'Cells/MovieDownloadStatusTemplate', + sortKey : function(model) { + debugger; + return 0; + } +}); diff --git a/src/UI/Cells/MovieDownloadStatusTemplate.hbs b/src/UI/Cells/MovieDownloadStatusTemplate.hbs new file mode 100644 index 000000000..93a88fdf1 --- /dev/null +++ b/src/UI/Cells/MovieDownloadStatusTemplate.hbs @@ -0,0 +1 @@ +<span class="label label-{{DownloadedStatusColor}}" title="{{DownloadedQuality}}">{{DownloadedStatus}}</span> diff --git a/src/UI/Cells/MovieLinksCell.js b/src/UI/Cells/MovieLinksCell.js new file mode 100644 index 000000000..ff8643d1a --- /dev/null +++ b/src/UI/Cells/MovieLinksCell.js @@ -0,0 +1,6 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'movie-links-cell', + template : 'Cells/MovieLinksTemplate' +}); diff --git a/src/UI/Cells/MovieLinksTemplate.hbs b/src/UI/Cells/MovieLinksTemplate.hbs new file mode 100644 index 000000000..01a3f52e2 --- /dev/null +++ b/src/UI/Cells/MovieLinksTemplate.hbs @@ -0,0 +1,11 @@ +<span class="series-info-links"> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a>--> + {{#if website}} + <a href="{{homepage}}" class="label label-info">Homepage</a> + {{/if}} + <a href="{{tmdbUrl}}" class="label label-info">The Movie DB</a> + + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} +</span> diff --git a/src/UI/Cells/MovieStatusCell.js b/src/UI/Cells/MovieStatusCell.js new file mode 100644 index 000000000..668e36d4e --- /dev/null +++ b/src/UI/Cells/MovieStatusCell.js @@ -0,0 +1,46 @@ +var NzbDroneCell = require('./NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'movie-status-cell', + + render : function() { + this.$el.empty(); + var monitored = this.model.get('monitored'); + var status = this.model.get('status'); + var inCinemas = this.model.get("inCinemas"); + var date = new Date(inCinemas); + var timeSince = new Date().getTime() - date.getTime(); + var numOfMonths = timeSince / 1000 / 60 / 60 / 24 / 30; + + if (status === 'released') { + this.$el.html('<i class="icon-sonarr-movie-released grid-icon" title="Released"></i>'); + this._setStatusWeight(3); + } + + if (numOfMonths > 3) { + this.$el.html('<i class="icon-sonarr-movie-released grid-icon" title="Released"></i>');//TODO: Update for PreDB.me + this._setStatusWeight(2); + } + + if (numOfMonths < 3) { + this.$el.html('<i class="icon-sonarr-movie-cinemas grid-icon" title="In Cinemas"></i>'); + this._setStatusWeight(2); + } + + if (status === "announced") { + this.$el.html('<i class="icon-sonarr-movie-announced grid-icon" title="Announced"></i>'); + this._setStatusWeight(1); + } + + // else if (!monitored) { + // this.$el.html('<i class="icon-sonarr-series-unmonitored grid-icon" title="Not Monitored"></i>'); + // this._setStatusWeight(0); + // } + + return this; + }, + + _setStatusWeight : function(weight) { + this.model.set('statusWeight', weight, { silent : true }); + } +}); diff --git a/src/UI/Cells/MovieTitleCell.js b/src/UI/Cells/MovieTitleCell.js new file mode 100644 index 000000000..8158d6300 --- /dev/null +++ b/src/UI/Cells/MovieTitleCell.js @@ -0,0 +1,7 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Cells/SeriesTitleTemplate', + +}); diff --git a/src/UI/Cells/MovieTitleHistoryCell.js b/src/UI/Cells/MovieTitleHistoryCell.js new file mode 100644 index 000000000..174d5d361 --- /dev/null +++ b/src/UI/Cells/MovieTitleHistoryCell.js @@ -0,0 +1,14 @@ +var TemplatedCell = require('./TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'Cells/SeriesTitleTemplate', + + + render : function() { + this.$el.html(this.model.get("movie").get("title")); //Hack, but somehow handlebar helper does not work. + debugger; + return this; + + } +}); diff --git a/src/UI/Cells/QualityCell.js b/src/UI/Cells/QualityCell.js index 962bd2ab4..947eb4e91 100644 --- a/src/UI/Cells/QualityCell.js +++ b/src/UI/Cells/QualityCell.js @@ -4,5 +4,7 @@ var QualityCellEditor = require('./Edit/QualityCellEditor'); module.exports = TemplatedCell.extend({ className : 'quality-cell', template : 'Cells/QualityCellTemplate', - editor : QualityCellEditor -}); \ No newline at end of file + editor : QualityCellEditor, + + +}); diff --git a/src/UI/Cells/QualityCellTemplate.hbs b/src/UI/Cells/QualityCellTemplate.hbs index 6625ade9b..9c76376a9 100644 --- a/src/UI/Cells/QualityCellTemplate.hbs +++ b/src/UI/Cells/QualityCellTemplate.hbs @@ -1,5 +1,5 @@ {{#if_gt proper compare="1"}} <span class="badge badge-info" title="PROPER">{{quality.name}}</span> {{else}} - <span class="badge">{{quality.name}}</span> -{{/if_gt}} \ No newline at end of file + <span class="badge" title="{{#if hardcodedSubs}}Warning: {{hardcodedSubs}}{{/if}}">{{quality.name}}</span> +{{/if_gt}} diff --git a/src/UI/Cells/ReleaseTitleCell.js b/src/UI/Cells/ReleaseTitleCell.js index 7d3551e41..761c642ff 100644 --- a/src/UI/Cells/ReleaseTitleCell.js +++ b/src/UI/Cells/ReleaseTitleCell.js @@ -17,4 +17,4 @@ module.exports = NzbDroneCell.extend({ return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Cells/TemplatedCell.js b/src/UI/Cells/TemplatedCell.js index 1299d4e36..eaf8d348e 100644 --- a/src/UI/Cells/TemplatedCell.js +++ b/src/UI/Cells/TemplatedCell.js @@ -18,4 +18,4 @@ module.exports = NzbDroneCell.extend({ this.delegateEvents(); return this; } -}); \ No newline at end of file +}); diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js index 2232d45ae..929870107 100644 --- a/src/UI/Commands/CommandController.js +++ b/src/UI/Commands/CommandController.js @@ -86,7 +86,7 @@ var singleton = function() { } } }); - + console.warn(options) options.element.startSpin(); } }; diff --git a/src/UI/Content/Images/background/logo.png b/src/UI/Content/Images/background/logo.png index 5b3a8e515..a2c98be2b 100644 Binary files a/src/UI/Content/Images/background/logo.png and b/src/UI/Content/Images/background/logo.png differ diff --git a/src/UI/Content/Images/favicon-debug.ico b/src/UI/Content/Images/favicon-debug.ico index db19f38e4..90bf72090 100644 Binary files a/src/UI/Content/Images/favicon-debug.ico and b/src/UI/Content/Images/favicon-debug.ico differ diff --git a/src/UI/Content/Images/favicon.ico b/src/UI/Content/Images/favicon.ico index 1922557d6..90bf72090 100644 Binary files a/src/UI/Content/Images/favicon.ico and b/src/UI/Content/Images/favicon.ico differ diff --git a/src/UI/Content/Images/logos/128.png b/src/UI/Content/Images/logos/128.png index 2309be500..5e143b52e 100644 Binary files a/src/UI/Content/Images/logos/128.png and b/src/UI/Content/Images/logos/128.png differ diff --git a/src/UI/Content/Images/logos/32.png b/src/UI/Content/Images/logos/32.png index be10f9551..f1fe93db5 100644 Binary files a/src/UI/Content/Images/logos/32.png and b/src/UI/Content/Images/logos/32.png differ diff --git a/src/UI/Content/Images/logos/48.png b/src/UI/Content/Images/logos/48.png index e425a1e4f..8b9d0fc88 100644 Binary files a/src/UI/Content/Images/logos/48.png and b/src/UI/Content/Images/logos/48.png differ diff --git a/src/UI/Content/Images/logos/64.png b/src/UI/Content/Images/logos/64.png index 74997cb6e..80edc7894 100644 Binary files a/src/UI/Content/Images/logos/64.png and b/src/UI/Content/Images/logos/64.png differ diff --git a/src/UI/Content/Images/poster-dark.png b/src/UI/Content/Images/poster-dark.png index 63509de2b..79d777533 100644 Binary files a/src/UI/Content/Images/poster-dark.png and b/src/UI/Content/Images/poster-dark.png differ diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index cce09293a..e06a481c0 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -121,6 +121,14 @@ .fa-icon-color(@brand-danger); } +.icon-sonarr-form-cut { + .fa-icon-content(@fa-var-scissors); +} + +.icon-sonarr-form-special { + .fa-icon-content(@fa-var-exclamation-circle); +} + .icon-sonarr-form-info-link { .clickable(); .fa-icon-content(@fa-var-info-circle); @@ -199,6 +207,18 @@ .fa-icon-content(@fa-var-bookmark-o); } +.icon-sonarr-movie-announced { + .fa-icon-content(@fa-var-bullhorn); +} + +.icon-sonarr-movie-released { + .fa-icon-content(@fa-var-file-video-o); +} + +.icon-sonarr-movie-cinemas { + .fa-icon-content(@fa-var-ticket); +} + .icon-sonarr-log-info { .fa-icon-content(@fa-var-info-circle); .fa-icon-color(dodgerblue); @@ -502,4 +522,4 @@ .icon-sonarr-header-rejections { .fa-icon-content(@fa-var-exclamation-circle); -} \ No newline at end of file +} diff --git a/src/UI/Controller.js b/src/UI/Controller.js index f1e4032ab..89775a3ba 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -4,19 +4,26 @@ var Marionette = require('marionette'); var ActivityLayout = require('./Activity/ActivityLayout'); var SettingsLayout = require('./Settings/SettingsLayout'); var AddSeriesLayout = require('./AddSeries/AddSeriesLayout'); +var AddMoviesLayout = require('./AddMovies/AddMoviesLayout'); var WantedLayout = require('./Wanted/WantedLayout'); var CalendarLayout = require('./Calendar/CalendarLayout'); var ReleaseLayout = require('./Release/ReleaseLayout'); var SystemLayout = require('./System/SystemLayout'); var SeasonPassLayout = require('./SeasonPass/SeasonPassLayout'); var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout'); +var MovieEditorLayout = require('./Movies/Editor/MovieEditorLayout'); module.exports = NzbDroneController.extend({ addSeries : function(action) { - this.setTitle('Add Series'); + this.setTitle('Add Movie'); this.showMainRegion(new AddSeriesLayout({ action : action })); }, + addMovies : function(action) { + this.setTitle("Add Movie"); + this.showMainRegion(new AddMoviesLayout({action : action})); + }, + calendar : function() { this.setTitle('Calendar'); this.showMainRegion(new CalendarLayout()); @@ -55,5 +62,10 @@ module.exports = NzbDroneController.extend({ seriesEditor : function() { this.setTitle('Series Editor'); this.showMainRegion(new SeriesEditorLayout()); + }, + + movieEditor : function() { + this.setTitle('Movie Editor'); + this.showMainRegion(new MovieEditorLayout()); } -}); \ No newline at end of file +}); diff --git a/src/UI/Episode/EpisodeDetailsLayout.js b/src/UI/Episode/EpisodeDetailsLayout.js index ba1631d0e..8bdf13525 100644 --- a/src/UI/Episode/EpisodeDetailsLayout.js +++ b/src/UI/Episode/EpisodeDetailsLayout.js @@ -127,4 +127,4 @@ module.exports = Marionette.Layout.extend({ this.ui.monitored.removeClass('icon-sonarr-monitored'); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Episode/Search/ManualLayout.js b/src/UI/Episode/Search/ManualLayout.js index 58c792063..3b7463e19 100644 --- a/src/UI/Episode/Search/ManualLayout.js +++ b/src/UI/Episode/Search/ManualLayout.js @@ -8,6 +8,7 @@ var DownloadReportCell = require('../../Release/DownloadReportCell'); var AgeCell = require('../../Release/AgeCell'); var ProtocolCell = require('../../Release/ProtocolCell'); var PeersCell = require('../../Release/PeersCell'); +var EditionCell = require('../../Cells/EditionCell'); module.exports = Marionette.Layout.extend({ template : 'Episode/Search/ManualLayoutTemplate', @@ -32,6 +33,12 @@ module.exports = Marionette.Layout.extend({ label : 'Title', cell : ReleaseTitleCell }, + { + name : 'edition', + label : 'Edition', + cell : EditionCell, + title : "Edition" + }, { name : 'indexer', label : 'Indexer', @@ -83,4 +90,4 @@ module.exports = Marionette.Layout.extend({ })); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 2c8a96bed..5f5cce2ed 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -11,7 +11,7 @@ Handlebars.registerHelper('poster', function() { if (!poster[0].url.match(/^https?:\/\//)) { return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); } else { - var url = poster[0].url.replace(/^https?\:/, ''); + var url = poster[0].url.replace(/^https?\:/, 'https://'); //IMDb posters need https to work, k? return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); } } @@ -19,6 +19,22 @@ Handlebars.registerHelper('poster', function() { return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder)); }); +Handlebars.registerHelper('remotePoster', function() { + var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; + var poster = this.remotePoster; + + if (poster) { + if (!poster.match(/^https?:\/\//)) { + return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster, 250))); + } else { + var url = poster.replace(/^https?\:/, 'https://'); //IMDb posters need https to work, k? + return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); + } + } + + return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder)); +}); + Handlebars.registerHelper('traktUrl', function() { return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; }); @@ -28,7 +44,150 @@ Handlebars.registerHelper('imdbUrl', function() { }); Handlebars.registerHelper('tvdbUrl', function() { - return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId; + return 'http://imdb.com/title/tt' + this.imdbId; +}); + +Handlebars.registerHelper('tmdbUrl', function() { + return 'https://www.themoviedb.org/movie/' + this.tmdbId; +}); + +Handlebars.registerHelper('homepage', function() { + return this.website; +}); + +Handlebars.registerHelper('alternativeTitlesString', function() { + var titles = this.alternativeTitles; + if (titles.length == 0) { + return ""; + } + if (titles.length == 1) { + return titles[0]; + } + return titles.slice(0,titles.length-1).join(", ") + " and " + titles[titles.length-1]; +}); + +Handlebars.registerHelper('GetStatus', function() { + var monitored = this.monitored; + var status = this.status; + var inCinemas = this.inCinemas; + var date = new Date(inCinemas); + var timeSince = new Date().getTime() - date.getTime(); + var numOfMonths = timeSince / 1000 / 60 / 60 / 24 / 30; + + + if (status === "announced") { + return new Handlebars.SafeString('<i class="icon-sonarr-movie-announced grid-icon" title=""></i> Announced'); + } + + if (numOfMonths < 3) { + + return new Handlebars.SafeString('<i class="icon-sonarr-movie-cinemas grid-icon" title=""></i> In Cinemas'); + } + + if (numOfMonths > 3) { + return new Handlebars.SafeString('<i class="icon-sonarr-movie-released grid-icon" title=""></i> Released');//TODO: Update for PreDB.me + } + + if (status === 'released') { + return new Handlebars.SafeString('<i class="icon-sonarr-movie-released grid-icon" title=""></i> Released'); + } + + else if (!monitored) { + return new Handlebars.SafeString('<i class="icon-sonarr-series-unmonitored grid-icon" title=""></i> Not Monitored'); + } +}) + +Handlebars.registerHelper('GetBannerStatus', function() { + var monitored = this.monitored; + var status = this.status; + var inCinemas = this.inCinemas; + var date = new Date(inCinemas); + var timeSince = new Date().getTime() - date.getTime(); + var numOfMonths = timeSince / 1000 / 60 / 60 / 24 / 30; + + if (status === "announced") { + return new Handlebars.SafeString('<div class="announced-banner"><i class="icon-sonarr-movie-announced grid-icon" title=""></i> Announced</div>'); + } + + if (numOfMonths < 3) { + return new Handlebars.SafeString('<div class="cinemas-banner"><i class="icon-sonarr-movie-cinemas grid-icon" title=""></i> In Cinemas</div>'); + } + + if (status === 'released') { + return new Handlebars.SafeString('<div class="released-banner"><i class="icon-sonarr-movie-released grid-icon" title=""></i> Released</div>'); + } + + if (numOfMonths > 3) { + return new Handlebars.SafeString('<div class="released-banner"><i class="icon-sonarr-movie-released grid-icon" title=""></i> Released</div>');//TODO: Update for PreDB.me + } + + + + + else if (!monitored) { + return new Handlebars.SafeString('<div class="announced-banner"><i class="icon-sonarr-series-unmonitored grid-icon" title=""></i> Not Monitored</div>'); + } +}); + +Handlebars.registerHelper('DownloadedStatusColor', function() { + if (!this.monitored) { + if (this.downloaded) { + return "default"; + } + return "warning"; + } + + if (this.downloaded) { + return "success"; + } + + if (this.status != "released") { + return "primary"; + } + + return "danger"; +}) + +Handlebars.registerHelper('DownloadedStatus', function() { + + if (this.downloaded) { + return "Downloaded"; + } + if (!this.monitored) { + return "Not Monitored"; + } + + + return "Missing"; +}); + +Handlebars.registerHelper("DownloadedQuality", function() { + if (this.movieFile) { + return this.movieFile.quality.quality.name; + } + + return ""; +}) + + +Handlebars.registerHelper('inCinemas', function() { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" +]; + if (this.physicalRelease) { + var d = new Date(this.physicalRelease); + var day = d.getDate(); + var month = monthNames[d.getMonth()]; + var year = d.getFullYear(); + return "Available: " + day + ". " + month + " " + year; + } + if (this.inCinemas) { + var cinemasDate = new Date(this.inCinemas); + var year = cinemasDate.getFullYear(); + var month = monthNames[cinemasDate.getMonth()]; + return "In Cinemas: " + month + " " + year; + } + return "To be announced"; }); Handlebars.registerHelper('tvRageUrl', function() { @@ -40,7 +199,7 @@ Handlebars.registerHelper('tvMazeUrl', function() { }); Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/series/' + this.titleSlug; + return StatusModel.get('urlBase') + '/movies/' + this.titleSlug; }); Handlebars.registerHelper('percentOfEpisodes', function() { diff --git a/src/UI/ManualImport/Cells/MovieCell.js b/src/UI/ManualImport/Cells/MovieCell.js new file mode 100644 index 000000000..8849f8ea3 --- /dev/null +++ b/src/UI/ManualImport/Cells/MovieCell.js @@ -0,0 +1,43 @@ +var vent = require('../../vent'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SelectMovieLayout = require('../Movie/SelectMovieLayout'); + +module.exports = NzbDroneCell.extend({ + className : 'series-title-cell editable', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + + var movie = this.model.get('movie'); + + if (movie) + { + this.$el.html(movie.title + " (" + movie.year + ")" ); + } + + this.delegateEvents(); + return this; + }, + + _onClick : function () { + var view = new SelectMovieLayout(); + + this.listenTo(view, 'manualimport:selected:movie', this._setMovie); + + vent.trigger(vent.Commands.OpenModal2Command, view); + }, + + _setMovie : function (e) { + if (this.model.has('movie') && e.model.id === this.model.get('movie').id) { + return; + } + + this.model.set({ + movie : e.model.toJSON() + }); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayout.js b/src/UI/ManualImport/ManualImportLayout.js index ba5a139fc..97c20e75d 100644 --- a/src/UI/ManualImport/ManualImportLayout.js +++ b/src/UI/ManualImport/ManualImportLayout.js @@ -16,6 +16,7 @@ var QualityCell = require('./Cells/QualityCell'); var FileSizeCell = require('../Cells/FileSizeCell'); var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); var ManualImportCollection = require('./ManualImportCollection'); +var MovieCell = require('./Cells/MovieCell'); var Messenger = require('../Shared/Messenger'); module.exports = Marionette.Layout.extend({ @@ -49,23 +50,29 @@ module.exports = Marionette.Layout.extend({ sortable : true }, { - name : 'series', - label : 'Series', - cell : SeriesCell, + name : 'movie', + label : 'Movie', + cell : MovieCell, sortable : true }, - { - name : 'seasonNumber', - label : 'Season', - cell : SeasonCell, - sortable : true - }, - { - name : 'episodes', - label : 'Episode(s)', - cell : EpisodesCell, - sortable : false - }, + // { + // name : 'series', + // label : 'Series', + // cell : SeriesCell, + // sortable : true + // }, + // { + // name : 'seasonNumber', + // label : 'Season', + // cell : SeasonCell, + // sortable : true + // }, + // { + // name : 'episodes', + // label : 'Episode(s)', + // cell : EpisodesCell, + // sortable : false + // }, { name : 'quality', label : 'Quality', @@ -161,8 +168,8 @@ module.exports = Marionette.Layout.extend({ }, _automaticImport : function (e) { - CommandController.Execute('downloadedEpisodesScan', { - name : 'downloadedEpisodesScan', + CommandController.Execute('downloadedMovieScan', { + name : 'downloadedMovieScan', path : e.folder }); @@ -176,29 +183,36 @@ module.exports = Marionette.Layout.extend({ return; } - if (_.any(selected, function (model) { - return !model.has('series'); - })) { - - this._showErrorMessage('Series must be chosen for each selected file'); + if(_.any(selected, function(model) { + return !model.has('movie'); + })) { + this._showErrorMessage('Movie must be chosen for each selected file'); return; } - if (_.any(selected, function (model) { - return !model.has('seasonNumber'); - })) { + // if (_.any(selected, function (model) { + // return !model.has('series'); + // })) { - this._showErrorMessage('Season must be chosen for each selected file'); - return; - } + // this._showErrorMessage('Series must be chosen for each selected file'); + // return; + // } - if (_.any(selected, function (model) { - return !model.has('episodes') || model.get('episodes').length === 0; - })) { + // if (_.any(selected, function (model) { + // return !model.has('seasonNumber'); + // })) { - this._showErrorMessage('One or more episodes must be chosen for each selected file'); - return; - } + // this._showErrorMessage('Season must be chosen for each selected file'); + // return; + // } + + // if (_.any(selected, function (model) { + // return !model.has('episodes') || model.get('episodes').length === 0; + // })) { + + // this._showErrorMessage('One or more episodes must be chosen for each selected file'); + // return; + // } var importMode = this.ui.importMode.val(); @@ -207,8 +221,9 @@ module.exports = Marionette.Layout.extend({ files : _.map(selected, function (file) { return { path : file.get('path'), - seriesId : file.get('series').id, - episodeIds : _.map(file.get('episodes'), 'id'), + movieId : file.get('movie').id, + // seriesId : file.get('series').id, + // episodeIds : _.map(file.get('episodes'), 'id'), quality : file.get('quality'), downloadId : file.get('downloadId') }; diff --git a/src/UI/ManualImport/Movie/SelectMovieLayout.js b/src/UI/ManualImport/Movie/SelectMovieLayout.js new file mode 100644 index 000000000..ecde63b54 --- /dev/null +++ b/src/UI/ManualImport/Movie/SelectMovieLayout.js @@ -0,0 +1,101 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var MoviesCollection = require('../../Movies/MoviesCollection'); +var SelectRow = require('./SelectMovieRow'); + +module.exports = Marionette.Layout.extend({ + template : 'ManualImport/Movie/SelectMovieLayoutTemplate', + + regions : { + movie : '.x-movie' + }, + + ui : { + filter : '.x-filter' + }, + + columns : [ + { + name : 'title', + label : 'Title', + cell : 'String', + sortValue : 'title' + } + ], + + initialize : function() { + this.movieCollection = MoviesCollection.clone(); + this._setModelCollection(); + + this.listenTo(this.movieCollection, 'row:selected', this._onSelected); + this.listenTo(this, 'modal:afterShow', this._setFocus); + }, + + onRender : function() { + this.movieView = new Backgrid.Grid({ + columns : this.columns, + collection : this.movieCollection, + className : 'table table-hover season-grid', + row : SelectRow + }); + + this.movie.show(this.movieView); + this._setupFilter(); + }, + + _setupFilter : function () { + var self = this; + + //TODO: This should be a mixin (same as Add Series searching) + this.ui.filter.keyup(function(e) { + if (_.contains([ + 9, + 16, + 17, + 18, + 19, + 20, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 91, + 92, + 93 + ], e.keyCode)) { + return; + } + + self._filter(self.ui.filter.val()); + }); + }, + + _filter : function (term) { + this.movieCollection.setFilter(['title', term, 'contains']); + this._setModelCollection(); + }, + + _onSelected : function (e) { + this.trigger('manualimport:selected:movie', { model: e.model }); + + vent.trigger(vent.Commands.CloseModal2Command); + }, + + _setFocus : function () { + this.ui.filter.focus(); + }, + + _setModelCollection: function () { + var self = this; + + _.each(this.movieCollection.models, function (model) { + model.collection = self.movieCollection; + }); + } +}); diff --git a/src/UI/ManualImport/Movie/SelectMovieLayoutTemplate.hbs b/src/UI/ManualImport/Movie/SelectMovieLayoutTemplate.hbs new file mode 100644 index 000000000..25b3c39d4 --- /dev/null +++ b/src/UI/ManualImport/Movie/SelectMovieLayoutTemplate.hbs @@ -0,0 +1,30 @@ +<div class="modal-content"> + <div class="manual-import-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + + <h3> + Manual Import - Select Movie + </h3> + + </div> + <div class="modal-body"> + <div class="row"> + <div class="col-md-12"> + <div class="form-group"> + <input type="text" class="form-control x-filter" placeholder="Filter movies" /> + </div> + </div> + </div> + + <div class="row"> + <div class="col-md-12 x-movie"></div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-default" data-dismiss="modal">Cancel</button> + </div> + </div> +</div> + + diff --git a/src/UI/ManualImport/Movie/SelectMovieRow.js b/src/UI/ManualImport/Movie/SelectMovieRow.js new file mode 100644 index 000000000..38a2d5ca6 --- /dev/null +++ b/src/UI/ManualImport/Movie/SelectMovieRow.js @@ -0,0 +1,13 @@ +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Row.extend({ + className : 'select-row select-series-row', + + events : { + 'click' : '_onClick' + }, + + _onClick : function() { + this.model.collection.trigger('row:selected', { model: this.model }); + } +}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryView.js b/src/UI/ManualImport/Summary/ManualImportSummaryView.js index a4ab847c2..141f2ca26 100644 --- a/src/UI/ManualImport/Summary/ManualImportSummaryView.js +++ b/src/UI/ManualImport/Summary/ManualImportSummaryView.js @@ -4,16 +4,25 @@ var Marionette = require('marionette'); module.exports = Marionette.ItemView.extend({ template : 'ManualImport/Summary/ManualImportSummaryViewTemplate', + // initialize : function (options) { + // var episodes = _.map(options.episodes, function (episode) { + // return episode.toJSON(); + // }); + + // this.templateHelpers = { + // file : options.file, + // series : options.series, + // season : options.season, + // episodes : episodes, + // quality : options.quality + // }; + // } + initialize : function (options) { - var episodes = _.map(options.episodes, function (episode) { - return episode.toJSON(); - }); this.templateHelpers = { file : options.file, - series : options.series, - season : options.season, - episodes : episodes, + movie : options.movie, quality : options.quality }; } diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs index d65ff52f1..36497083e 100644 --- a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs +++ b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs @@ -3,16 +3,8 @@ <dt>Path:</dt> <dd>{{file}}</dd> - <dt>Series:</dt> - <dd>{{series.title}}</dd> - - <dt>Season:</dt> - <dd>{{season.seasonNumber}}</dd> - - {{#each episodes}} - <dt>Episode:</dt> - <dd>{{episodeNumber}} - {{title}}</dd> - {{/each}} + <dt>Movie:</dt> + <dd>{{movie.title}} ({{movie.year}})</dd> <dt>Quality:</dt> <dd>{{quality.name}}</dd> diff --git a/src/UI/Movies/Details/EpisodeNumberCell.js b/src/UI/Movies/Details/EpisodeNumberCell.js new file mode 100644 index 000000000..4e09a7603 --- /dev/null +++ b/src/UI/Movies/Details/EpisodeNumberCell.js @@ -0,0 +1,47 @@ +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var reqres = require('../../reqres'); +var SeriesCollection = require('../SeriesCollection'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-number-cell', + template : 'Movies/Details/EpisodeNumberCellTemplate', + + render : function() { + this.$el.empty(); + this.$el.html(this.model.get('episodeNumber')); + + var series = SeriesCollection.get(this.model.get('seriesId')); + + if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) { + this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber'))); + } + + var alternateTitles = []; + + if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { + alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); + } + + if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { + this.templateFunction = Marionette.TemplateCache.get(this.template); + + var json = this.model.toJSON(); + json.alternateTitles = alternateTitles; + + var html = this.templateFunction(json); + + this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : 'Scene Information', + placement : 'right', + container : this.$el + }); + } + + this.delegateEvents(); + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs b/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs new file mode 100644 index 000000000..a9028a423 --- /dev/null +++ b/src/UI/Movies/Details/EpisodeNumberCellTemplate.hbs @@ -0,0 +1,39 @@ +<div class="scene-info"> + {{#if sceneSeasonNumber}} + <div class="row"> + <div class="key">Season</div> + <div class="value">{{sceneSeasonNumber}}</div> + </div> + {{/if}} + + {{#if sceneEpisodeNumber}} + <div class="row"> + <div class="key">Episode</div> + <div class="value">{{sceneEpisodeNumber}}</div> + </div> + {{/if}} + + {{#if sceneAbsoluteEpisodeNumber}} + <div class="row"> + <div class="key">Absolute</div> + <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> + </div> + {{/if}} + + {{#if alternateTitles}} + <div class="row"> + {{#if_gt alternateTitles.length compare="1"}} + <div class="key">Titles</div> + {{else}} + <div class="key">Title</div> + {{/if_gt}} + <div class="value"> + <ul> + {{#each alternateTitles}} + <li>{{title}}</li> + {{/each}} + </ul> + </div> + </div> + {{/if}} +</div> \ No newline at end of file diff --git a/src/UI/Movies/Details/EpisodeWarningCell.js b/src/UI/Movies/Details/EpisodeWarningCell.js new file mode 100644 index 000000000..c9befe7a1 --- /dev/null +++ b/src/UI/Movies/Details/EpisodeWarningCell.js @@ -0,0 +1,21 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var SeriesCollection = require('../SeriesCollection'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-warning-cell', + + render : function() { + this.$el.empty(); + + if (this.model.get('unverifiedSceneNumbering')) { + this.$el.html('<i class="icon-sonarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); + } + + else if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { + this.$el.html('<i class="icon-sonarr-form-warning" title="Episode does not have an absolute episode number"></i>'); + } + + this.delegateEvents(); + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/InfoView.js b/src/UI/Movies/Details/InfoView.js new file mode 100644 index 000000000..4a2f71b29 --- /dev/null +++ b/src/UI/Movies/Details/InfoView.js @@ -0,0 +1,18 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Details/InfoViewTemplate', + + initialize : function(options) { + //this.episodeFileCollection = options.episodeFileCollection; + + this.listenTo(this.model, 'change', this.render); + //this.listenTo(this.episodeFileCollection, 'sync', this.render); TODO: Update this; + }, + + templateHelpers : function() { + return { + fileCount : 0 + }; + } +}); diff --git a/src/UI/Movies/Details/InfoViewTemplate.hbs b/src/UI/Movies/Details/InfoViewTemplate.hbs new file mode 100644 index 000000000..c62818297 --- /dev/null +++ b/src/UI/Movies/Details/InfoViewTemplate.hbs @@ -0,0 +1,56 @@ +<div class="row"> + <div class="col-md-8"> + {{profile profileId}} + + {{#if network}} + <span class="label label-info">{{network}}</span> + {{/if}} + + <span class="label label-info">{{runtime}} minutes</span> + <span class="label label-info">{{path}}</span> + + {{#if ratings}} + <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> + {{/if}} + + <span class="label label-info">{{Bytes sizeOnDisk}}</span> + + {{#if_eq status compare="released"}} + <span class="label label-info">{{inCinemas}}</span> + {{else}} + <span class="label label-default">Announced</span> + {{/if_eq}} + <span class="label label-{{DownloadedStatusColor}}" title="{{DownloadedQuality}}">{{DownloadedStatus}}</span> + </div> + <div class="col-md-4"> + <span class="series-info-links"> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a>--> + {{#if website}} + <a href="{{homepage}}" class="label label-info">Homepage</a> + {{/if}} + <a href="{{tmdbUrl}}" class="label label-info">The Movie DB</a> + + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} + </span> + </div> +</div> + +{{#if alternativeTitles}} +<div class="row"> + <div class="col-md-12"> + <span class="alternative-titles"> + Also known as: {{alternativeTitlesString}}. + </span> + </div> +</div> +{{/if}} + +{{#if tags}} +<div class="row"> + <div class="col-md-12"> + {{tagDisplay tags}} + </div> +</div> +{{/if}} diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js new file mode 100644 index 000000000..1d01f8439 --- /dev/null +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -0,0 +1,317 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var vent = require('vent'); +var reqres = require('../../reqres'); +var Marionette = require('marionette'); +var Backbone = require('backbone'); +var MoviesCollection = require('../MoviesCollection'); +var InfoView = require('./InfoView'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); +var HistoryLayout = require('../History/MovieHistoryLayout'); +var SearchLayout = require('../Search/MovieSearchLayout'); +var FilesLayout = require("../Files/FilesLayout"); +require('backstrech'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + itemViewContainer : '.x-movie-seasons', + template : 'Movies/Details/MoviesDetailsTemplate', + + regions : { + seasons : '#seasons', + info : '#info', + search : '#movie-search', + history : '#movie-history', + files : "#movie-files" + }, + + + ui : { + header : '.x-header', + monitored : '.x-monitored', + edit : '.x-edit', + refresh : '.x-refresh', + rename : '.x-rename', + searchAuto : '.x-search', + poster : '.x-movie-poster', + manualSearch : '.x-manual-search', + history : '.x-movie-history', + search : '.x-movie-search', + files : ".x-movie-files" + }, + + events : { + 'click .x-episode-file-editor' : '_showFiles', + 'click .x-monitored' : '_toggleMonitored', + 'click .x-edit' : '_editMovie', + 'click .x-refresh' : '_refreshMovies', + 'click .x-rename' : '_renameMovies', + 'click .x-search' : '_moviesSearch', + 'click .x-manual-search' : '_showSearch', + 'click .x-movie-history' : '_showHistory', + 'click .x-movie-search' : '_showSearch', + "click .x-movie-files" : "_showFiles", + }, + + initialize : function() { + this.moviesCollection = MoviesCollection.clone(); + this.moviesCollection.shadowCollection.bindSignalR(); + + this.listenTo(this.model, 'change:monitored', this._setMonitoredState); + this.listenTo(this.model, 'remove', this._moviesRemoved); + this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); + + this.listenTo(this.model, 'change', function(model, options) { + if (options && options.changeSource === 'signalr') { + this._refresh(); + } + }); + + this.listenTo(this.model, 'change:images', this._updateImages); + }, + + onShow : function() { + this.searchLayout = new SearchLayout({ model : this.model }); + this.searchLayout.startManualSearch = true; + + this.filesLayout = new FilesLayout({ model : this.model }); + + this._showBackdrop(); + this._showSeasons(); + this._setMonitoredState(); + this._showInfo(); + if (this.model.get("movieFile")) { + this._showFiles() + } else { + this._showHistory(); + } + + }, + + onRender : function() { + CommandController.bindToCommand({ + element : this.ui.refresh, + command : { + name : 'refreshMovie' + } + }); + + CommandController.bindToCommand({ + element : this.ui.searchAuto, + command : { + name : 'moviesSearch' + } + }); + + CommandController.bindToCommand({ + element : this.ui.rename, + command : { + name : 'renameMovieFiles', + movieId : this.model.id, + seasonNumber : -1 + } + }); + }, + + onClose : function() { + if (this._backstrech) { + this._backstrech.destroy(); + delete this._backstrech; + } + + $('body').removeClass('backdrop'); + reqres.removeHandler(reqres.Requests.GetEpisodeFileById); + }, + + _getImage : function(type) { + var image = _.where(this.model.get('images'), { coverType : type }); + + if (image && image[0]) { + return image[0].url; + } + + return undefined; + }, + + _showHistory : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.history.tab('show'); + this.history.show(new HistoryLayout({ + model : this.model + })); + }, + + _showSearch : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.search.tab('show'); + this.search.show(this.searchLayout); + }, + + _showFiles : function(e) { + if (e) { + e.preventDefault(); + } + + this.ui.files.tab('show'); + this.files.show(this.filesLayout); + }, + + _toggleMonitored : function() { + var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); + + this.ui.monitored.spinForPromise(savePromise); + }, + + _setMonitoredState : function() { + var monitored = this.model.get('monitored'); + + this.ui.monitored.removeAttr('data-idle-icon'); + this.ui.monitored.removeClass('fa-spin icon-sonarr-spinner'); + + if (monitored) { + this.ui.monitored.addClass('icon-sonarr-monitored'); + this.ui.monitored.removeClass('icon-sonarr-unmonitored'); + this.$el.removeClass('movie-not-monitored'); + } else { + this.ui.monitored.addClass('icon-sonarr-unmonitored'); + this.ui.monitored.removeClass('icon-sonarr-monitored'); + this.$el.addClass('movie-not-monitored'); + } + }, + + _editMovie : function() { + vent.trigger(vent.Commands.EditMovieCommand, { movie : this.model }); + }, + + _refreshMovies : function() { + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', + movieId : this.model.id + }); + }, + + _moviesRemoved : function() { + Backbone.history.navigate('/', { trigger : true }); + }, + + _renameMovies : function() { + vent.trigger(vent.Commands.ShowRenamePreview, { movie : this.model }); + }, + + _moviesSearch : function() { + CommandController.Execute('moviesSearch', { + name : 'moviesSearch', + movieId : this.model.id + }); + }, + + _showSeasons : function() { + var self = this; + + return; + + reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { + return self.episodeFileCollection.get(episodeFileId); + }); + + reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(moviesId, seasonNumber, sceneSeasonNumber) { + if (self.model.get('id') !== moviesId) { + return []; + } + + if (sceneSeasonNumber === undefined) { + sceneSeasonNumber = seasonNumber; + } + + return _.where(self.model.get('alternateTitles'), + function(alt) { + return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; + }); + }); + + $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { + var seasonCollectionView = new SeasonCollectionView({ + collection : self.seasonCollection, + episodeCollection : self.episodeCollection, + movies : self.model + }); + + if (!self.isClosed) { + self.seasons.show(seasonCollectionView); + } + }); + }, + + _showInfo : function() { + this.info.show(new InfoView({ + model : this.model + })); + }, + + _commandComplete : function(options) { + if (options.command.get('name') === 'renameMoviefiles') { + if (options.command.get('moviesId') === this.model.get('id')) { + this._refresh(); + } + } + }, + + _refresh : function() { + //this.seasonCollection.add(this.model.get('seasons'), { merge : true }); + //this.episodeCollection.fetch(); + //this.episodeFileCollection.fetch(); + + this._setMonitoredState(); + this._showInfo(); + }, + + _openEpisodeFileEditor : function() { + var view = new EpisodeFileEditorLayout({ + movies : this.model, + episodeCollection : this.episodeCollection + }); + + vent.trigger(vent.Commands.OpenModalCommand, view); + }, + + _updateImages : function () { + var poster = this._getImage('poster'); + + if (poster) { + this.ui.poster.attr('src', poster); + } + + this._showBackdrop(); + }, + + _showBackdrop : function () { + $('body').addClass('backdrop'); + var fanArt = this._getImage('banner'); + + if (fanArt) { + this._backstrech = $.backstretch(fanArt); + } else { + $('body').removeClass('backdrop'); + } + }, + + _manualSearchM : function() { + console.warn("Manual Search started"); + console.warn(this.model.id); + console.warn(this.model) + console.warn(this.episodeCollection); + vent.trigger(vent.Commands.ShowEpisodeDetails, { + episode : this.model, + hideMoviesLink : true, + openingTab : 'search' + }); + } +}); diff --git a/src/UI/Movies/Details/MoviesDetailsTemplate.hbs b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs new file mode 100644 index 000000000..8e5bb874e --- /dev/null +++ b/src/UI/Movies/Details/MoviesDetailsTemplate.hbs @@ -0,0 +1,55 @@ +<div class="row movie-page-header"> + <div class="visible-lg col-lg-2 poster"> + {{poster}} + </div> + <div class="col-md-12 col-lg-10"> + <div> + <h1 class="header-text"> + <i class="x-monitored" title="Toggle monitored state for movie"/> + {{title}} + <div class="movie-actions pull-right"> + <div class="x-episode-file-editor"> + <i class="icon-sonarr-episode-file" title="Modify movie files"/> + </div> + <div class="x-refresh"> + <i class="icon-sonarr-refresh icon-can-spin" title="Update movie info and scan disk"/> + </div> + <div class="x-rename"> + <i class="icon-sonarr-rename" title="Preview rename for movie"/> + </div> + <div class="x-search"> + <i class="icon-sonarr-search" title="Search for movie"/> + </div> + <div class="x-manual-search"> + <i class="icon-sonarr-search-manual" title="Manual Search"/> + </div> + <div class="x-edit"> + <i class="icon-sonarr-edit" title="Edit movie"/> + </div> + </div> + </h1> + </div> + <div class="movie-detail-overview"> + {{overview}} + </div> + <div id="info" class="movie-info"></div> + </div> +</div> +<div id="movie-info"> + <div class="movie-tabs"> + <div> + <div class="movie-tabs-card"> + <ul class="nav nav-tabs" id="myTab"> + <li><a href="#movie-history" class="x-movie-history">History</a></li> + <li><a href="#movie-search" class="x-movie-search">Search</a></li> + <li><a href="#movie-files" class="x-movie-files">Files</a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane" id="movie-history"/> + <div class="tab-pane" id="movie-search"/> + <div class="tab-pane" id="movie-files"/> + </div> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Details/SeasonCollectionView.js b/src/UI/Movies/Details/SeasonCollectionView.js new file mode 100644 index 000000000..24da6171c --- /dev/null +++ b/src/UI/Movies/Details/SeasonCollectionView.js @@ -0,0 +1,44 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); +var SeasonLayout = require('./SeasonLayout'); +var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); + +var view = Marionette.CollectionView.extend({ + + itemView : SeasonLayout, + + initialize : function(options) { + if (!options.episodeCollection) { + throw 'episodeCollection is needed'; + } + + this.episodeCollection = options.episodeCollection; + this.series = options.series; + }, + + itemViewOptions : function() { + return { + episodeCollection : this.episodeCollection, + series : this.series + }; + }, + + onEpisodeGrabbed : function(message) { + if (message.episode.series.id !== this.episodeCollection.seriesId) { + return; + } + + var self = this; + + _.each(message.episode.episodes, function(episode) { + var ep = self.episodeCollection.get(episode.id); + ep.set('downloading', true); + }); + + this.render(); + } +}); + +AsSortedCollectionView.call(view); + +module.exports = view; \ No newline at end of file diff --git a/src/UI/Movies/Details/SeasonLayout.js b/src/UI/Movies/Details/SeasonLayout.js new file mode 100644 index 000000000..2638a2b70 --- /dev/null +++ b/src/UI/Movies/Details/SeasonLayout.js @@ -0,0 +1,301 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var ToggleCell = require('../../Cells/EpisodeMonitoredCell'); +var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); +var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell'); +var EpisodeNumberCell = require('./EpisodeNumberCell'); +var EpisodeWarningCell = require('./EpisodeWarningCell'); +var CommandController = require('../../Commands/CommandController'); +var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); +var moment = require('moment'); +var _ = require('underscore'); +var Messenger = require('../../Shared/Messenger'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Details/SeasonLayoutTemplate', + + ui : { + seasonSearch : '.x-season-search', + seasonMonitored : '.x-season-monitored', + seasonRename : '.x-season-rename' + }, + + events : { + 'click .x-season-episode-file-editor' : '_openEpisodeFileEditor', + 'click .x-season-monitored' : '_seasonMonitored', + 'click .x-season-search' : '_seasonSearch', + 'click .x-season-rename' : '_seasonRename', + 'click .x-show-hide-episodes' : '_showHideEpisodes', + 'dblclick .series-season h2' : '_showHideEpisodes' + }, + + regions : { + episodeGrid : '.x-episode-grid' + }, + + columns : [ + { + name : 'monitored', + label : '', + cell : ToggleCell, + trueClass : 'icon-sonarr-monitored', + falseClass : 'icon-sonarr-unmonitored', + tooltip : 'Toggle monitored status', + sortable : false + }, + { + name : 'episodeNumber', + label : '#', + cell : EpisodeNumberCell + }, + { + name : 'this', + label : '', + cell : EpisodeWarningCell, + sortable : false, + className : 'episode-warning-cell' + }, + { + name : 'this', + label : 'Title', + hideSeriesLink : true, + cell : EpisodeTitleCell, + sortable : false + }, + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell + }, + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable : false + }, + { + name : 'this', + label : '', + cell : EpisodeActionsCell, + sortable : false + } + ], + + templateHelpers : function() { + var episodeCount = this.episodeCollection.filter(function(episode) { + return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()); + }).length; + + var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length; + var percentOfEpisodes = 100; + + if (episodeCount > 0) { + percentOfEpisodes = episodeFileCount / episodeCount * 100; + } + + return { + showingEpisodes : this.showingEpisodes, + episodeCount : episodeCount, + episodeFileCount : episodeFileCount, + percentOfEpisodes : percentOfEpisodes + }; + }, + + initialize : function(options) { + if (!options.episodeCollection) { + throw 'episodeCollection is required'; + } + + this.series = options.series; + this.fullEpisodeCollection = options.episodeCollection; + this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')); + this._updateEpisodeCollection(); + + this.showingEpisodes = this._shouldShowEpisodes(); + + this.listenTo(this.model, 'sync', this._afterSeasonMonitored); + this.listenTo(this.episodeCollection, 'sync', this.render); + + this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes); + }, + + onRender : function() { + if (this.showingEpisodes) { + this._showEpisodes(); + } + + this._setSeasonMonitoredState(); + + CommandController.bindToCommand({ + element : this.ui.seasonSearch, + command : { + name : 'seasonSearch', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + } + }); + + CommandController.bindToCommand({ + element : this.ui.seasonRename, + command : { + name : 'renameFiles', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + } + }); + }, + + _seasonSearch : function() { + CommandController.Execute('seasonSearch', { + name : 'seasonSearch', + seriesId : this.series.id, + seasonNumber : this.model.get('seasonNumber') + }); + }, + + _seasonRename : function() { + vent.trigger(vent.Commands.ShowRenamePreview, { + series : this.series, + seasonNumber : this.model.get('seasonNumber') + }); + }, + + _seasonMonitored : function() { + if (!this.series.get('monitored')) { + + Messenger.show({ + message : 'Unable to change monitored state when series is not monitored', + type : 'error' + }); + + return; + } + + var name = 'monitored'; + this.model.set(name, !this.model.get(name)); + this.series.setSeasonMonitored(this.model.get('seasonNumber')); + + var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this)); + + this.ui.seasonMonitored.spinForPromise(savePromise); + }, + + _afterSeasonMonitored : function() { + var self = this; + + _.each(this.episodeCollection.models, function(episode) { + episode.set({ monitored : self.model.get('monitored') }); + }); + + this.render(); + }, + + _setSeasonMonitoredState : function() { + this.ui.seasonMonitored.removeClass('icon-sonarr-spinner fa-spin'); + + if (this.model.get('monitored')) { + this.ui.seasonMonitored.addClass('icon-sonarr-monitored'); + this.ui.seasonMonitored.removeClass('icon-sonarr-unmonitored'); + } else { + this.ui.seasonMonitored.addClass('icon-sonarr-unmonitored'); + this.ui.seasonMonitored.removeClass('icon-sonarr-monitored'); + } + }, + + _showEpisodes : function() { + this.episodeGrid.show(new Backgrid.Grid({ + columns : this.columns, + collection : this.episodeCollection, + className : 'table table-hover season-grid' + })); + }, + + _shouldShowEpisodes : function() { + var startDate = moment().add('month', -1); + var endDate = moment().add('year', 1); + + return this.episodeCollection.some(function(episode) { + var airDate = episode.get('airDateUtc'); + + if (airDate) { + var airDateMoment = moment(airDate); + + if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { + return true; + } + } + + return false; + }); + }, + + _showHideEpisodes : function() { + if (this.showingEpisodes) { + this.showingEpisodes = false; + this.episodeGrid.close(); + } else { + this.showingEpisodes = true; + this._showEpisodes(); + } + + this.templateHelpers.showingEpisodes = this.showingEpisodes; + this.render(); + }, + + _episodeMonitoredToggled : function(options) { + var model = options.model; + var shiftKey = options.shiftKey; + + if (!this.episodeCollection.get(model.get('id'))) { + return; + } + + if (!shiftKey) { + return; + } + + var lastToggled = this.episodeCollection.lastToggled; + + if (!lastToggled) { + return; + } + + var currentIndex = this.episodeCollection.indexOf(model); + var lastIndex = this.episodeCollection.indexOf(lastToggled); + + var low = Math.min(currentIndex, lastIndex); + var high = Math.max(currentIndex, lastIndex); + var range = _.range(low + 1, high); + + this.episodeCollection.lastToggled = model; + }, + + _updateEpisodeCollection : function() { + var self = this; + + this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true }); + + this.episodeCollection.each(function(model) { + model.episodeCollection = self.episodeCollection; + }); + }, + + _refreshEpisodes : function() { + this._updateEpisodeCollection(); + this.episodeCollection.fullCollection.sort(); + this.render(); + }, + + _openEpisodeFileEditor : function() { + var view = new EpisodeFileEditorLayout({ + model : this.model, + series : this.series, + episodeCollection : this.episodeCollection + }); + + vent.trigger(vent.Commands.OpenModalCommand, view); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Details/SeasonLayoutTemplate.hbs b/src/UI/Movies/Details/SeasonLayoutTemplate.hbs new file mode 100644 index 000000000..06034f19d --- /dev/null +++ b/src/UI/Movies/Details/SeasonLayoutTemplate.hbs @@ -0,0 +1,50 @@ +<div class="series-season" id="season-{{seasonNumber}}"> + <h2> + <i class="x-season-monitored season-monitored clickable" title="Toggle season monitored status"/> + + {{#if seasonNumber}} + Season {{seasonNumber}} + {{else}} + Specials + {{/if}} + + + {{#if_eq episodeCount compare=0}} + {{#if monitored}} + <span class="badge badge-primary season-status" title="No aired episodes"> </span> + {{else}} + <span class="badge badge-warning season-status" title="Season is not monitored"> </span> + {{/if}} + {{else}} + {{#if_eq percentOfEpisodes compare=100}} + <span class="badge badge-success season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> + {{else}} + <span class="badge badge-danger season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> + {{/if_eq}} + {{/if_eq}} + + <span class="season-actions pull-right"> + <div class="x-season-episode-file-editor"> + <i class="icon-sonarr-episode-file" title="Modify episode files for season"/> + </div> + <div class="x-season-rename"> + <i class="icon-sonarr-rename" title="Preview rename for season {{seasonNumber}}"/> + </div> + <div class="x-season-search"> + <i class="icon-sonarr-search" title="Search for monitored episodes in season {{seasonNumber}}"/> + </div> + </span> + </h2> + <div class="show-hide-episodes x-show-hide-episodes"> + <h4> + {{#if showingEpisodes}} + <i class="icon-sonarr-panel-hide"/> + Hide Episodes + {{else}} + <i class="icon-sonarr-panel-show"/> + Show Episodes + {{/if}} + </h4> + </div> + <div class="x-episode-grid table-responsive"></div> +</div> diff --git a/src/UI/Movies/Edit/EditMovieTemplate.hbs b/src/UI/Movies/Edit/EditMovieTemplate.hbs new file mode 100644 index 000000000..c59520c7c --- /dev/null +++ b/src/UI/Movies/Edit/EditMovieTemplate.hbs @@ -0,0 +1,97 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>{{title}}</h3> + </div> + <div class="modal-body edit-series-modal"> + <div class="row"> + <div class="col-sm-3 hidden-xs"> + {{poster}} + </div> + <div class="col-sm-9"> + <div class="form-horizontal"> + + <div class="form-group"> + <label class="col-sm-4 control-label">Monitored</label> + + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="monitored"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-sonarr-form-info" title="Should Radarr download the movie?"/> + </span> + </div> + </div> + </div> + + <!--<div class="form-group"> + <label class="col-sm-4 control-label">Use Season Folder</label> + + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="seasonFolder"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-sonarr-form-info" title="Should downloaded episodes be stored in season folders?"/> + </span> + </div> + </div> + </div>--> + + <div class="form-group"> + <label class="col-sm-4 control-label">Profile</label> + + <div class="col-sm-4"> + <select class="form-control x-profile" id="inputProfile" name="profileId"> + {{#each profiles.models}} + <option value="{{id}}">{{attributes.name}}</option> + {{/each}} + </select> + + </div> + </div> + + <div class="form-group"> + <label class="col-sm-4 control-label">Path</label> + + <div class="col-sm-6"> + <input type="text" class="form-control x-path" placeholder="Path" name="path"> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-4 control-label">Tags</label> + + <div class="col-sm-6"> + <input type="text" class="form-control x-tags"> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-danger pull-left x-remove">Delete</button> + + <span class="indicator x-indicator"><i class="icon-sonarr-spinner fa-spin"></i></span> + <button class="btn" data-dismiss="modal">Cancel</button> + <button class="btn btn-primary x-save">Save</button> + </div> +</div> diff --git a/src/UI/Movies/Edit/EditMovieView.js b/src/UI/Movies/Edit/EditMovieView.js new file mode 100644 index 000000000..db5a626a7 --- /dev/null +++ b/src/UI/Movies/Edit/EditMovieView.js @@ -0,0 +1,54 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Profiles = require('../../Profile/ProfileCollection'); +var AsModelBoundView = require('../../Mixins/AsModelBoundView'); +var AsValidatedView = require('../../Mixins/AsValidatedView'); +var AsEditModalView = require('../../Mixins/AsEditModalView'); +require('../../Mixins/TagInput'); +require('../../Mixins/FileBrowser'); + +var view = Marionette.ItemView.extend({ + template : 'Movies/Edit/EditMovieTemplate', + + ui : { + profile : '.x-profile', + path : '.x-path', + tags : '.x-tags' + }, + + events : { + 'click .x-remove' : '_removeSeries' + }, + + initialize : function() { + this.model.set('profiles', Profiles); + }, + + onRender : function() { + this.ui.path.fileBrowser(); + this.ui.tags.tagInput({ + model : this.model, + property : 'tags' + }); + }, + + _onBeforeSave : function() { + var profileId = this.ui.profile.val(); + this.model.set({ profileId : profileId }); + }, + + _onAfterSave : function() { + this.trigger('saved'); + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _removeSeries : function() { + vent.trigger(vent.Commands.DeleteSeriesCommand, { series : this.model }); + } +}); + +AsModelBoundView.call(view); +AsValidatedView.call(view); +AsEditModalView.call(view); + +module.exports = view; diff --git a/src/UI/Movies/Editor/MovieEditorFooterView.js b/src/UI/Movies/Editor/MovieEditorFooterView.js new file mode 100644 index 000000000..05983e3f6 --- /dev/null +++ b/src/UI/Movies/Editor/MovieEditorFooterView.js @@ -0,0 +1,126 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); +var vent = require('vent'); +var Profiles = require('../../Profile/ProfileCollection'); +var RootFolders = require('../../AddMovies/RootFolders/RootFolderCollection'); +var RootFolderLayout = require('../../AddMovies/RootFolders/RootFolderLayout'); +var UpdateFilesMoviesView = require('./Organize/OrganizeFilesView'); +var Config = require('../../Config'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Editor/MovieEditorFooterViewTemplate', + + ui : { + monitored : '.x-monitored', + profile : '.x-profiles', + seasonFolder : '.x-season-folder', + rootFolder : '.x-root-folder', + selectedCount : '.x-selected-count', + container : '.series-editor-footer', + actions : '.x-action' + }, + + events : { + 'click .x-save' : '_updateAndSave', + 'change .x-root-folder' : '_rootFolderChanged', + 'click .x-organize-files' : '_organizeFiles' + }, + + templateHelpers : function() { + return { + profiles : Profiles, + rootFolders : RootFolders.toJSON() + }; + }, + + initialize : function(options) { + this.moviesCollection = options.collection; + + RootFolders.fetch().done(function() { + RootFolders.synced = true; + }); + + this.editorGrid = options.editorGrid; + this.listenTo(this.moviesCollection, 'backgrid:selected', this._updateInfo); + this.listenTo(RootFolders, 'all', this.render); + }, + + onRender : function() { + this._updateInfo(); + }, + + _updateAndSave : function() { + var selected = this.editorGrid.getSelectedModels(); + + var monitored = this.ui.monitored.val(); + var profile = this.ui.profile.val(); + var seasonFolder = this.ui.seasonFolder.val(); + var rootFolder = this.ui.rootFolder.val(); + + _.each(selected, function(model) { + if (monitored === 'true') { + model.set('monitored', true); + } else if (monitored === 'false') { + model.set('monitored', false); + } + + if (profile !== 'noChange') { + model.set('profileId', parseInt(profile, 10)); + } + + if (seasonFolder === 'true') { + model.set('seasonFolder', true); + } else if (seasonFolder === 'false') { + model.set('seasonFolder', false); + } + + if (rootFolder !== 'noChange') { + var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); + + model.set('rootFolderPath', rootFolderPath.get('path')); + } + + model.edited = true; + }); + + this.moviesCollection.save(); + }, + + _updateInfo : function() { + var selected = this.editorGrid.getSelectedModels(); + var selectedCount = selected.length; + + this.ui.selectedCount.html('{0} movies selected'.format(selectedCount)); + + if (selectedCount === 0) { + this.ui.actions.attr('disabled', 'disabled'); + } else { + this.ui.actions.removeAttr('disabled'); + } + }, + + _rootFolderChanged : function() { + var rootFolderValue = this.ui.rootFolder.val(); + if (rootFolderValue === 'addNew') { + var rootFolderLayout = new RootFolderLayout(); + this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); + vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); + } else { + Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); + } + }, + + _setRootFolder : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.ui.rootFolder.val(options.model.id); + this._rootFolderChanged(); + }, + + _organizeFiles : function() { + var selected = this.editorGrid.getSelectedModels(); + var updateFilesMoviesView = new UpdateFilesMoviesView({ movies : selected }); + this.listenToOnce(updateFilesMoviesView, 'updatingFiles', this._afterSave); + + vent.trigger(vent.Commands.OpenModalCommand, updateFilesMoviesView); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Editor/MovieEditorFooterViewTemplate.hbs b/src/UI/Movies/Editor/MovieEditorFooterViewTemplate.hbs new file mode 100644 index 000000000..5cefdae3a --- /dev/null +++ b/src/UI/Movies/Editor/MovieEditorFooterViewTemplate.hbs @@ -0,0 +1,54 @@ +<div class="series-editor-footer"> + <div class="row"> + <div class="form-group col-md-2"> + <label>Monitored</label> + + <select class="form-control x-action x-monitored"> + <option value="noChange">No change</option> + <option value="true">Monitored</option> + <option value="false">Unmonitored</option> + </select> + </div> + + <div class="form-group col-md-2"> + <label>Profile</label> + + <select class="form-control x-action x-profiles"> + <option value="noChange">No change</option> + {{#each profiles.models}} + <option value="{{id}}">{{attributes.name}}</option> + {{/each}} + </select> + </div> + + {{!--<div class="form-group col-md-2"> + <label>Season Folder</label> + + <select class="form-control x-action x-season-folder"> + <option value="noChange">No change</option> + <option value="true">Yes</option> + <option value="false">No</option> + </select> + </div>--}} + + <div class="form-group col-md-3"> + <label>Root Folder</label> + + <select class="form-control x-action x-root-folder" validation-name="RootFolderPath"> + <option value="noChange">No change</option> + {{#each rootFolders}} + <option value="{{id}}">{{path}}</option> + {{/each}} + <option value="addNew">Add a different path</option> + </select> + </div> + + <div class="form-group col-md-3 actions"> + <label class="x-selected-count">0 movies selected</label> + <div> + <button class="btn btn-primary x-action x-save">Save</button> + <button class="btn btn-danger x-action x-organize-files" title="Organize and rename movie files">Organize</button> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Editor/MovieEditorLayout.js b/src/UI/Movies/Editor/MovieEditorLayout.js new file mode 100644 index 000000000..dbbb5f4f5 --- /dev/null +++ b/src/UI/Movies/Editor/MovieEditorLayout.js @@ -0,0 +1,153 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var EmptyView = require('../Index/EmptyView'); +var MoviesCollection = require('../MoviesCollection'); +var MovieTitleCell = require('../../Cells/MovieTitleCell'); +var ProfileCell = require('../../Cells/ProfileCell'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); +var FooterView = require('./MovieEditorFooterView'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Editor/MovieEditorLayoutTemplate', + + regions : { + seriesRegion : '#x-series-editor', + toolbar : '#x-toolbar' + }, + + ui : { + monitored : '.x-monitored', + profiles : '.x-profiles', + rootFolder : '.x-root-folder', + selectedCount : '.x-selected-count' + }, + + events : { + 'click .x-save' : '_updateAndSave', + 'change .x-root-folder' : '_rootFolderChanged' + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false + }, + { + name : 'title', + label : 'Title', + cell : MovieTitleCell, + cellValue : 'this' + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell + }, + { + name : 'path', + label : 'Path', + cell : 'string' + } + ], + + leftSideButtons : { + type : 'default', + storeState : false, + items : [ + { + title : 'Update Library', + icon : 'icon-sonarr-refresh', + command : 'refreshseries', + successMessage : 'Library was updated!', + errorMessage : 'Library update failed!' + } + ] + }, + + initialize : function() { + this.movieCollection = MoviesCollection.clone(); + this.movieCollection.shadowCollection.bindSignalR(); + this.listenTo(this.movieCollection, 'save', this.render); + + this.filteringOptions = { + type : 'radio', + storeState : true, + menuKey : 'serieseditor.filterMode', + defaultAction : 'all', + items : [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-sonarr-all', + callback : this._setFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-sonarr-monitored', + callback : this._setFilter + } + ] + }; + }, + + onRender : function() { + this._showToolbar(); + this._showTable(); + }, + + onClose : function() { + vent.trigger(vent.Commands.CloseControlPanelCommand); + }, + + _showTable : function() { + if (this.movieCollection.shadowCollection.length === 0) { + this.seriesRegion.show(new EmptyView()); + this.toolbar.close(); + return; + } + + this.columns[0].sortedCollection = this.movieCollection; + + this.editorGrid = new Backgrid.Grid({ + collection : this.movieCollection, + columns : this.columns, + className : 'table table-hover' + }); + + this.seriesRegion.show(this.editorGrid); + this._showFooter(); + }, + + _showToolbar : function() { + this.toolbar.show(new ToolbarLayout({ + left : [ + this.leftSideButtons + ], + right : [ + this.filteringOptions + ], + context : this + })); + }, + + _showFooter : function() { + vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ + editorGrid : this.editorGrid, + collection : this.movieCollection + })); + }, + + _setFilter : function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.movieCollection.setFilterMode(mode); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Editor/MovieEditorLayoutTemplate.hbs b/src/UI/Movies/Editor/MovieEditorLayoutTemplate.hbs new file mode 100644 index 000000000..1d0519894 --- /dev/null +++ b/src/UI/Movies/Editor/MovieEditorLayoutTemplate.hbs @@ -0,0 +1,7 @@ +<div id="x-toolbar"></div> + +<div class="row"> + <div class="col-md-12"> + <div id="x-series-editor" class="table-responsive"></div> + </div> +</div> \ No newline at end of file diff --git a/src/UI/Movies/Editor/Organize/OrganizeFilesView.js b/src/UI/Movies/Editor/Organize/OrganizeFilesView.js new file mode 100644 index 000000000..2e03da618 --- /dev/null +++ b/src/UI/Movies/Editor/Organize/OrganizeFilesView.js @@ -0,0 +1,33 @@ +var _ = require('underscore'); +var vent = require('vent'); +var Backbone = require('backbone'); +var Marionette = require('marionette'); +var CommandController = require('../../../Commands/CommandController'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Editor/Organize/OrganizeFilesViewTemplate', + + events : { + 'click .x-confirm-organize' : '_organize' + }, + + initialize : function(options) { + this.movies = options.movies; + this.templateHelpers = { + numberOfMovies : this.movies.length, + movies : new Backbone.Collection(this.movies).toJSON() + }; + }, + + _organize : function() { + var movieIds = _.pluck(this.movies, 'id'); + + CommandController.Execute('renameMovie', { + name : 'renameMovie', + movieIds : movieIds + }); + + this.trigger('organizingFiles'); + vent.trigger(vent.Commands.CloseModalCommand); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/Editor/Organize/OrganizeFilesViewTemplate.hbs b/src/UI/Movies/Editor/Organize/OrganizeFilesViewTemplate.hbs new file mode 100644 index 000000000..1d603d7a7 --- /dev/null +++ b/src/UI/Movies/Editor/Organize/OrganizeFilesViewTemplate.hbs @@ -0,0 +1,25 @@ +<div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Organize Selected Movies</h3> + </div> + <div class="modal-body update-files-series-modal"> + <div class="alert alert-info"> + <button type="button" class="close" data-dismiss="alert">×</button> + Tip: To preview a rename... select "Cancel" then any movie title and use the <i data-original-title="" class="icon-sonarr-rename" title=""></i> + </div> + + Are you sure you want to update all files in the {{numberOfMovies}} selected movies? + + {{debug}} + <ul class="selected-series"> + {{#each movie}} + <li>{{title}}</li> + {{/each}} + </ul> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">Cancel</button> + <button class="btn btn-danger x-confirm-organize">Organize</button> + </div> +</div> diff --git a/src/UI/Movies/Files/DeleteFileCell.js b/src/UI/Movies/Files/DeleteFileCell.js new file mode 100644 index 000000000..45f815f04 --- /dev/null +++ b/src/UI/Movies/Files/DeleteFileCell.js @@ -0,0 +1,26 @@ +var vent = require('vent'); +var Backgrid = require('backgrid'); + +module.exports = Backgrid.Cell.extend({ + className : 'delete-episode-file-cell', + + events : { + 'click' : '_onClick' + }, + + render : function() { + this.$el.empty(); + this.$el.html('<i class="icon-sonarr-delete" title="Delete movie file from disk"></i>'); + + return this; + }, + + _onClick : function() { + var self = this; + if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('relativePath')))) { + this.model.destroy().done(function() { + vent.trigger(vent.Events.MovieFileDeleted, { movieFile : self.model }); + }); + } + } +}); diff --git a/src/UI/Movies/Files/FileModel.js b/src/UI/Movies/Files/FileModel.js new file mode 100644 index 000000000..cb2f217f3 --- /dev/null +++ b/src/UI/Movies/Files/FileModel.js @@ -0,0 +1,3 @@ +var Backbone = require('backbone'); + +module.exports = Backbone.Model.extend({}); diff --git a/src/UI/Movies/Files/FilesCollection.js b/src/UI/Movies/Files/FilesCollection.js new file mode 100644 index 000000000..dc787838e --- /dev/null +++ b/src/UI/Movies/Files/FilesCollection.js @@ -0,0 +1,30 @@ +var PagableCollection = require('backbone.pageable'); +var FileModel = require('./FileModel'); +var AsSortedCollection = require('../../Mixins/AsSortedCollection'); + +var Collection = PagableCollection.extend({ + url : window.NzbDrone.ApiRoot + "/moviefile", + model : FileModel, + + state : { + pageSize : 2000, + sortKey : 'title', + order : -1 + }, + + mode : 'client', + + sortMappings : { + 'quality' : { + sortKey : "qualityWeight" + }, + "edition" : { + sortKey : "edition" + } + }, + +}); + +Collection = AsSortedCollection.call(Collection); + +module.exports = Collection; diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js new file mode 100644 index 000000000..3e6dd2bdd --- /dev/null +++ b/src/UI/Movies/Files/FilesLayout.js @@ -0,0 +1,107 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +//var ButtonsView = require('./ButtonsView'); +//var ManualSearchLayout = require('./ManualLayout'); +var FilesCollection = require('./FilesCollection'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var NoResultsView = require('./NoFilesView'); +var FileModel = require("./FileModel"); +var FileTitleCell = require('../../Cells/FileTitleCell'); +var FileSizeCell = require('../../Cells/FileSizeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var MediaInfoCell = require('../../Cells/MediaInfoCell'); +var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); +var DownloadReportCell = require('../../Release/DownloadReportCell'); +var AgeCell = require('../../Release/AgeCell'); +var ProtocolCell = require('../../Release/ProtocolCell'); +var PeersCell = require('../../Release/PeersCell'); +var EditionCell = require('../../Cells/EditionCell'); +var DeleteFileCell = require("./DeleteFileCell"); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Files/FilesLayoutTemplate', + + regions : { + main : '#movie-files-region', + grid : "#movie-files-grid" + }, + + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, + + columns : [ + { + name : 'title', + label : 'Title', + cell : FileTitleCell + }, + { + name : "mediaInfo", + label : "Media Info", + cell : MediaInfoCell + }, + { + name : 'edition', + label : 'Edition', + cell : EditionCell, + title : "Edition", + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + }, + { + name : "delete", + label : "", + cell : DeleteFileCell, + } + ], + + + initialize : function(movie) { + this.filesCollection = new FilesCollection(); + var file = movie.model.get("movieFile"); + this.filesCollection.add(file); + //this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + }, + + onShow : function() { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.filesCollection, + className : 'table table-hover' + })); + }, + + _showMainView : function() { + this.main.show(this.mainView); + }, + + _showButtons : function() { + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + //this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } +}); diff --git a/src/UI/Movies/Files/FilesLayoutTemplate.hbs b/src/UI/Movies/Files/FilesLayoutTemplate.hbs new file mode 100644 index 000000000..ac6a3ca36 --- /dev/null +++ b/src/UI/Movies/Files/FilesLayoutTemplate.hbs @@ -0,0 +1 @@ +<div id="movie-files-region"><div id="movie-files-grid" class="table-responsive"></div></div> diff --git a/src/UI/Movies/Files/NoFilesView.js b/src/UI/Movies/Files/NoFilesView.js new file mode 100644 index 000000000..22a2f7c4c --- /dev/null +++ b/src/UI/Movies/Files/NoFilesView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Files/NoFilesViewTemplate' +}); diff --git a/src/UI/Movies/Files/NoFilesViewTemplate.hbs b/src/UI/Movies/Files/NoFilesViewTemplate.hbs new file mode 100644 index 000000000..300e4f666 --- /dev/null +++ b/src/UI/Movies/Files/NoFilesViewTemplate.hbs @@ -0,0 +1,3 @@ +<p class="text-warning"> + No files for this movie. +</p> diff --git a/src/UI/Movies/History/MovieHistoryActionsCell.js b/src/UI/Movies/History/MovieHistoryActionsCell.js new file mode 100644 index 000000000..c8c352aab --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryActionsCell.js @@ -0,0 +1,35 @@ +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-actions-cell', + + events : { + 'click .x-failed' : '_markAsFailed' + }, + + render : function() { + this.$el.empty(); + + if (this.model.get('eventType') === 'grabbed') { + this.$el.html('<i class="icon-sonarr-delete x-failed" title="Mark download as failed"></i>'); + } + + return this; + }, + + _markAsFailed : function() { + var url = window.NzbDrone.ApiRoot + '/history/failed'; + var data = { + id : this.model.get('id') + }; + + $.ajax({ + url : url, + type : 'POST', + data : data + }); + } +}); \ No newline at end of file diff --git a/src/UI/Movies/History/MovieHistoryDetailsCell.js b/src/UI/Movies/History/MovieHistoryDetailsCell.js new file mode 100644 index 000000000..366a25040 --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryDetailsCell.js @@ -0,0 +1,28 @@ +var $ = require('jquery'); +var vent = require('vent'); +var Marionette = require('marionette'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); +require('bootstrap'); + +module.exports = NzbDroneCell.extend({ + className : 'episode-history-details-cell', + + render : function() { + this.$el.empty(); + this.$el.html('<i class="icon-sonarr-form-info"></i>'); + + var html = new HistoryDetailsView({ model : this.model }).render().$el; + + this.$el.popover({ + content : html, + html : true, + trigger : 'hover', + title : 'Details', + placement : 'left', + container : this.$el + }); + + return this; + } +}); \ No newline at end of file diff --git a/src/UI/Movies/History/MovieHistoryLayout.js b/src/UI/Movies/History/MovieHistoryLayout.js new file mode 100644 index 000000000..3cbe20c24 --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryLayout.js @@ -0,0 +1,83 @@ +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var HistoryCollection = require('../../Activity/History/HistoryCollection'); +var EventTypeCell = require('../../Cells/EventTypeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var RelativeDateCell = require('../../Cells/RelativeDateCell'); +var EpisodeHistoryActionsCell = require('./MovieHistoryActionsCell'); +var EpisodeHistoryDetailsCell = require('./MovieHistoryDetailsCell'); +var NoHistoryView = require('./NoHistoryView'); +var LoadingView = require('../../Shared/LoadingView'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/History/MovieHistoryLayoutTemplate', + + regions : { + historyTable : '.history-table' + }, + + columns : [ + { + name : 'eventType', + label : '', + cell : EventTypeCell, + cellValue : 'this' + }, + { + name : 'sourceTitle', + label : 'Source Title', + cell : 'string' + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell + }, + { + name : 'date', + label : 'Date', + cell : RelativeDateCell + }, + { + name : 'this', + label : '', + cell : EpisodeHistoryDetailsCell, + sortable : false + }, + { + name : 'this', + label : '', + cell : EpisodeHistoryActionsCell, + sortable : false + } + ], + + initialize : function(options) { + this.model = options.model; + + this.collection = new HistoryCollection({ + movieId : this.model.id, + tableName : 'episodeHistory' + }); + this.collection.fetch(); + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onRender : function() { + this.historyTable.show(new LoadingView()); + }, + + _showTable : function() { + if (this.collection.any()) { + this.historyTable.show(new Backgrid.Grid({ + collection : this.collection, + columns : this.columns, + className : 'table table-hover table-condensed' + })); + } + + else { + this.historyTable.show(new NoHistoryView()); + } + } +}); diff --git a/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs b/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs new file mode 100644 index 000000000..a9dfe8197 --- /dev/null +++ b/src/UI/Movies/History/MovieHistoryLayoutTemplate.hbs @@ -0,0 +1 @@ +<div class="history-table table-responsive"></div> diff --git a/src/UI/Movies/History/NoHistoryView.js b/src/UI/Movies/History/NoHistoryView.js new file mode 100644 index 000000000..554534a3b --- /dev/null +++ b/src/UI/Movies/History/NoHistoryView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/History/NoHistoryViewTemplate' +}); diff --git a/src/UI/Movies/History/NoHistoryViewTemplate.hbs b/src/UI/Movies/History/NoHistoryViewTemplate.hbs new file mode 100644 index 000000000..244d82d65 --- /dev/null +++ b/src/UI/Movies/History/NoHistoryViewTemplate.hbs @@ -0,0 +1,3 @@ +<p class="text-warning"> + No history for this movie. +</p> diff --git a/src/UI/Movies/Index/EmptyTemplate.hbs b/src/UI/Movies/Index/EmptyTemplate.hbs new file mode 100644 index 000000000..42cb619d2 --- /dev/null +++ b/src/UI/Movies/Index/EmptyTemplate.hbs @@ -0,0 +1,16 @@ +<div class="no-movies"> + <div class="row"> + <div class="well col-md-12"> + <i class="icon-sonarr-comment"/> + You must be new around here, You should add some movies. + </div> + </div> + <div class="row"> + <div class="col-md-4 col-md-offset-4"> + <a href="/addmovies" class='btn btn-lg btn-block btn-success x-add-movies'> + <i class='icon-sonarr-add'></i> + Add Movie + </a> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/EmptyView.js b/src/UI/Movies/Index/EmptyView.js new file mode 100644 index 000000000..ef1393355 --- /dev/null +++ b/src/UI/Movies/Index/EmptyView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'Movies/Index/EmptyTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/EpisodeProgressPartial.hbs b/src/UI/Movies/Index/EpisodeProgressPartial.hbs new file mode 100644 index 000000000..db5c49a2b --- /dev/null +++ b/src/UI/Movies/Index/EpisodeProgressPartial.hbs @@ -0,0 +1,4 @@ +<div class="progress episode-progress"> + <span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span> + <div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> +</div> \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterModel.js b/src/UI/Movies/Index/FooterModel.js new file mode 100644 index 000000000..235552061 --- /dev/null +++ b/src/UI/Movies/Index/FooterModel.js @@ -0,0 +1,4 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterView.js b/src/UI/Movies/Index/FooterView.js new file mode 100644 index 000000000..c025a3be1 --- /dev/null +++ b/src/UI/Movies/Index/FooterView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'Movies/Index/FooterViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/FooterViewTemplate.hbs b/src/UI/Movies/Index/FooterViewTemplate.hbs new file mode 100644 index 000000000..3a1c75b35 --- /dev/null +++ b/src/UI/Movies/Index/FooterViewTemplate.hbs @@ -0,0 +1,46 @@ +<div class="row"> + <div class="series-legend legend col-xs-6 col-sm-4"> + <ul class='legend-labels'> + <li><span class="progress-bar"></span>Missing, but not yet available.</li> + <li><span class="progress-bar-success"></span>Downloaded and imported.</li> + <li><span class="progress-bar-danger"></span>Missing and monitored.</li> + <li><span class="progress-bar-warning"></span>Missing, but not monitored.</li> + </ul> + </div> + <div class="col-xs-5 col-sm-7"> + <div class="row"> + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Movies</dt> + <dd>{{series}}</dd> + + <dt>Released</dt> + <dd>{{released}}</dd> + + <dt>Announced</dt> + <dd>{{announced}}</dd> + </dl> + </div> + + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Monitored</dt> + <dd>{{monitored}}</dd> + + <dt>Unmonitored</dt> + <dd>{{unmonitored}}</dd> + </dl> + </div> + + <div class="series-stats col-sm-4"> + <dl class="dl-horizontal"> + <dt>Episodes</dt> + <dd>{{episodes}}</dd> + + <dt>Files</dt> + <dd>{{episodeFiles}}</dd> + </dl> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/MoviesIndexItemView.js b/src/UI/Movies/Index/MoviesIndexItemView.js new file mode 100644 index 000000000..999b8367a --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexItemView.js @@ -0,0 +1,35 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var CommandController = require('../../Commands/CommandController'); + +module.exports = Marionette.ItemView.extend({ + ui : { + refresh : '.x-refresh' + }, + + events : { + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' + }, + + onRender : function() { + CommandController.bindToCommand({ + element : this.ui.refresh, + command : { + name : 'refreshMovie', + seriesId : this.model.get('id') + } + }); + }, + + _editSeries : function() { + vent.trigger(vent.Commands.EditMovieCommand, { movie : this.model }); + }, + + _refreshSeries : function() { + CommandController.Execute('refreshMovie', { + name : 'refreshMovie', + movieId : this.model.id + }); + } +}); diff --git a/src/UI/Movies/Index/MoviesIndexLayout.js b/src/UI/Movies/Index/MoviesIndexLayout.js new file mode 100644 index 000000000..87a2de772 --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexLayout.js @@ -0,0 +1,335 @@ +var _ = require('underscore'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var PosterCollectionView = require('./Posters/SeriesPostersCollectionView'); +var ListCollectionView = require('./Overview/SeriesOverviewCollectionView'); +var EmptyView = require('./EmptyView'); +var MoviesCollection = require('../MoviesCollection'); +var InCinemasCell = require('../../Cells/InCinemasCell'); +var MovieTitleCell = require('../../Cells/MovieTitleCell'); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var ProfileCell = require('../../Cells/ProfileCell'); +var MovieLinksCell = require('../../Cells/MovieLinksCell'); +var MovieActionCell = require('../../Cells/MovieActionCell'); +var MovieStatusCell = require('../../Cells/MovieStatusCell'); +var MovieDownloadStatusCell = require('../../Cells/MovieDownloadStatusCell'); +var DownloadedQualityCell = require('../../Cells/DownloadedQualityCell'); +var FooterView = require('./FooterView'); +var FooterModel = require('./FooterModel'); +var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Index/MoviesIndexLayoutTemplate', + + regions : { + seriesRegion : '#x-series', + toolbar : '#x-toolbar', + toolbar2 : '#x-toolbar2', + footer : '#x-series-footer' + }, + + columns : [ + { + name : 'statusWeight', + label : '', + cell : MovieStatusCell + }, + { + name : 'title', + label : 'Title', + cell : MovieTitleCell, + cellValue : 'this', + }, + { + name : "downloadedQuality", + label : "Downloaded", + cell : DownloadedQualityCell, + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell + }, + { + name : 'inCinemas', + label : 'In Cinemas', + cell : InCinemasCell + }, + { + name : 'this', + label : 'Links', + cell : MovieLinksCell, + className : "movie-links-cell", + sortable : false, + }, + { + name : "this", + label : "Status", + cell : MovieDownloadStatusCell, + sortValue : function(m, k) { + if (m.get("downloaded")) { + return -1; + } + return 0; + } + }, + { + name : 'this', + label : '', + sortable : false, + cell : MovieActionCell + } + ], + + leftSideButtons : { + type : 'default', + storeState : false, + collapse : true, + items : [ + { + title : 'Add Movie', + icon : 'icon-sonarr-add', + route : 'addmovies' + }, + { + title : 'Movie Editor', + icon : 'icon-sonarr-edit', + route : 'movieeditor' + }, + { + title : 'RSS Sync', + icon : 'icon-sonarr-rss', + command : 'rsssync', + errorMessage : 'RSS Sync Failed!' + }, + { + title : 'Update Library', + icon : 'icon-sonarr-refresh', + command : 'refreshmovie', + successMessage : 'Library was updated!', + errorMessage : 'Library update failed!' + } + ] + }, + + initialize : function() { + this.seriesCollection = MoviesCollection.clone(); + this.seriesCollection.shadowCollection.bindSignalR(); + + this.listenTo(this.seriesCollection.shadowCollection, 'sync', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(this.seriesCollection.shadowCollection, 'add', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.listenTo(this.seriesCollection.shadowCollection, 'remove', function(model, collection, options) { + this.seriesCollection.fullCollection.resetFiltered(); + this._renderView(); + }); + + this.sortingOptions = { + type : 'sorting', + storeState : false, + viewCollection : this.seriesCollection, + items : [ + { + title : 'Title', + name : 'sortTitle' + }, + { + title : 'Quality', + name : 'profileId' + }, + { + title : 'In Cinemas', + name : 'inCinemas' + }, + { + title : "Status", + name : "status", + } + ] + }; + + this.filteringOptions = { + type : 'radio', + storeState : true, + menuKey : 'series.filterMode', + defaultAction : 'all', + items : [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-sonarr-all', + callback : this._setFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-sonarr-monitored', + callback : this._setFilter + }, + { + key : 'missing', + title : '', + tooltip : 'Missing Only', + icon : 'icon-sonarr-missing', + callback : this._setFilter + } + ] + }; + + this.viewButtons = { + type : 'radio', + storeState : true, + menuKey : 'seriesViewMode', + defaultAction : 'listView', + items : [ + { + key : 'posterView', + title : '', + tooltip : 'Posters', + icon : 'icon-sonarr-view-poster', + callback : this._showPosters + }, + { + key : 'listView', + title : '', + tooltip : 'Overview List', + icon : 'icon-sonarr-view-list', + callback : this._showList + }, + { + key : 'tableView', + title : '', + tooltip : 'Table', + icon : 'icon-sonarr-view-table', + callback : this._showTable + } + ] + }; + }, + + onShow : function() { + this._showToolbar(); + this._fetchCollection(); + }, + + _showTable : function() { + this.currentView = new Backgrid.Grid({ + collection : this.seriesCollection, + columns : this.columns, + className : 'table table-hover' + }); + + this._renderView(); + }, + + _showList : function() { + this.currentView = new ListCollectionView({ + collection : this.seriesCollection + }); + + this._renderView(); + }, + + _showPosters : function() { + this.currentView = new PosterCollectionView({ + collection : this.seriesCollection + }); + + this._renderView(); + }, + + _renderView : function() { + if (MoviesCollection.length === 0) { + this.seriesRegion.show(new EmptyView()); + + this.toolbar.close(); + this.toolbar2.close(); + } else { + this.seriesRegion.show(this.currentView); + + this._showToolbar(); + this._showFooter(); + } + }, + + _fetchCollection : function() { + this.seriesCollection.fetch(); + }, + + _setFilter : function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.seriesCollection.setFilterMode(mode); + }, + + _showToolbar : function() { + if (this.toolbar.currentView) { + return; + } + + this.toolbar2.show(new ToolbarLayout({ + right : [ + this.filteringOptions + ], + context : this + })); + + this.toolbar.show(new ToolbarLayout({ + right : [ + this.sortingOptions, + this.viewButtons + ], + left : [ + this.leftSideButtons + ], + context : this + })); + }, + + _showFooter : function() { + var footerModel = new FooterModel(); + var series = MoviesCollection.models.length; + var episodes = 0; + var episodeFiles = 0; + var announced = 0; + var released = 0; + var monitored = 0; + + _.each(MoviesCollection.models, function(model) { + episodes += model.get('episodeCount'); + episodeFiles += model.get('episodeFileCount'); + + if (model.get('status').toLowerCase() === 'released') { + released++; + } else { + announced++; + } + + if (model.get('monitored')) { + monitored++; + } + }); + + footerModel.set({ + series : series, + released : released, + announced : announced, + monitored : monitored, + unmonitored : series - monitored, + episodes : episodes, + episodeFiles : episodeFiles + }); + + this.footer.show(new FooterView({ model : footerModel })); + } +}); diff --git a/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs new file mode 100644 index 000000000..0c41b4108 --- /dev/null +++ b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs @@ -0,0 +1,12 @@ +<div class="toolbars"> + <div id="x-toolbar"></div> + <div id="x-toolbar2"></div> +</div> + +<div class="row"> + <div class="col-md-12"> + <div id="x-series" class="table-responsive"></div> + </div> +</div> + +<div id="x-series-footer"></div> diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js new file mode 100644 index 000000000..d77741892 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var ListItemView = require('./SeriesOverviewItemView'); + +module.exports = Marionette.CompositeView.extend({ + itemView : ListItemView, + itemViewContainer : '#x-series-list', + template : 'Movies/Index/Overview/SeriesOverviewCollectionViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs new file mode 100644 index 000000000..046bb3348 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs @@ -0,0 +1 @@ +<div id="x-series-list"/> diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js new file mode 100644 index 000000000..dd718d315 --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewItemView.js @@ -0,0 +1,7 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var SeriesIndexItemView = require('../MoviesIndexItemView'); + +module.exports = SeriesIndexItemView.extend({ + template : 'Movies/Index/Overview/SeriesOverviewItemViewTemplate' +}); diff --git a/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs new file mode 100644 index 000000000..b1181c07d --- /dev/null +++ b/src/UI/Movies/Index/Overview/SeriesOverviewItemViewTemplate.hbs @@ -0,0 +1,62 @@ +<div class="movie-item"> + <div class="row"> + <div class="col-md-2 col-xs-3"> + <a href="{{route}}"> + {{poster}} + </a> + </div> + <div class="col-md-10 col-xs-9"> + <div class="row"> + <div class="col-md-10 col-xs-10"> + <a href="{{route}}" target="_blank"> + <h2>{{title}}</h2> + </a> + </div> + <div class="col-md-2 col-xs-2"> + <div class="pull-right series-overview-list-actions"> + <i class="icon-sonarr-refresh x-refresh" title="Update movie info and scan disk"/> + <i class="icon-sonarr-edit x-edit" title="Edit Movie"/> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-12 col-xs-12"> + <a href="{{route}}"> + <div> + {{overview}} + </div> + </a> + </div> + </div> + <div class="row"> + <div class="col-md-12"> +   + </div> + </div> + <div class="row"> + <div class="col-md-8 col-xs-8"> + <span class="label label-default">{{GetStatus}}</span> + + <span class="label label-default">{{inCinemas}}</span> + + {{profile profileId}} + + <span class="label label-{{DownloadedStatusColor}}" title="{{DownloadedQuality}}">{{DownloadedStatus}}</span> + </div> + <div class="col-md-4 col-xs-4"> + <span class="movie-info-links"> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a>--> + {{#if website}} + <a href="{{homepage}}" class="label label-info">Homepage</a> + {{/if}} + <a href="{{tmdbUrl}}" class="label label-info">The Movie DB</a> + + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} + </span> + </div> + </div> + </div> + </div> +</div> diff --git a/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js b/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js new file mode 100644 index 000000000..0d6094f1c --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersCollectionView.js @@ -0,0 +1,8 @@ +var Marionette = require('marionette'); +var PosterItemView = require('./SeriesPostersItemView'); + +module.exports = Marionette.CompositeView.extend({ + itemView : PosterItemView, + itemViewContainer : '#x-series-posters', + template : 'Series/Index/Posters/SeriesPostersCollectionViewTemplate' +}); \ No newline at end of file diff --git a/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs b/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs new file mode 100644 index 000000000..11b8e8ac7 --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersCollectionViewTemplate.hbs @@ -0,0 +1 @@ +<ul id="x-series-posters" class="series-posters"></ul> \ No newline at end of file diff --git a/src/UI/Movies/Index/Posters/SeriesPostersItemView.js b/src/UI/Movies/Index/Posters/SeriesPostersItemView.js new file mode 100644 index 000000000..bc4545906 --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersItemView.js @@ -0,0 +1,19 @@ +var SeriesIndexItemView = require('../MoviesIndexItemView'); + +module.exports = SeriesIndexItemView.extend({ + tagName : 'li', + template : 'Movies/Index/Posters/SeriesPostersItemViewTemplate', + + initialize : function() { + this.events['mouseenter .x-series-poster-container'] = 'posterHoverAction'; + this.events['mouseleave .x-series-poster-container'] = 'posterHoverAction'; + + this.ui.controls = '.x-series-controls'; + this.ui.title = '.x-title'; + }, + + posterHoverAction : function() { + this.ui.controls.slideToggle(); + this.ui.title.slideToggle(); + } +}); diff --git a/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs new file mode 100644 index 000000000..92e6e3298 --- /dev/null +++ b/src/UI/Movies/Index/Posters/SeriesPostersItemViewTemplate.hbs @@ -0,0 +1,32 @@ +<div class="series-posters-item"> + <div class="center"> + <div class="series-poster-container x-series-poster-container"> + <div class="series-controls x-series-controls"> + <i class="icon-sonarr-refresh x-refresh" title="Refresh Movie"/> + <i class="icon-sonarr-edit x-edit" title="Edit Movie"/> + </div> + {{GetBannerStatus}} + <a href="{{route}}"> + {{poster}} + <div class="center title">{{title}}</div> + </a> + <div class="hidden-title x-title"> + {{title}} + </div> + </div> + </div> + + <div class="center"> + <div class="labels"> + + {{#if website}} + <a href="{{homepage}}" class="label label-info">Homepage</a> + {{/if}} + <a href="{{tmdbUrl}}" class="label label-info">The Movie DB</a> + {{#if imdbId}} + <a href="{{imdbUrl}}" class="label label-info">IMDB</a> + {{/if}} + + </div> + </div> +</div> diff --git a/src/UI/Movies/MovieModel.js b/src/UI/Movies/MovieModel.js new file mode 100644 index 000000000..5c8538271 --- /dev/null +++ b/src/UI/Movies/MovieModel.js @@ -0,0 +1,39 @@ +var Backbone = require('backbone'); +var _ = require('underscore'); + +module.exports = Backbone.Model.extend({ + urlRoot : window.NzbDrone.ApiRoot + '/movie', + + defaults : { + episodeFileCount : 0, + episodeCount : 0, + isExisting : false, + status : 0 + }, + + getStatus : function() { + var monitored = this.get("monitored"); + var status = this.get("status"); + var inCinemas = this.get("inCinemas"); + var date = new Date(inCinemas); + var timeSince = new Date().getTime() - date.getTime(); + var numOfMonths = timeSince / 1000 / 60 / 60 / 24 / 30; + + if (status === "announced") { + return "announced" + } + + if (numOfMonths < 3 && numOfMonths > 0) { + + return "inCinemas"; + } + + if (status === 'released') { + return "released"; + } + + if (numOfMonths > 3) { + return "released";//TODO: Update for PreDB.me + } + } +}); diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js new file mode 100644 index 000000000..193f47ef6 --- /dev/null +++ b/src/UI/Movies/MoviesCollection.js @@ -0,0 +1,157 @@ +var _ = require('underscore'); +var Backbone = require('backbone'); +var PageableCollection = require('backbone.pageable'); +var MovieModel = require('./MovieModel'); +var ApiData = require('../Shared/ApiData'); +var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); +var AsSortedCollection = require('../Mixins/AsSortedCollection'); +var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); +var moment = require('moment'); +require('../Mixins/backbone.signalr.mixin'); + +var Collection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/movie', + model : MovieModel, + tableName : 'movie', + + state : { + sortKey : 'sortTitle', + order : 1, + pageSize : 100000, + secondarySortKey : 'sortTitle', + secondarySortOrder : -1 + }, + + mode : 'client', + + save : function() { + var self = this; + + var proxy = _.extend(new Backbone.Model(), { + id : '', + + url : self.url + '/editor', + + toJSON : function() { + return self.filter(function(model) { + return model.edited; + }); + } + }); + + this.listenTo(proxy, 'sync', function(proxyModel, models) { + this.add(models, { merge : true }); + this.trigger('save', this); + }); + + return proxy.save(); + }, + + filterModes : { + 'all' : [ + null, + null + ], + 'continuing' : [ + 'status', + 'continuing' + ], + 'ended' : [ + 'status', + 'ended' + ], + 'monitored' : [ + 'monitored', + true + ], + 'missing' : [ + 'downloaded', + false + ] + }, + + sortMappings : { + title : { + sortKey : 'sortTitle' + }, + statusWeight : { + sortValue : function(model, attr) { + if (model.getStatus() == "released") { + return 1; + } + if (model.getStatus() == "inCinemas") { + return 0; + } + return -1; + } + }, + downloadedQuality : { + sortValue : function(model, attr) { + if (model.get("movieFile")) { + return 1000-model.get("movieFile").quality.quality.id; + } + + return -1; + } + }, + nextAiring : { + sortValue : function(model, attr, order) { + var nextAiring = model.get(attr); + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (order === 1) { + return 0; + } + + return Number.MAX_VALUE; + } + }, + status: { + sortValue : function(model, attr) { + debugger; + if (model.get("downloaded")) { + return -1; + } + return 0; + } + }, + percentOfEpisodes : { + sortValue : function(model, attr) { + var percentOfEpisodes = model.get(attr); + var episodeCount = model.get('episodeCount'); + + return percentOfEpisodes + episodeCount / 1000000; + } + }, + inCinemas : { + + sortValue : function(model, attr) { + var monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + if (model.get("inCinemas")) { + return model.get("inCinemas"); + } + return "2100-01-01"; + } + }, + path : { + sortValue : function(model) { + var path = model.get('path'); + + return path.toLowerCase(); + } + } + } +}); + +Collection = AsFilteredCollection.call(Collection); +Collection = AsSortedCollection.call(Collection); +Collection = AsPersistedStateCollection.call(Collection); + +var data = ApiData.get('movie'); + +module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Movies/MoviesController.js b/src/UI/Movies/MoviesController.js new file mode 100644 index 000000000..3d5ee48bb --- /dev/null +++ b/src/UI/Movies/MoviesController.js @@ -0,0 +1,38 @@ +var NzbDroneController = require('../Shared/NzbDroneController'); +var AppLayout = require('../AppLayout'); +var MoviesCollection = require('./MoviesCollection'); +var MoviesIndexLayout = require('./Index/MoviesIndexLayout'); +var MoviesDetailsLayout = require('./Details/MoviesDetailsLayout'); +var SeriesDetailsLayout = require('../Series/Details/SeriesDetailsLayout'); + +module.exports = NzbDroneController.extend({ + _originalInit : NzbDroneController.prototype.initialize, + + initialize : function() { + this.route('', this.series); + this.route('movies', this.series); + this.route('movies/:query', this.seriesDetails); + + this._originalInit.apply(this, arguments); + }, + + series : function() { + this.setTitle('Movies'); + this.showMainRegion(new MoviesIndexLayout()); + }, + + seriesDetails : function(query) { + var series = MoviesCollection.where({ titleSlug : query }); + if (series.length !== 0) { + var targetMovie = series[0]; + console.log(AppLayout.mainRegion); + + this.setTitle(targetMovie.get('title')); + //this.showNotFound(); + //this.showMainRegion(new SeriesDetailsLayout({model : targetMovie})); + this.showMainRegion(new MoviesDetailsLayout({ model : targetMovie })); + } else { + this.showNotFound(); + } + } +}); diff --git a/src/UI/Movies/Search/ButtonsView.js b/src/UI/Movies/Search/ButtonsView.js new file mode 100644 index 000000000..534e2f960 --- /dev/null +++ b/src/UI/Movies/Search/ButtonsView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Search/ButtonsViewTemplate' +}); diff --git a/src/UI/Movies/Search/ButtonsViewTemplate.hbs b/src/UI/Movies/Search/ButtonsViewTemplate.hbs new file mode 100644 index 000000000..6ad9474d5 --- /dev/null +++ b/src/UI/Movies/Search/ButtonsViewTemplate.hbs @@ -0,0 +1,4 @@ +<div class="search-buttons"> + <button class="btn btn-lg btn-block x-search-auto"><i class="icon-sonarr-search-automatic"/> Automatic Search</button> + <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-sonarr-search-manual"/> Manual Search</button> +</div> \ No newline at end of file diff --git a/src/UI/Movies/Search/ManualLayout.js b/src/UI/Movies/Search/ManualLayout.js new file mode 100644 index 000000000..304b08911 --- /dev/null +++ b/src/UI/Movies/Search/ManualLayout.js @@ -0,0 +1,93 @@ +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); +var FileSizeCell = require('../../Cells/FileSizeCell'); +var QualityCell = require('../../Cells/QualityCell'); +var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); +var DownloadReportCell = require('../../Release/DownloadReportCell'); +var AgeCell = require('../../Release/AgeCell'); +var ProtocolCell = require('../../Release/ProtocolCell'); +var PeersCell = require('../../Release/PeersCell'); +var EditionCell = require('../../Cells/EditionCell'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Search/ManualLayoutTemplate', + + regions : { + grid : '#episode-release-grid' + }, + + columns : [ + { + name : 'protocol', + label : 'Source', + cell : ProtocolCell + }, + { + name : 'age', + label : 'Age', + cell : AgeCell + }, + { + name : 'title', + label : 'Title', + cell : ReleaseTitleCell + }, + { + name : 'edition', + label : 'Edition', + cell : EditionCell, + title : "Edition", + }, + { + name : 'indexer', + label : 'Indexer', + cell : Backgrid.StringCell + }, + { + name : 'size', + label : 'Size', + cell : FileSizeCell + }, + { + name : 'seeders', + label : 'Peers', + cell : PeersCell + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + }, + { + name : 'rejections', + label : '<i class="icon-sonarr-header-rejections" />', + tooltip : 'Rejections', + cell : ApprovalStatusCell, + sortable : true, + sortType : 'fixed', + direction : 'ascending', + title : 'Release Rejected' + }, + { + name : 'download', + label : '<i class="icon-sonarr-download" />', + tooltip : 'Auto-Search Prioritization', + cell : DownloadReportCell, + sortable : true, + sortType : 'fixed', + direction : 'ascending' + } + ], + + onShow : function() { + if (!this.isClosed) { + this.grid.show(new Backgrid.Grid({ + row : Backgrid.Row, + columns : this.columns, + collection : this.collection, + className : 'table table-hover' + })); + } + } +}); diff --git a/src/UI/Movies/Search/ManualLayoutTemplate.hbs b/src/UI/Movies/Search/ManualLayoutTemplate.hbs new file mode 100644 index 000000000..3a5f59438 --- /dev/null +++ b/src/UI/Movies/Search/ManualLayoutTemplate.hbs @@ -0,0 +1 @@ +<div id="episode-release-grid" class="table-responsive"></div> diff --git a/src/UI/Movies/Search/MovieSearchLayout.js b/src/UI/Movies/Search/MovieSearchLayout.js new file mode 100644 index 000000000..aa8d994c3 --- /dev/null +++ b/src/UI/Movies/Search/MovieSearchLayout.js @@ -0,0 +1,82 @@ +var vent = require('vent'); +var Marionette = require('marionette'); +var ButtonsView = require('./ButtonsView'); +var ManualSearchLayout = require('./ManualLayout'); +var ReleaseCollection = require('../../Release/ReleaseCollection'); +var CommandController = require('../../Commands/CommandController'); +var LoadingView = require('../../Shared/LoadingView'); +var NoResultsView = require('./NoResultsView'); + +module.exports = Marionette.Layout.extend({ + template : 'Movies/Search/MovieSearchLayoutTemplate', + + regions : { + main : '#episode-search-region' + }, + + events : { + 'click .x-search-auto' : '_searchAuto', + 'click .x-search-manual' : '_searchManual', + 'click .x-search-back' : '_showButtons' + }, + + initialize : function() { + this.mainView = new ButtonsView(); + this.releaseCollection = new ReleaseCollection(); + + this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); + }, + + onShow : function() { + if (this.startManualSearch) { + this._searchManual(); + } + + else { + this._showMainView(); + } + }, + + _searchAuto : function(e) { + if (e) { + e.preventDefault(); + } + + CommandController.Execute('episodeSearch', { + episodeIds : [this.model.get('id')] + }); + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _searchManual : function(e) { + if (e) { + e.preventDefault(); + } + + this.mainView = new LoadingView(); + this._showMainView(); + this.releaseCollection.fetchMovieReleases(this.model.id); + }, + + _showMainView : function() { + this.main.show(this.mainView); + }, + + _showButtons : function() { + this.mainView = new ButtonsView(); + this._showMainView(); + }, + + _showSearchResults : function() { + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); + } + + this._showMainView(); + } +}); diff --git a/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs b/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs new file mode 100644 index 000000000..879e0b356 --- /dev/null +++ b/src/UI/Movies/Search/MovieSearchLayoutTemplate.hbs @@ -0,0 +1 @@ +<div id="episode-search-region"></div> \ No newline at end of file diff --git a/src/UI/Movies/Search/NoResultsView.js b/src/UI/Movies/Search/NoResultsView.js new file mode 100644 index 000000000..2b8bffd7c --- /dev/null +++ b/src/UI/Movies/Search/NoResultsView.js @@ -0,0 +1,5 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.ItemView.extend({ + template : 'Movies/Search/NoResultsViewTemplate' +}); diff --git a/src/UI/Movies/Search/NoResultsViewTemplate.hbs b/src/UI/Movies/Search/NoResultsViewTemplate.hbs new file mode 100644 index 000000000..7904e5520 --- /dev/null +++ b/src/UI/Movies/Search/NoResultsViewTemplate.hbs @@ -0,0 +1 @@ +<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Movies/movies.less b/src/UI/Movies/movies.less new file mode 100644 index 000000000..6c19733dc --- /dev/null +++ b/src/UI/Movies/movies.less @@ -0,0 +1,532 @@ +@import "../Content/Bootstrap/variables"; +@import "../Shared/Styles/card.less"; +@import "../Shared/Styles/clickable.less"; +@import "../Content/prefixer"; + +.series-poster { + min-width: 56px; + max-width: 100%; +} + +.movie-tabs-card { + .card; + .opacity(0.9); + margin : 30px 10px; + padding : 10px 25px; + + .show-hide-episodes { + .clickable(); + text-align : center; + + i { + .clickable(); + } + } +} + +.edit-movie-modal, .delete-movie-modal { + overflow : visible; + + .series-poster { + padding-left : 20px; + width : 168px; + } + + .form-horizontal { + margin-top : 10px; + } + + .twitter-typeahead { + .form-control[disabled] { + background-color: #ffffff; + } + } +} + +.delete-movie-modal { + .path { + margin-left : 30px; + } + + .delete-files-info { + margin-top : 10px; + display : none; + } +} + +.movie-item { + padding-bottom : 30px; + + :hover { + text-decoration : none; + } + + h2 { + margin-top : 0px; + } + + a { + color : #000000; + } + + .movie-info-links { + a { + color: white; + } + } +} + +.movie-page-header { + .card(black); + .opacity(0.9); + background : #000000; + color : #ffffff; + padding : 30px 15px; + margin : 50px 10px; + + .poster { + margin-top : 4px; + } + + .header-text { + margin-top : 0px; + } +} + +.movie-season { + .card; + .opacity(0.9); + margin : 30px 10px; + padding : 10px 25px; + + .show-hide-episodes { + .clickable(); + text-align : center; + + i { + .clickable(); + } + } +} + +.series-posters { + list-style-type: none; + + @media (max-width: @screen-xs-max) { + padding : 0px; + } + + li { + display : inline-block; + vertical-align : top; + } + + .series-posters-item { + + .card; + .clickable; + margin-bottom : 20px; + height : 324px; + + .center { + display : block; + margin-left : auto; + margin-right : auto; + text-align : center; + + .progress { + text-align : left; + margin-top : 5px; + left : 0px; + width : 170px; + + .progressbar-front-text, .progressbar-back-text { + width : 170px; + } + } + } + + .labels { + display : inline-block; + .opacity(0.75); + width : 170px; + + :hover { + cursor : default; + } + + .label { + margin-top : 3px; + display : block; + } + + .tooltip { + .opacity(1); + } + } + + @media (max-width: @screen-xs-max) { + height : 235px; + margin : 5px; + padding : 6px 5px; + + .center { + .progress { + width : 125px; + + .progressbar-front-text, .progressbar-back-text { + width : 125px + } + } + } + + .labels { + width: 125px; + } + } + } + + .series-poster-container { + position : relative; + overflow : hidden; + display : inline-block; + + .placeholder-image ~ .title { + opacity: 1.0; + } + + .title { + position : absolute; + top : 25px; + color : #f5f5f5; + width : 100%; + font-size : 22px; + line-height: 24px; + opacity : 0.0; + font-weight: 100; + } + + .announced-banner { + color : #eeeeee; + background-color : #777; + .box-shadow(2px 2px 20px #888888); + -moz-transform-origin : 50% 50%; + -webkit-transform-origin : 50% 50%; + position : absolute; + width : 320px; + top : 200px; + left : -122px; + text-align : center; + .opacity(0.9); + + .transform(rotate(45deg)); + } + + .released-banner { + color : #eeeeee; + background-color : #5cb85c; + .box-shadow(2px 2px 20px #888888); + -moz-transform-origin : 50% 50%; + -webkit-transform-origin : 50% 50%; + position : absolute; + width : 320px; + top : 200px; + left : -122px; + text-align : center; + .opacity(0.9); + + .transform(rotate(45deg)); + } + + .cinemas-banner { + color : #eeeeee; + background-color : #b94a48; + .box-shadow(2px 2px 20px #888888); + -moz-transform-origin : 50% 50%; + -webkit-transform-origin : 50% 50%; + position : absolute; + width : 320px; + top : 200px; + left : -122px; + text-align : center; + .opacity(0.9); + + .transform(rotate(45deg)); + } + + .movie-controls { + position : absolute;; + top : 0px; + overflow : hidden; + background-color : #eeeeee; + width : 100%; + text-align : right; + padding-right : 10px; + display : none; + .opacity(0.8); + + i { + .clickable(); + } + } + + .hidden-title { + position : absolute;; + bottom : 0px; + overflow : hidden; + background-color : #eeeeee; + width : 100%; + text-align : center; + .opacity(0.8); + display : none; + } + + .series-poster { + width : 168px; + height : 247px; + display : block; + font-size : 34px; + line-height : 34px; + } + + @media (max-width: @screen-xs-max) { + .series-poster { + width : 120px; + height : 176px; + } + + .ended-banner { + top : 145px; + left : -137px; + } + } + } +} + + +.movie-detail-overview { + margin-bottom : 50px; +} + +.alternative-titles { + font-size: 12px; + color: rgba(255, 255, 255, 180); + opacity: .75; +} + +.movie-season { + + .episode-number-cell { + width : 40px; + white-space: nowrap; + } + .episode-air-date-cell { + width : 150px; + } + + .episode-status-cell { + width : 100px; + } + + .episode-title-cell { + cursor : pointer; + } +} + +#movie-info { + + .episode-info { + margin-bottom : 10px; + } + + .episode-overview { + font-style : italic; + } + + .episode-file-info { + margin-top : 30px; + font-size : 12px; + } + + .episode-history-details-cell .popover { + max-width: 800px; + } + + .hidden-movie-title { + display : none; + } +} + +.season-grid { + .toggle-cell { + width : 28px; + text-align : center; + padding-left : 0px; + padding-right : 0px; + } + + .toggle-cell { + i { + .clickable; + } + } +} + +.season-actions { + width: 100px; +} + +.season-actions, .movie-actions { + + div { + display : inline-block + } + + text-transform : none; + + i { + .clickable(); + font-size : 24px; + margin-left : 5px; + } +} + +.movie-stats { + font-size : 11px; +} + +.movie-legend { + padding-top : 5px; +} + +.seasonpass-movie { + .card; + margin : 20px 0px; + + .title { + font-weight : 300; + font-size : 24px; + line-height : 30px; + margin-left : 5px; + } + + .season-select { + margin-bottom : 0px; + } + + .expander { + .clickable; + line-height : 30px; + margin-left : 8px; + width : 16px; + } + + .season-grid { + margin-top : 10px; + } + + .season-pass-button { + display : inline-block; + } + + .movie-monitor-toggle { + font-size : 24px; + margin-top : 3px; + } + + .help-inline { + margin-top : 7px; + display : inline-block; + } +} + +.season-status { + font-size : 11px; + vertical-align : middle !important; +} + +//Overview List +.movie-overview-list-actions { + min-width: 56px; + max-width: 56px; + + i { + .clickable(); + } +} + +//Editor + +.movie-editor-footer { + max-width: 1160px; + color: #f5f5f5; + margin-left: auto; + margin-right: auto; + + .form-group { + padding-top: 0px; + } +} + +.update-files-movie-modal { + .selected-movie { + margin-top: 15px; + } +} + +//Series Details + +.movie-not-monitored { + .season-monitored, .episode-monitored { + color: #888888; + cursor: not-allowed; + + i { + cursor: not-allowed; + } + } +} + +.movie-info { + .row { + margin-bottom : 3px; + + .label { + display : inline-block; + margin-bottom : 2px; + padding : 4px 6px 3px 6px; + max-width : 100%; + white-space : normal; + word-wrap : break-word; + } + } + + .movie-info-links { + @media (max-width: @screen-sm-max) { + display : inline-block; + margin-top : 5px; + } + } +} + +.scene-info { + .key, .value { + display : inline-block; + } + + .key { + width : 80px; + margin-left : 10px; + vertical-align : top; + } + + .value { + margin-right : 10px; + max-width : 170px; + } + + ul { + padding-left : 0px; + list-style-type : none; + } +} diff --git a/src/UI/Navbar/NavbarLayoutTemplate.hbs b/src/UI/Navbar/NavbarLayoutTemplate.hbs index 75cfc096f..1665c6838 100644 --- a/src/UI/Navbar/NavbarLayoutTemplate.hbs +++ b/src/UI/Navbar/NavbarLayoutTemplate.hbs @@ -7,19 +7,19 @@ <span class="icon-sonarr-navbar-collapsed fa-lg"></span> </button> <a class="navbar-brand" href="{{UrlBase}}/"> - <!--<img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="Sonarr">--> + <!--<img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="Radarr">--> <img src="{{UrlBase}}/Content/Images/logos/128.png" class="visible-lg"/> <img src="{{UrlBase}}/Content/Images/logos/64.png" class="visible-md visible-sm"/> <span class="visible-xs"> <img src="{{UrlBase}}/Content/Images/logos/32.png"/> - <span class="logo-text">sonarr</span> + <span class="logo-text">Radarr</span> </span> </a> </div> <div class="navbar-collapse collapse x-navbar-collapse"> <ul class="nav navbar-nav"> - <li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-series"></i> Series</a></li> + <li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-series"></i> Movies</a></li> <li><a href="{{UrlBase}}/calendar" class="x-calendar-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-calendar"></i> Calendar</a></li> <li><a href="{{UrlBase}}/activity" class="x-activity-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-activity"></i> Activity<span id="x-queue-count" class="navbar-info"></span></a></li> <li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-sonarr-navbar-icon icon-sonarr-navbar-wanted"></i> Wanted</a></li> @@ -37,8 +37,8 @@ <div class="col-md-6 col-md-offset-3"> <div class="input-group"> <span class="input-group-addon"><i class="fa fa-search"></i></span> - <input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the series in your library"> + <input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the movies in your library"> </div> </div> </div> -</div> \ No newline at end of file +</div> diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js index ec1e14ead..dce7e8204 100644 --- a/src/UI/Navbar/Search.js +++ b/src/UI/Navbar/Search.js @@ -2,7 +2,7 @@ var _ = require('underscore'); var $ = require('jquery'); var vent = require('vent'); var Backbone = require('backbone'); -var SeriesCollection = require('../Series/SeriesCollection'); +var SeriesCollection = require('../Movies/MoviesCollection'); require('typeahead'); vent.on(vent.Hotkeys.NavbarSearch, function() { @@ -32,6 +32,6 @@ $.fn.bindSearch = function() { $(this).on('typeahead:selected typeahead:autocompleted', function(e, series) { this.blur(); $(this).val(''); - Backbone.history.navigate('/series/{0}'.format(series.titleSlug), { trigger : true }); + Backbone.history.navigate('/movies/{0}'.format(series.titleSlug), { trigger : true }); }); -}; \ No newline at end of file +}; diff --git a/src/UI/Release/ReleaseCollection.js b/src/UI/Release/ReleaseCollection.js index a66547f00..2b810982a 100644 --- a/src/UI/Release/ReleaseCollection.js +++ b/src/UI/Release/ReleaseCollection.js @@ -16,7 +16,7 @@ var Collection = PagableCollection.extend({ sortMappings : { 'quality' : { - sortKey : 'qualityWeight' + sortKey : "qualityWeight" }, 'rejections' : { sortValue : function(model) { @@ -30,6 +30,9 @@ var Collection = PagableCollection.extend({ return releaseWeight; } }, + "edition" : { + sortKey : "edition" + }, 'download' : { sortKey : 'releaseWeight' }, @@ -48,9 +51,14 @@ var Collection = PagableCollection.extend({ fetchEpisodeReleases : function(episodeId) { return this.fetch({ data : { episodeId : episodeId } }); + }, + + fetchMovieReleases : function(movieId) { + return this.fetch({ data : { movieId : movieId}}); } + }); Collection = AsSortedCollection.call(Collection); -module.exports = Collection; \ No newline at end of file +module.exports = Collection; diff --git a/src/UI/Release/ReleaseLayout.js b/src/UI/Release/ReleaseLayout.js index 07f4a1af6..9b1791ff6 100644 --- a/src/UI/Release/ReleaseLayout.js +++ b/src/UI/Release/ReleaseLayout.js @@ -7,6 +7,7 @@ var FileSizeCell = require('../Cells/FileSizeCell'); var QualityCell = require('../Cells/QualityCell'); var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); var LoadingView = require('../Shared/LoadingView'); +var EditionCell = require('../Cells/EditionCell'); module.exports = Marionette.Layout.extend({ template : 'Release/ReleaseLayoutTemplate', @@ -17,6 +18,12 @@ module.exports = Marionette.Layout.extend({ }, columns : [ + { + name : 'edition', + label : 'Edition', + sortable : false, + cell : EditionCell + }, { name : 'indexer', label : 'Indexer', @@ -29,12 +36,12 @@ module.exports = Marionette.Layout.extend({ sortable : true, cell : Backgrid.StringCell }, - { + /*{ name : 'episodeNumbers', episodes : 'episodeNumbers', label : 'season', cell : EpisodeNumberCell - }, + },*/ { name : 'size', label : 'Size', @@ -75,4 +82,4 @@ module.exports = Marionette.Layout.extend({ })); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Rename/RenamePreviewCollection.js b/src/UI/Rename/RenamePreviewCollection.js index ce9f49b4a..3048dc135 100644 --- a/src/UI/Rename/RenamePreviewCollection.js +++ b/src/UI/Rename/RenamePreviewCollection.js @@ -1,3 +1,38 @@ +// var Backbone = require('backbone'); +// var RenamePreviewModel = require('./RenamePreviewModel'); + +// module.exports = Backbone.Collection.extend({ +// url : window.NzbDrone.ApiRoot + '/rename', +// model : RenamePreviewModel, + +// originalFetch : Backbone.Collection.prototype.fetch, + +// initialize : function(options) { +// if (!options.seriesId) { +// throw 'seriesId is required'; +// } + +// this.seriesId = options.seriesId; +// this.seasonNumber = options.seasonNumber; +// }, + +// fetch : function(options) { +// if (!this.seriesId) { +// throw 'seriesId is required'; +// } + +// options = options || {}; +// options.data = {}; +// options.data.seriesId = this.seriesId; + +// if (this.seasonNumber !== undefined) { +// options.data.seasonNumber = this.seasonNumber; +// } + +// return this.originalFetch.call(this, options); +// } +// }); + var Backbone = require('backbone'); var RenamePreviewModel = require('./RenamePreviewModel'); @@ -8,26 +43,26 @@ module.exports = Backbone.Collection.extend({ originalFetch : Backbone.Collection.prototype.fetch, initialize : function(options) { - if (!options.seriesId) { - throw 'seriesId is required'; + if (!options.movieId) { + throw 'movieId is required'; } - this.seriesId = options.seriesId; - this.seasonNumber = options.seasonNumber; + this.movieId = options.movieId; + //this.seasonNumber = options.seasonNumber; }, fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; + if (!this.movieId) { + throw 'movieId is required'; } options = options || {}; options.data = {}; - options.data.seriesId = this.seriesId; + options.data.movieId = this.movieId; - if (this.seasonNumber !== undefined) { - options.data.seasonNumber = this.seasonNumber; - } + // if (this.seasonNumber !== undefined) { + // options.data.seasonNumber = this.seasonNumber; + //} return this.originalFetch.call(this, options); } diff --git a/src/UI/Rename/RenamePreviewLayout.js b/src/UI/Rename/RenamePreviewLayout.js index eb1cf604a..f8b26658e 100644 --- a/src/UI/Rename/RenamePreviewLayout.js +++ b/src/UI/Rename/RenamePreviewLayout.js @@ -29,12 +29,13 @@ module.exports = Marionette.Layout.extend({ }, initialize : function(options) { - this.model = options.series; + this.model = options.movie; this.seasonNumber = options.seasonNumber; var viewOptions = {}; - viewOptions.seriesId = this.model.id; - viewOptions.seasonNumber = this.seasonNumber; + //viewOptions.seriesId = this.model.id; + //viewOptions.seasonNumber = this.seasonNumber; + viewOptions.movieId = this.model.id; this.collection = new RenamePreviewCollection(viewOptions); this.listenTo(this.collection, 'sync', this._showPreviews); @@ -66,7 +67,8 @@ module.exports = Marionette.Layout.extend({ } var files = _.map(this.collection.where({ rename : true }), function(model) { - return model.get('episodeFileId'); + //return model.get('episodeFileId'); + return model.get('movieFileId'); }); if (files.length === 0) { @@ -74,21 +76,11 @@ module.exports = Marionette.Layout.extend({ return; } - if (this.seasonNumber) { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - seriesId : this.model.id, - seasonNumber : this.seasonNumber, - files : files - }); - } else { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - seriesId : this.model.id, - seasonNumber : -1, - files : files - }); - } + CommandController.Execute('renameMovieFiles', { + name : 'renameMovieFiles', + movieId : this.model.id, + files : files + }); vent.trigger(vent.Commands.CloseModalCommand); }, diff --git a/src/UI/Router.js b/src/UI/Router.js index 91b42a074..c04492dba 100644 --- a/src/UI/Router.js +++ b/src/UI/Router.js @@ -6,6 +6,8 @@ module.exports = Marionette.AppRouter.extend({ appRoutes : { 'addseries' : 'addSeries', 'addseries/:action(/:query)' : 'addSeries', + 'addmovies' : 'addMovies', + 'addmovies/:action(/:query)' : 'addMovies', 'calendar' : 'calendar', 'settings' : 'settings', 'settings/:action(/:query)' : 'settings', @@ -19,7 +21,7 @@ module.exports = Marionette.AppRouter.extend({ 'system' : 'system', 'system/:action' : 'system', 'seasonpass' : 'seasonPass', - 'serieseditor' : 'seriesEditor', + 'movieeditor' : 'movieEditor', ':whatever' : 'showNotFound' } -}); \ No newline at end of file +}); diff --git a/src/UI/Series/Details/InfoViewTemplate.hbs b/src/UI/Series/Details/InfoViewTemplate.hbs index b52130246..666003f77 100644 --- a/src/UI/Series/Details/InfoViewTemplate.hbs +++ b/src/UI/Series/Details/InfoViewTemplate.hbs @@ -29,9 +29,9 @@ </div> <div class="col-md-3"> <span class="series-info-links"> - <a href="{{traktUrl}}" class="label label-info">Trakt</a> + <!--<a href="{{traktUrl}}" class="label label-info">Trakt</a> - <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a> + <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a>--> {{#if imdbId}} <a href="{{imdbUrl}}" class="label label-info">IMDB</a> diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js index f33cb0414..d869b47c9 100644 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ b/src/UI/Series/Details/SeriesDetailsLayout.js @@ -32,7 +32,8 @@ module.exports = Marionette.Layout.extend({ refresh : '.x-refresh', rename : '.x-rename', search : '.x-search', - poster : '.x-series-poster' + poster : '.x-series-poster', + manualSearch : '.x-manual-search' }, events : { @@ -41,7 +42,8 @@ module.exports = Marionette.Layout.extend({ 'click .x-edit' : '_editSeries', 'click .x-refresh' : '_refreshSeries', 'click .x-rename' : '_renameSeries', - 'click .x-search' : '_seriesSearch' + 'click .x-search' : '_seriesSearch', + 'click .x-manual-search' : '_manualSearchM' }, initialize : function() { @@ -178,11 +180,11 @@ module.exports = Marionette.Layout.extend({ if (self.model.get('id') !== seriesId) { return []; } - + if (sceneSeasonNumber === undefined) { sceneSeasonNumber = seasonNumber; } - + return _.where(self.model.get('alternateTitles'), function(alt) { return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; @@ -254,5 +256,17 @@ module.exports = Marionette.Layout.extend({ } else { $('body').removeClass('backdrop'); } + }, + + _manualSearchM : function() { + console.warn("Manual Search started"); + console.warn(this.model.get("seriesId")); + console.warn(this.model) + console.warn(this.episodeCollection); + vent.trigger(vent.Commands.ShowEpisodeDetails, { + episode : this.episodeCollection.models[0], + hideSeriesLink : true, + openingTab : 'search' + }); } -}); \ No newline at end of file +}); diff --git a/src/UI/Series/Details/SeriesDetailsTemplate.hbs b/src/UI/Series/Details/SeriesDetailsTemplate.hbs index 818cee455..04978e3d4 100644 --- a/src/UI/Series/Details/SeriesDetailsTemplate.hbs +++ b/src/UI/Series/Details/SeriesDetailsTemplate.hbs @@ -5,23 +5,26 @@ <div class="col-md-12 col-lg-10"> <div> <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire series"/> + <i class="x-monitored" title="Toggle monitored state for movie"/> {{title}} <div class="series-actions pull-right"> <div class="x-episode-file-editor"> - <i class="icon-sonarr-episode-file" title="Modify episode files for series"/> + <i class="icon-sonarr-episode-file" title="Modify episode files for movie"/> </div> <div class="x-refresh"> - <i class="icon-sonarr-refresh icon-can-spin" title="Update series info and scan disk"/> + <i class="icon-sonarr-refresh icon-can-spin" title="Update movie info and scan disk"/> </div> <div class="x-rename"> <i class="icon-sonarr-rename" title="Preview rename for all episodes"/> </div> <div class="x-search"> - <i class="icon-sonarr-search" title="Search for monitored episodes in this series"/> + <i class="icon-sonarr-search" title="Search for movie"/> + </div> + <div class="x-manual-search"> + <i class="icon-sonarr-search-manual" title="Manual Search"/> </div> <div class="x-edit"> - <i class="icon-sonarr-edit" title="Edit series"/> + <i class="icon-sonarr-edit" title="Edit movie"/> </div> </div> </h1> diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs b/src/UI/Series/Edit/EditSeriesViewTemplate.hbs index 746504cc9..baeab579b 100644 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs +++ b/src/UI/Series/Edit/EditSeriesViewTemplate.hbs @@ -27,7 +27,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Should Sonarr download episodes for this series?"/> + <i class="icon-sonarr-form-info" title="Should Radarr download episodes for this series?"/> </span> </div> </div> diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs index abca7f764..06fb40fe5 100644 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ b/src/UI/Series/Index/EmptyTemplate.hbs @@ -7,9 +7,9 @@ </div> <div class="row"> <div class="col-md-4 col-md-offset-4"> - <a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'> + <a href="/addmovies" class='btn btn-lg btn-block btn-success x-add-series'> <i class='icon-sonarr-add'></i> - Add Series + Add Movie </a> </div> </div> diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index f5f47b983..77f31aac4 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -80,9 +80,9 @@ module.exports = Marionette.Layout.extend({ collapse : true, items : [ { - title : 'Add Series', + title : 'Add Movie', icon : 'icon-sonarr-add', - route : 'addseries' + route : 'addmovies' }, { title : 'Season Pass', diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js index 60d1049cd..5638b5d85 100644 --- a/src/UI/Series/SeriesController.js +++ b/src/UI/Series/SeriesController.js @@ -8,27 +8,30 @@ module.exports = NzbDroneController.extend({ _originalInit : NzbDroneController.prototype.initialize, initialize : function() { - this.route('', this.series); + //this.route('', this.series); this.route('series', this.series); this.route('series/:query', this.seriesDetails); - + this._originalInit.apply(this, arguments); }, series : function() { - this.setTitle('Sonarr'); + this.setTitle('Radarr'); this.showMainRegion(new SeriesIndexLayout()); }, seriesDetails : function(query) { + console.warn(AppLayout.mainRegion) + var series = SeriesCollection.where({ titleSlug : query }); - + if (series.length !== 0) { var targetSeries = series[0]; + this.setTitle(targetSeries.get('title')); this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries })); } else { this.showNotFound(); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs index 9043ad2f5..a10f5d234 100644 --- a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs +++ b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs @@ -23,7 +23,7 @@ </div> <div class="col-sm-2 col-sm-pull-1"> - <input type="number" name="downloadedEpisodesScanInterval" class="form-control" /> + <input type="number" name="downloadedMovieScanInterval" class="form-control" /> </div> </div> </fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs index bc7926439..f5f08a591 100644 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs +++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs @@ -10,7 +10,7 @@ <div class="modal-body remotepath-mapping-modal"> <div class="form-horizontal"> <div> - <p>Use this feature if you have a remotely running Download Client. Sonarr will use the information provided to translate the paths provided by the Download Client API to something Sonarr can access and import.</p> + <p>Use this feature if you have a remotely running Download Client. Radarr will use the information provided to translate the paths provided by the Download Client API to something Radarr can access and import.</p> </div> <div class="form-group"> <label class="col-sm-3 control-label">Host</label> @@ -40,7 +40,7 @@ <label class="col-sm-3 control-label">Local Path</label> <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-sonarr-form-info" title="Path that Sonarr should use to access the same directory remotely." /> + <i class="icon-sonarr-form-info" title="Path that Radarr should use to access the same directory remotely." /> </div> <div class="col-sm-5 col-sm-pull-1"> diff --git a/src/UI/Settings/General/GeneralViewTemplate.hbs b/src/UI/Settings/General/GeneralViewTemplate.hbs index a8493cd8e..e4b3e6b59 100644 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ b/src/UI/Settings/General/GeneralViewTemplate.hbs @@ -100,7 +100,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Open a web browser and navigate to Sonarr homepage on app start. Has no effect if installed as a windows service"/> + <i class="icon-sonarr-form-info" title="Open a web browser and navigate to Radarr homepage on app start. Has no effect if installed as a windows service"/> </span> </div> </div> @@ -114,7 +114,7 @@ <div class="col-sm-1 col-sm-push-4 help-inline"> <i class="icon-sonarr-form-warning" title="Requires restart to take effect"/> - <i class="icon-sonarr-form-info" title="Require Username and Password to access Sonarr"/> + <i class="icon-sonarr-form-info" title="Require Username and Password to access Radarr"/> </div> <div class="col-sm-4 col-sm-pull-1"> @@ -308,7 +308,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Send anonymous information about your browser and which parts of the web interface you use to Sonarr servers. We use this information to prioritize features and browser support. We will NEVER include any personal information or any information that could identify you."/> + <i class="icon-sonarr-form-info" title="Send anonymous information about your browser and which parts of the web interface you use to Radarr servers. We use this information to prioritize features and browser support. We will NEVER include any personal information or any information that could identify you."/> <i class="icon-sonarr-form-warning" title="Requires restart to take effect"/> </span> </div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs index 16bc741ad..3d581b5e4 100644 --- a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs +++ b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs @@ -5,7 +5,7 @@ </div> <div class="modal-body"> <div class="alert alert-info"> - Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.<br/> + Radarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.<br/> For more information on the individual indexers, click on the info buttons. </div> <div class="add-indexer add-thingies"> diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs index 2a3dd5d51..bee701a07 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs @@ -2,7 +2,7 @@ <legend>File Management</legend> <div class="form-group"> - <label class="col-sm-3 control-label">Ignore Deleted Episodes</label> + <label class="col-sm-3 control-label">Ignore Deleted Movies</label> <div class="col-sm-9"> <div class="input-group"> @@ -17,7 +17,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Episodes deleted from disk are automatically unmonitored in Sonarr"/> + <i class="icon-sonarr-form-info" title="Movies deleted from disk are automatically unmonitored in Radarr"/> </span> </div> </div> @@ -39,7 +39,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Should Sonarr automatically upgrade to propers when available?"/> + <i class="icon-sonarr-form-info" title="Should Radarr automatically upgrade to propers when available?"/> </span> </div> </div> @@ -61,7 +61,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Extract video information such as resolution, runtime and codec information from files. This requires Sonarr to read parts of the file which may cause high disk or network activity during scans."/> + <i class="icon-sonarr-form-info" title="Extract video information such as resolution, runtime and codec information from files. This requires Radarr to read parts of the file which may cause high disk or network activity during scans."/> </span> </div> </div> diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js index 916a15aed..2f6320ac8 100644 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js @@ -26,10 +26,10 @@ var view = Marionette.ItemView.extend({ }, _parseNamingModel : function() { - var standardFormat = this.namingModel.get('standardEpisodeFormat'); + var standardFormat = this.namingModel.get('standardMovieFormat'); - var includeSeriesTitle = standardFormat.match(/\{Series[-_. ]Title\}/i); - var includeEpisodeTitle = standardFormat.match(/\{Episode[-_. ]Title\}/i); + var includeSeriesTitle = false;//standardFormat.match(/\{Series[-_. ]Title\}/i); + var includeEpisodeTitle = false;//standardFormat.match(/\{Episode[-_. ]Title\}/i); var includeQuality = standardFormat.match(/\{Quality[-_. ]Title\}/i); var numberStyle = standardFormat.match(/s?\{season(?:\:0+)?\}[ex]\{episode(?:\:0+)?\}/i); var replaceSpaces = standardFormat.indexOf(' ') === -1; @@ -115,4 +115,4 @@ var view = Marionette.ItemView.extend({ } }); -module.exports = AsModelBoundView.call(view); \ No newline at end of file +module.exports = AsModelBoundView.call(view); diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs index 06429a722..0b6e7f0b5 100644 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs @@ -1,5 +1,5 @@ <div class="form-group"> - <label class="col-sm-3 control-label">Include Series Title</label> + {{!--<label class="col-sm-3 control-label">Include Series Title</label> <div class="col-sm-9"> <div class="input-group"> @@ -36,7 +36,7 @@ </label> </div> </div> -</div> +</div>--}} <div class="form-group"> <label class="col-sm-3 control-label">Include Quality</label> @@ -88,7 +88,7 @@ </div> </div> -<div class="form-group"> +{{!--<div class="form-group"> <label class="col-sm-3 control-label">Numbering Style</label> <div class="col-sm-9"> @@ -99,4 +99,4 @@ <option value="s{season:00}e{episode:00}">s01e05</option> </select> </div> -</div> +</div>--}} diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 71e4df4f8..5496fb784 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -19,7 +19,9 @@ module.exports = (function() { namingTokenHelper : '.x-naming-token-helper', multiEpisodeStyle : '.x-multi-episode-style', seriesFolderExample : '.x-series-folder-example', - seasonFolderExample : '.x-season-folder-example' + seasonFolderExample : '.x-season-folder-example', + movieExample : '.x-movie-example', + movieFolderExample : '.x-movie-folder-example' }, events : { "change .x-rename-episodes" : '_setFailedDownloadOptionsVisibility', @@ -58,6 +60,8 @@ module.exports = (function() { this.ui.animeMultiEpisodeExample.html(this.namingSampleModel.get('animeMultiEpisodeExample')); this.ui.seriesFolderExample.html(this.namingSampleModel.get('seriesFolderExample')); this.ui.seasonFolderExample.html(this.namingSampleModel.get('seasonFolderExample')); + this.ui.movieExample.html(this.namingSampleModel.get('movieExample')); + this.ui.movieFolderExample.html(this.namingSampleModel.get('movieFolderExample')); }, _addToken : function(e) { e.preventDefault(); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs index 361954d70..615f95b1a 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs @@ -1,8 +1,8 @@ <fieldset> - <legend>Episode Naming</legend> + <legend>Movie Naming</legend> <div class="form-group"> - <label class="col-sm-3 control-label">Rename Episodes</label> + <label class="col-sm-3 control-label">Rename Movies</label> <div class="col-sm-8"> <div class="input-group"> @@ -18,7 +18,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-warning" title="Sonarr will use the existing file name if set to no"/> + <i class="icon-sonarr-form-warning" title="Radarr will use the existing file name if set to no"/> </span> </div> </div> @@ -51,25 +51,23 @@ <div class="basic-setting x-basic-naming"></div> <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Standard Episode Format</label> + <label class="col-sm-3 control-label">Standard Movie Format</label> <div class="col-sm-1 col-sm-push-8 help-inline"> <i class="icon-sonarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-sonarr-form-info-link"/></a> + <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-sonarr-form-info-link"/></a> <!-- TODO: Update wiki link --> </div> <div class="col-sm-8 col-sm-pull-1"> <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="standardEpisodeFormat" data-onkeyup="true" /> + <input type="text" class="form-control naming-format" name="standardMovieFormat" data-onkeyup="true" /> <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-sonarr-add"></i> </button> <ul class="dropdown-menu"> - {{> SeriesTitleNamingPartial}} - {{> SeasonNamingPartial}} - {{> EpisodeNamingPartial}} - {{> EpisodeTitleNamingPartial}} + {{> MovieTitleNamingPartial}} + {{> ReleaseYearNamingPartial}} {{> QualityNamingPartial}} {{> MediaInfoNamingPartial}} {{> ReleaseGroupNamingPartial}} @@ -81,7 +79,7 @@ </div> </div> - <div class="form-group advanced-setting"> + {{!--<div class="form-group advanced-setting"> <label class="col-sm-3 control-label">Daily Episode Format</label> <div class="col-sm-1 col-sm-push-8 help-inline"> @@ -111,9 +109,9 @@ </div> </div> </div> - </div> + </div>--}} - <div class="form-group advanced-setting"> + {{!--<div class="form-group advanced-setting"> <label class="col-sm-3 control-label">Anime Episode Format</label> <div class="col-sm-1 col-sm-push-8 help-inline"> @@ -144,31 +142,32 @@ </div> </div> </div> - </div> + </div>--}} <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Series Folder Format</label> + <label class="col-sm-3 control-label">Movie Folder Format</label> <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-sonarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new series."></i> + <i class="icon-sonarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new movie."></i> </div> <div class="col-sm-8 col-sm-pull-1"> <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="seriesFolderFormat" data-onkeyup="true"/> + <input type="text" class="form-control naming-format" name="movieFolderFormat" data-onkeyup="true"/> <div class="input-group-btn btn-group x-naming-token-helper"> <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> <i class="icon-sonarr-add"></i> </button> <ul class="dropdown-menu"> - {{> SeriesTitleNamingPartial}} + {{> MovieTitleNamingPartial}} + {{> ReleaseYearNamingPartial}} </ul> </div> </div> </div> </div> - <div class="form-group"> + {{!--<div class="form-group"> <label class="col-sm-3 control-label">Season Folder Format</label> <div class="col-sm-8"> @@ -186,9 +185,9 @@ </div> </div> </div> - </div> + </div>--}} - <div class="x-naming-options"> + {{!--<div class="x-naming-options"> <div class="form-group"> <label class="col-sm-3 control-label">Multi-Episode Style</label> @@ -203,17 +202,17 @@ </select> </div> </div> - </div> + </div>--}} <div class="form-group"> - <label class="col-sm-3 control-label">Single Episode Example</label> + <label class="col-sm-3 control-label">Movie Example</label> <div class="col-sm-8"> - <p class="form-control-static x-single-episode-example naming-example"></p> + <p class="form-control-static x-movie-example naming-example"></p> </div> </div> - <div class="form-group"> + {{!--<div class="form-group"> <label class="col-sm-3 control-label">Multi-Episode Example</label> <div class="col-sm-8"> @@ -242,21 +241,21 @@ <div class="col-sm-8"> <p class="form-control-static x-anime-multi-episode-example naming-example"></p> </div> - </div> + </div>--}} <div class="form-group"> - <label class="col-sm-3 control-label">Series Folder Example</label> + <label class="col-sm-3 control-label">Movie Folder Example</label> <div class="col-sm-8"> - <p class="form-control-static x-series-folder-example naming-example"></p> + <p class="form-control-static x-movie-folder-example naming-example"></p> </div> </div> - <div class="form-group"> + {{!--<div class="form-group"> <label class="col-sm-3 control-label">Season Folder Example</label> <div class="col-sm-8"> <p class="form-control-static x-season-folder-example naming-example"></p> </div> - </div> + </div>--}} </fieldset> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/MovieTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/MovieTitleNamingPartial.hbs new file mode 100644 index 000000000..916416fdb --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/MovieTitleNamingPartial.hbs @@ -0,0 +1,11 @@ +<li class="dropdown-submenu"> + <a href="#" tabindex="-1" data-token="Movie Title">Movie Title</a> + <ul class="dropdown-menu"> + <li><a href="#" data-token="Movie Title">Movie Title</a></li> + <li><a href="#" data-token="Movie.Title">Movie.Title</a></li> + <li><a href="#" data-token="Movie_Title">Movie_Title</a></li> + <li><a href="#" data-token="Movie CleanTitle">Movie CleanTitle</a></li> + <li><a href="#" data-token="Movie.CleanTitle">Movie.CleanTitle</a></li> + <li><a href="#" data-token="Movie_CleanTitle">Movie_CleanTitle</a></li> + </ul> +</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs new file mode 100644 index 000000000..0a4153d66 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs @@ -0,0 +1 @@ +<li><a href="#" data-token="Release Year">Release Year</a></li> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs index 2d870c1ae..6342dae47 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs @@ -28,7 +28,7 @@ <label class="col-sm-3 control-label">File chmod mask</label> <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-sonarr-form-info" title="Octal, applied to media files when imported/renamed by Sonarr"/> + <i class="icon-sonarr-form-info" title="Octal, applied to media files when imported/renamed by Radarr"/> </div> <div class="col-sm-4 col-sm-pull-1"> @@ -40,7 +40,7 @@ <label class="col-sm-3 control-label">Folder chmod mask</label> <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-sonarr-form-info" title="Octal, applied to series/season folders created by Sonarr"/> + <i class="icon-sonarr-form-info" title="Octal, applied to series/season folders created by Radarr"/> </div> <div class="col-sm-4 col-sm-pull-1"> diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs index c78c7393a..c0784c942 100644 --- a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs +++ b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs @@ -2,7 +2,7 @@ <legend>Folders</legend> <div class="form-group"> - <label class="col-sm-3 control-label">Create empty series folders</label> + <label class="col-sm-3 control-label">Create empty movie folders</label> <div class="col-sm-9"> <div class="input-group"> @@ -18,7 +18,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Create missing series folders during disk scan"/> + <i class="icon-sonarr-form-info" title="Create missing movie folders during disk scan"/> </span> </div> </div> @@ -46,7 +46,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-sonarr-form-info" title="Use when drone is unable to detect free space from your series root folder"/> + <i class="icon-sonarr-form-info" title="Use when drone is unable to detect free space from your movies root folder"/> </span> </div> </div> @@ -71,7 +71,7 @@ <span class="help-inline-checkbox"> <i class="icon-sonarr-form-info" title="Use Hardlinks when trying to copy files from torrents that are still being seeded"/> - <i class="icon-sonarr-form-warning" title="Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Sonarr's rename function as a work around."/> + <i class="icon-sonarr-form-warning" title="Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Radarr's rename function as a work around."/> </span> </div> </div> diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs index cae0f2447..c19d10e5c 100644 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs @@ -40,6 +40,6 @@ </div> <div class="col-sm-1 help-inline"> - <i class="icon-sonarr-form-info" title="Once this quality is reached Sonarr will no longer download episodes"/> + <i class="icon-sonarr-form-info" title="Once this quality is reached Radarr will no longer download episodes"/> </div> </div> diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index ae5c1ec8c..4392967df 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -2,6 +2,7 @@ var vent = require('vent'); var AppLayout = require('../../AppLayout'); var Marionette = require('marionette'); var EditSeriesView = require('../../Series/Edit/EditSeriesView'); +var EditMovieView = require('../../Movies/Edit/EditMovieView'); var DeleteSeriesView = require('../../Series/Delete/DeleteSeriesView'); var EpisodeDetailsLayout = require('../../Episode/EpisodeDetailsLayout'); var HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout'); @@ -9,6 +10,7 @@ var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView'); var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout'); var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); +var MoviesDetailsLayout = require('../../Movies/Details/MoviesDetailsLayout'); module.exports = Marionette.AppRouter.extend({ initialize : function() { @@ -17,8 +19,10 @@ module.exports = Marionette.AppRouter.extend({ vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); + vent.on(vent.Commands.EditMovieCommand, this._editMovie, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); + vent.on(vent.Commands.ShowMovieDetails, this._showMovie, this); vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); @@ -48,6 +52,11 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.modalRegion.show(view); }, + _editMovie : function(options) { + var view = new EditMovieView({ model : options.movie }); + AppLayout.modalRegion.show(view); + }, + _deleteSeries : function(options) { var view = new DeleteSeriesView({ model : options.series }); AppLayout.modalRegion.show(view); @@ -62,6 +71,15 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.modalRegion.show(view); }, + _showMovie : function(options) { + var view = new MoviesDetailsLayout({ + model : options.movie, + hideSeriesLink : options.hideSeriesLink, + openingTab : options.openingTab + }); + AppLayout.modalRegion.show(view); + }, + _showHistory : function(options) { var view = new HistoryDetailsLayout({ model : options.model }); AppLayout.modalRegion.show(view); @@ -90,4 +108,4 @@ module.exports = Marionette.AppRouter.extend({ _closeFileBrowser : function() { AppLayout.modalRegion2.closeModal(); } -}); \ No newline at end of file +}); diff --git a/src/UI/Shared/NzbDroneController.js b/src/UI/Shared/NzbDroneController.js index a97dea369..4eae5c9a6 100644 --- a/src/UI/Shared/NzbDroneController.js +++ b/src/UI/Shared/NzbDroneController.js @@ -16,10 +16,10 @@ module.exports = Marionette.AppRouter.extend({ setTitle : function(title) { title = title; - if (title === 'Sonarr') { - document.title = 'Sonarr'; + if (title === 'Radarr') { + document.title = 'Radarr'; } else { - document.title = title + ' - Sonarr'; + document.title = title + ' - Radarr'; } if (window.NzbDrone.Analytics && window.Piwik) { @@ -41,7 +41,7 @@ module.exports = Marionette.AppRouter.extend({ var label = window.location.pathname === window.NzbDrone.UrlBase + '/system/updates' ? 'Reload' : 'View Changes'; Messenger.show({ - message : 'Sonarr has been updated', + message : 'Radarr has been updated', hideAfter : 0, id : 'sonarrUpdated', actions : { @@ -64,4 +64,4 @@ module.exports = Marionette.AppRouter.extend({ AppLayout.mainRegion.show(view); } } -}); \ No newline at end of file +}); diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js index 6f6833ed2..a2c9bc0d9 100644 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js @@ -67,4 +67,4 @@ module.exports = Marionette.ItemView.extend({ _removeSortIcon : function() { this.ui.icon.removeClass('icon-sonarr-sort-asc icon-sonarr-sort-desc'); } -}); \ No newline at end of file +}); diff --git a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs b/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs index c3e5971de..e14f181e3 100644 --- a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs +++ b/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs @@ -2,11 +2,16 @@ <legend>More Info</legend> <dl class="dl-horizontal info"> - <dt>Home page</dt> - <dd><a href="https://sonarr.tv/">sonarr.tv</a></dd> + <dt>Discord</dt> + <dd><a href="https://discord.gg/AD3UP37">Radarr on Discord</a> + + <dt>Reddit</dt> + <dd><a href="https://www.reddit.com/r/radarr/">Radarr Subreddit</a> + {{!--<dt>Home page</dt> + <dd><a href="https://radarr.tdb/">radarr.tdb</a></dd> <dt>Wiki</dt> - <dd><a href="https://wiki.sonarr.tv/">wiki.sonarr.tv</a></dd> + <dd><a href="https://wiki.radarr.tdb/">wiki.radarr.tdb</a></dd> <dt>Forums</dt> <dd><a href="https://forums.sonarr.tv/">forums.sonarr.tv</a></dd> @@ -15,14 +20,13 @@ <dd><a href="https://twitter.com/sonarrtv">@sonarrtv</a></dd> <dt>IRC</dt> - <dd><a href="irc://irc.freenode.net/#sonarr">#sonarr on Freenode</a> or (<a href="http://webchat.freenode.net/?channels=#sonarr">webchat</a>)</dd> + <dd><a href="irc://irc.freenode.net/#sonarr">#sonarr on Freenode</a> or (<a href="http://webchat.freenode.net/?channels=#sonarr">webchat</a>)</dd>--}} <dt>Source</dt> - <dd><a href="https://github.com/Sonarr/Sonarr/">github.com/Sonarr/Sonarr</a></dd> + <dd><a href="https://github.com/Radarr/Radarr">Radarr on Github</a></dd> <dt>Feature Requests</dt> - <dd><a href="https://forums.sonarr.tv/">forums.sonarr.tv</a></dd> - <dd><a href="https://github.com/Sonarr/Sonarr/issues">github.com/Sonarr/Sonarr/issues</a> <b>(Please post issues on the forum first and not on github)</b></dd> + <!--<dd><a href="https://forums.sonarr.tv/">forums.sonarr.tv</a></dd>--> + <dd><a href="https://github.com/Radarr/Radarr/issues">Github Issues</a></dd> </dl> </fieldset> - diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js index d0c71ca09..d0ec5f8a9 100644 --- a/src/UI/System/SystemLayout.js +++ b/src/UI/System/SystemLayout.js @@ -131,7 +131,7 @@ module.exports = Marionette.Layout.extend({ }); Messenger.show({ - message : 'Sonarr will shutdown shortly', + message : 'Radarr will shutdown shortly', type : 'info' }); }, @@ -143,8 +143,8 @@ module.exports = Marionette.Layout.extend({ }); Messenger.show({ - message : 'Sonarr will restart shortly', + message : 'Radarr will restart shortly', type : 'info' }); } -}); \ No newline at end of file +}); diff --git a/src/UI/Wanted/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js index 3adb4876b..e6a8ce33d 100644 --- a/src/UI/Wanted/Missing/MissingLayout.js +++ b/src/UI/Wanted/Missing/MissingLayout.js @@ -133,7 +133,7 @@ module.exports = Marionette.Layout.extend({ { title : 'Rescan Drone Factory Folder', icon : 'icon-sonarr-refresh', - command : 'downloadedepisodesscan', + command : 'downloadedMovieScan', properties : { sendUpdates : true } }, { diff --git a/src/UI/index.html b/src/UI/index.html index 94ebba2af..10e77adef 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -1,7 +1,7 @@ <!doctype html> <html> <head> - <title>Sonarr + Radarr @@ -22,10 +22,12 @@ + + @@ -38,7 +40,7 @@ - +
@@ -71,7 +73,7 @@
diff --git a/src/UI/login.html b/src/UI/login.html index 487e62680..e956bbd4c 100644 --- a/src/UI/login.html +++ b/src/UI/login.html @@ -1,7 +1,7 @@ - Sonarr - Login + Radarr - Login @@ -27,7 +27,7 @@