diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 000000000..8139cef01
Binary files /dev/null and b/.DS_Store differ
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..48583b0bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,9 +127,14 @@ bin
obj
output/*
+#Packages
+Radarr_*/
+Radarr_*.zip
+Radarr_*.gz
#OS X metadata files
._*
+.DS_Store
_start
_temp_*/**/*
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/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 @@
+
+
\ 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/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..28736e964
--- /dev/null
+++ b/package.sh
@@ -0,0 +1,67 @@
+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 Radarr_OSX_$VERSION | ./7za.exe a -si Radarr_OSX_$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
+zip -r Radarr_OSX_$VERSION.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..00b078e18 100644
--- a/readme.md
+++ b/readme.md
@@ -1,27 +1,45 @@
-# Sonarr #
+# Radarr [](https://travis-ci.org/galli-leo/Radarr)#
+This fork of Sonarr aims to turn it into something like Couchpotato.
-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.
+## 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.
+
+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 +55,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/.DS_Store b/src/.DS_Store
new file mode 100644
index 000000000..cbd549c7f
Binary files /dev/null and b/src/.DS_Store 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/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..291879a44 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
+ 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/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj
index 4ade4bcdf..455cc845a 100644
--- a/src/NzbDrone.Api/NzbDrone.Api.csproj
+++ b/src/NzbDrone.Api/NzbDrone.Api.csproj
@@ -231,8 +231,11 @@
+
+
+
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/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..b1924629d
--- /dev/null
+++ b/src/NzbDrone.Api/Series/MovieResource.cs
@@ -0,0 +1,192 @@
+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 List Images { get; set; }
+ public string Website { get; set; }
+
+ public string RemotePoster { get; set; }
+ public int Year { 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; }
+
+ //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;
+
+ return new MovieResource
+ {
+ Id = model.Id,
+ TmdbId = model.TmdbId,
+ Title = model.Title,
+ //AlternateTitles
+ SortTitle = model.SortTitle,
+ InCinemas = model.InCinemas,
+ //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,
+
+ 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
+ };
+ }
+
+ 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,
+ //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..bcf03c4ee 100644
--- a/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs
+++ b/src/NzbDrone.Common/Cloud/SonarrCloudRequestBuilder.cs
@@ -6,6 +6,8 @@ namespace NzbDrone.Common.Cloud
{
IHttpRequestBuilderFactory Services { get; }
IHttpRequestBuilderFactory SkyHookTvdb { get; }
+ IHttpRequestBuilderFactory TMDB { get; }
+ IHttpRequestBuilderFactory TMDBSingle { get; }
}
public class SonarrCloudRequestBuilder : ISonarrCloudRequestBuilder
@@ -18,10 +20,20 @@ namespace NzbDrone.Common.Cloud
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/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..893706f3d 100644
--- a/src/NzbDrone.Common/Extensions/PathExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs
@@ -13,7 +13,7 @@ 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;
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.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/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/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs
index fa6d8a914..2eeaf6463 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);
@@ -303,12 +303,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 +323,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/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/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs
index d86653478..e2d171ee4 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 = true;
+ 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,36 @@ 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.Info("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;
}
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..8ff6105d7 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.ParsedEpisodeInfo.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.ParsedEpisodeInfo.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/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/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs
index 39174d3b8..6312672a6 100644
--- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs
+++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs
@@ -31,6 +31,50 @@ namespace NzbDrone.Core.Download.Clients.Deluge
_proxy = proxy;
}
+ protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink)
+ {
+ var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
+
+ if (!Settings.TvCategory.IsNullOrWhiteSpace())
+ {
+ _proxy.SetLabel(actualHash, Settings.TvCategory, 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(RemoteMovie remoteEpisode, string hash, string filename, byte[] fileContent)
+ {
+ var actualHash = _proxy.AddTorrentFromFile(filename, fileContent, Settings);
+
+ if (!Settings.TvCategory.IsNullOrWhiteSpace())
+ {
+ _proxy.SetLabel(actualHash, Settings.TvCategory, 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)
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs
index b5fd1153e..73bc0212a 100644
--- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs
+++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
Host = "localhost";
Port = 8112;
Password = "deluge";
- TvCategory = "tv-sonarr";
+ TvCategory = "movie-radarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@@ -40,7 +40,7 @@ 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")]
+ [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(DelugePriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
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/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs
index 5b6d756cc..c358040f2 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;
diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetSettings.cs
index cbd104964..5aeef86b4 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;
}
@@ -44,7 +46,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
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; }
+ 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..4d4dc47db 100644
--- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs
+++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs
@@ -71,6 +71,46 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return hash;
}
+ protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink)
+ {
+ _proxy.AddTorrentFromUrl(magnetLink, Settings);
+
+ if (Settings.TvCategory.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);
+ }*/ //TODO: Maybe reimplement for movies
+
+ return hash;
+ }
+
+ protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, Byte[] fileContent)
+ {
+ _proxy.AddTorrentFromFile(filename, fileContent, Settings);
+
+ if (Settings.TvCategory.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);
+ }*/
+
+ return hash;
+ }
+
public override string Name => "qBittorrent";
public override IEnumerable GetItems()
@@ -210,7 +250,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
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 +260,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/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs
index fa6908f2c..d139d244b 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";
+ TvCategory = "movie-radarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
@@ -37,9 +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")]
+ [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; }
+ //Todo: update this shit.
[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; }
diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs
index 64a5e23de..bc32aedb1 100644
--- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs
+++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs
@@ -32,12 +32,27 @@ 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);
- 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(fileContents, filename, category, priority, Settings);
- var response = _proxy.DownloadNzb(fileContent, 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())
{
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
index 041708a93..b456f828c 100644
--- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
@@ -37,6 +37,37 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
_rTorrentDirectoryValidator = rTorrentDirectoryValidator;
}
+ 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);
+ 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.
+ var tries = 10;
+ var retryDelay = 500;
+ if (WaitForTorrent(hash, tries, retryDelay))
+ {
+ return hash;
+ }
+ else
+ {
+ _logger.Warn("rTorrent could not resolve magnet within {0} seconds, download may remain stuck: {1}.", tries * retryDelay / 1000, magnetLink);
+
+ return hash;
+ }
+ }
+
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
@@ -68,6 +99,32 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
}
}
+ protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent)
+ {
+ _proxy.AddTorrentFromFile(filename, fileContent, Settings);
+
+ var tries = 2;
+ var retryDelay = 100;
+ if (WaitForTorrent(hash, tries, retryDelay))
+ {
+ _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
+
+ //SetPriority(remoteEpisode, hash);
+ SetDownloadDirectory(hash);
+
+ _proxy.StartTorrent(hash, Settings);
+
+ return hash;
+ }
+ else
+ {
+ _logger.Debug("rTorrent could not add file");
+
+ RemoveItem(hash, true);
+ throw new ReleaseDownloadException(remoteMovie.Release, "Downloading torrent failed");
+ }
+ }
+
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
@@ -96,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
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()
{
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
index 81715246c..570392395 100644
--- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
Host = "localhost";
Port = 8080;
UrlBase = "RPC2";
- TvCategory = "tv-sonarr";
+ TvCategory = "movies-radarr";
OlderTvPriority = (int)RTorrentPriority.Normal;
RecentTvPriority = (int)RTorrentPriority.Normal;
}
diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs
index c4fbe11a2..4e2bebe59 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,19 +94,31 @@ namespace NzbDrone.Core.Download
return;
}
+
var series = _parsingService.GetSeries(trackedDownload.DownloadItem.Title);
if (series == null)
{
if (historyItem != null)
{
- series = _seriesService.GetSeries(historyItem.SeriesId);
+ //series = _seriesService.GetSeries(historyItem.SeriesId);
}
if (series == null)
{
- trackedDownload.Warn("Series title mismatch, automatic import is not possible.");
- return;
+ var movie = _parsingService.GetMovie(trackedDownload.DownloadItem.Title);
+
+ if (movie == null)
+ {
+ movie = _movieService.GetMovie(historyItem.MovieId);
+
+ if (movie == null)
+ {
+ trackedDownload.Warn("Movie title mismatch, automatic import is not possible.");
+ }
+ }
+ //trackedDownload.Warn("Series title mismatch, automatic import is not possible.");
+ //return;
}
}
}
@@ -111,29 +129,59 @@ 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);
-
- if (importResults.Empty())
+ if (trackedDownload.RemoteMovie.Movie != null)
{
- trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
- return;
+ var importResults = _downloadedMovieImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem);
+
+ if (importResults.Empty())
+ {
+ trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
+ return;
+ }
+
+ if (importResults.Count(c => c.Result == ImportResultType.Imported) >= 1)
+ {
+ trackedDownload.State = TrackedDownloadStage.Imported;
+ _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
+ return;
+ }
+
+ if (importResults.Any(c => c.Result != ImportResultType.Imported))
+ {
+ var statusMessages = importResults
+ .Where(v => v.Result != ImportResultType.Imported)
+ .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
+ .ToArray();
+
+ trackedDownload.Warn(statusMessages);
+ }
}
-
- if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
+ else
{
- trackedDownload.State = TrackedDownloadStage.Imported;
- _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
- return;
- }
+ var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
- if (importResults.Any(c => c.Result != ImportResultType.Imported))
- {
- var statusMessages = importResults
- .Where(v => v.Result != ImportResultType.Imported)
- .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
- .ToArray();
+ if (importResults.Empty())
+ {
+ trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
+ return;
+ }
- trackedDownload.Warn(statusMessages);
+ if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
+ {
+ trackedDownload.State = TrackedDownloadStage.Imported;
+ _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
+ return;
+ }
+
+ if (importResults.Any(c => c.Result != ImportResultType.Imported))
+ {
+ var statusMessages = importResults
+ .Where(v => v.Result != ImportResultType.Imported)
+ .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.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/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/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..adadebdee 100644
--- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs
+++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs
@@ -32,53 +32,112 @@ 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 (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/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/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/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/BitMeTv/BitMeTvRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs
index e7966dcba..0c631af39 100644
--- a/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.cs
+++ b/src/NzbDrone.Core/Indexers/BitMeTv/BitMeTvRequestGenerator.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.BitMeTv
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/BroadcastheNet/BroadcastheNetRequestGenerator.cs b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs
index b5a39a94c..579761d89 100644
--- a/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs
+++ b/src/NzbDrone.Core/Indexers/BroadcastheNet/BroadcastheNetRequestGenerator.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using NzbDrone.Common.Http;
using NzbDrone.Core.IndexerSearch.Definitions;
+using System;
namespace NzbDrone.Core.Indexers.BroadcastheNet
{
@@ -189,5 +190,10 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
yield return new IndexerRequest(builder.Build());
}
}
+
+ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
+ {
+ return new IndexerPageableRequestChain();
+ }
}
}
diff --git a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs
index 19585dad5..ad207eb38 100644
--- a/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs
+++ b/src/NzbDrone.Core/Indexers/Fanzub/FanzubRequestGenerator.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -84,5 +85,10 @@ namespace NzbDrone.Core.Indexers.Fanzub
{
return RemoveCharactersRegex.Replace(title, "");
}
+
+ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
+ {
+ return new IndexerPageableRequestChain();
+ }
}
}
diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs
index dacb87490..2b377a9fd 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;
@@ -128,5 +129,10 @@ namespace NzbDrone.Core.Indexers.HDBits
yield return new IndexerRequest(request);
}
+
+ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
+ {
+ return new IndexerPageableRequestChain();
+ }
}
}
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..e3788d6a1 100644
--- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs
+++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Indexers.Newznab
yield return GetDefinition("NZBFinder.ws", GetSettings("https://nzbfinder.ws", 5010, 5030, 5040, 5045));
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", 2000));
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 +106,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", "imdb" }.Any(v => capabilities.SupportedMovieSearchParamters.Contains(v)) &&
+ new[] { "imdbtitle", "imdbyear" }.All(v => capabilities.SupportedMovieSearchParamters.Contains(v)))
{
return null;
}
diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabCapabilities.cs
index 11e73da34..1385eaa1a 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[] SupportedMovieSearchParamters { 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" };
+ SupportedMovieSearchParamters = new[] { "q", "imdb", "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..c7343513c 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.SupportedMovieSearchParamters = null;
+ }
+ else if (xmlMovieSearch.Attribute("supportedParams") != null)
+ {
+ capabilities.SupportedMovieSearchParamters = 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..518494923 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.SupportedMovieSearchParamters != null &&
+ capabilities.SupportedMovieSearchParamters.Contains("imdb");
+ }
+ }
+
private bool SupportsAggregatedIdSearch
{
get
@@ -108,6 +120,25 @@ namespace NzbDrone.Core.Indexers.Newznab
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;
+ }
+
public virtual IndexerPageableRequestChain GetSearchRequests(SingleEpisodeSearchCriteria searchCriteria)
{
var pageableRequests = new IndexerPageableRequestChain();
diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabSettings.cs
index b33ef566d..d798ec80e 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[] { 2030, 2040, 2050 };
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..983b15a32 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;
@@ -101,5 +102,10 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
yield return new IndexerRequest(url.ToString(), HttpAccept.Rss);
}
+
+ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
+ {
+ return new IndexerPageableRequestChain();
+ }
}
}
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..2c21e408e
--- /dev/null
+++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoParser.cs
@@ -0,0 +1,65 @@
+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;
+
+ 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..f85969d1e
--- /dev/null
+++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs
@@ -0,0 +1,121 @@
+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);
+ requestBuilder.AddQueryParam("user", Settings.User);
+ // requestBuilder.AddQueryParam("imdbid", "tt0076759"); //For now just search for Star Wars.
+ requestBuilder.AddQueryParam("search", "the"); // there has to be movies with 'the' in the title on any indexer
+
+ 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);
+ requestBuilder.AddQueryParam("user", Settings.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..9b8300405
--- /dev/null
+++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoResponse.cs
@@ -0,0 +1,26 @@
+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; }
+ }
+
+}
diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoSettings.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoSettings.cs
new file mode 100644
index 000000000..d0b902f5e
--- /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 = "";
+ }
+
+ [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/TorrentleechRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs
index ebfa73788..51b458c6d 100644
--- a/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.cs
+++ b/src/NzbDrone.Core/Indexers/Torrentleech/TorrentleechRequestGenerator.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.Torrentleech
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/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs
index 8d2649c2d..602edc540 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
@@ -110,5 +112,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/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs
index 571a85288..7aa1eef9f 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
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/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..916c5681b 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,6 +31,8 @@ namespace NzbDrone.Core.MediaFiles
public class DiskScanService :
IDiskScanService,
IHandle,
+ IHandle,
+ IExecute,
IExecute
{
private readonly IDiskProvider _diskProvider;
@@ -39,6 +42,7 @@ namespace NzbDrone.Core.MediaFiles
private readonly ISeriesService _seriesService;
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
private readonly IEventAggregator _eventAggregator;
+ private readonly IMovieService _movieService;
private readonly Logger _logger;
public DiskScanService(IDiskProvider diskProvider,
@@ -48,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles
ISeriesService seriesService,
IMediaFileTableCleanupService mediaFileTableCleanupService,
IEventAggregator eventAggregator,
+ IMovieService movieService,
Logger logger)
{
_diskProvider = diskProvider;
@@ -57,6 +62,7 @@ namespace NzbDrone.Core.MediaFiles
_seriesService = seriesService;
_mediaFileTableCleanupService = mediaFileTableCleanupService;
_eventAggregator = eventAggregator;
+ _movieService = movieService;
_logger = logger;
}
@@ -121,6 +127,64 @@ 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);
+ decisionsStopwatch.Stop();
+ _logger.Trace("Import decisions complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed);
+
+ _importApprovedEpisodes.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 +220,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 +253,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..fcda97d24
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs
@@ -0,0 +1,265 @@
+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;
+
+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 Sonarr: {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.ParseTitle(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);
+ 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);
+
+ 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..27756cf4f 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 120; //2 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..6d766a0e3
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs
@@ -0,0 +1,172 @@
+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)
+ {
+ 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.ParsedEpisodeInfo.ReleaseGroup;
+
+ 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..8bb5e78ea 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();
+ LocalEpisode = new LocalEpisode
+ {
+ Quality = localMovie.Quality,
+ ExistingFile = localMovie.ExistingFile,
+ MediaInfo = localMovie.MediaInfo,
+ ParsedEpisodeInfo = localMovie.ParsedEpisodeInfo,
+ 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..52ffdfa07 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs
@@ -18,6 +18,8 @@ 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, ParsedEpisodeInfo folderInfo, bool sceneSource); //TODO: Needs changing to ParsedMovieInfo!!
List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
}
@@ -53,6 +55,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return GetImportDecisions(videoFiles, series, null, false);
}
+ public List GetImportDecisions(List videoFiles, Movie movie)
+ {
+ return GetImportDecisions(videoFiles, movie, null, false);
+ }
+
public List GetImportDecisions(List videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource)
{
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series);
@@ -70,6 +77,81 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return decisions;
}
+ public List GetImportDecisions(List videoFiles, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource)
+ {
+ 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));
+ }
+
+ return decisions;
+ }
+
+ private ImportDecision GetDecision(string file, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource, bool shouldUseFolderName)
+ {
+ 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);
+ decision = GetDecision(localMovie);
+ }
+ else
+ {
+ decision = GetDecision(localMovie);
+ }
+ }
+
+ else
+ {
+ var localEpisode = new LocalEpisode();
+ localEpisode.Path = file;
+
+ decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file"));
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e, "Couldn't import file. " + file);
+
+ var localEpisode = new LocalEpisode { Path = file };
+ decision = new ImportDecision(localEpisode, 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 +210,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 +291,51 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
}) == 1;
}
+ private bool ShouldUseFolderName(List videoFiles, Movie movie, ParsedEpisodeInfo 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(ParsedEpisodeInfo 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 +347,31 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return fileQuality;
}
+ private bool UseFolderQuality(ParsedEpisodeInfo 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/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..536ea093a 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs
@@ -37,5 +37,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept();
}
+
+ public Decision IsSatisfiedBy(LocalMovie localEpisode)
+ {
+ if (localEpisode.ExistingFile)
+ {
+ _logger.Debug("Existing file, skipping sample check");
+ return Decision.Accept();
+ }
+
+ 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/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
index ca3f68ce2..e3e82daa5 100644
--- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
+++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs
@@ -7,18 +7,24 @@ 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 episodeFile);
+ void Update(MovieFile episodeFile);
+ void Delete(MovieFile episodeFile, 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);
List Get(IEnumerable ids);
@@ -28,12 +34,14 @@ namespace NzbDrone.Core.MediaFiles
{
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 +67,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 +106,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);
@@ -98,5 +130,18 @@ namespace NzbDrone.Core.MediaFiles
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);
+ }
+
}
}
\ 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..dfb753ab6
--- /dev/null
+++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs
@@ -0,0 +1,29 @@
+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 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/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..d6076d3fd
--- /dev/null
+++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/TMDBResources.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
+{
+
+ 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 int 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 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..e0014a728 100644
--- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs
+++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs
@@ -9,21 +9,27 @@ 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;
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;
- public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
+ public SkyHookProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, ITmdbConfigService configService, Logger logger)
{
_httpClient = httpClient;
_requestBuilder = requestBuilder.SkyHookTvdb;
+ _movieBuilder = requestBuilder.TMDB;
+ _configService = configService;
_logger = logger;
}
@@ -57,6 +63,217 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return new Tuple>(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")
+ .AddQueryParam("country", "US")
+ .Build();
+
+ request.AllowAutoRedirect = true;
+ request.SuppressHttpError = true;
+
+ var response = _httpClient.Get(request);
+
+ var resource = response.Resource;
+
+ var movie = new Movie();
+
+ movie.TmdbId = TmdbId;
+ movie.ImdbId = resource.imdb_id;
+ movie.Title = resource.title;
+ movie.TitleSlug = movie.Title.ToLower().Replace(" ", "-");
+ movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title);
+ movie.Overview = resource.overview;
+ movie.Website = resource.homepage;
+ movie.InCinemas = DateTime.Parse(resource.release_date);
+ movie.Year = movie.InCinemas.Value.Year;
+
+ 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);
+ }
+
+ 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 imdbRequest = new HttpRequest("http://www.omdbapi.com/?i=" + ImdbId + "&plot=full&r=json");
+
+ var httpResponse = _httpClient.Get(imdbRequest);
+
+ if (httpResponse.HasHttpError)
+ {
+ if (httpResponse.StatusCode == HttpStatusCode.NotFound)
+ {
+ throw new MovieNotFoundException(ImdbId);
+ }
+ else
+ {
+ throw new HttpException(imdbRequest, httpResponse);
+ }
+ }
+
+ var response = httpResponse.Content;
+
+ dynamic json = JsonConvert.DeserializeObject(response);
+
+ var movie = new Movie();
+
+ movie.Title = json.Title;
+ movie.TitleSlug = movie.Title.ToLower().Replace(" ", "-");
+ movie.Overview = json.Plot;
+ movie.CleanTitle = Parser.Parser.CleanSeriesTitle(movie.Title);
+ string airDateStr = json.Released;
+ DateTime airDate = DateTime.Parse(airDateStr);
+ movie.InCinemas = airDate;
+ movie.Year = airDate.Year;
+ movie.ImdbId = ImdbId;
+ string imdbRating = json.imdbVotes;
+ if (imdbRating == "N/A")
+ {
+ movie.Status = MovieStatusType.Announced;
+ }
+ else
+ {
+ movie.Status = MovieStatusType.Released;
+ }
+ string url = json.Poster;
+ var imdbPoster = new MediaCover.MediaCover(MediaCoverTypes.Poster, url);
+ movie.Images.Add(imdbPoster);
+ string runtime = json.Runtime;
+ int runtimeNum = 0;
+ int.TryParse(runtime.Replace("min", "").Trim(), out runtimeNum);
+ movie.Runtime = runtimeNum;
+
+ return movie;
+ }
+
+ public List SearchForNewMovie(string title)
+ {
+ var lowerTitle = title.ToLower();
+
+ 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();
+ }
+
+ try
+ {
+ return new List { GetMovieInfo(imdbid) };
+ }
+ catch (SeriesNotFoundException)
+ {
+ return new List();
+ }
+ }
+
+ var searchTerm = lowerTitle.Replace("_", "+").Replace(" ", "+");
+
+ var firstChar = searchTerm.First();
+
+ var request = _movieBuilder.Create()
+ .SetSegment("route", "search")
+ .SetSegment("id", "movie")
+ .SetSegment("secondaryRoute", "")
+ .AddQueryParam("query", searchTerm)
+ .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(responseCleaned);
+
+ _logger.Warn("Json object: " + json);
+
+ _logger.Warn("Crash ahead.");*/
+
+ var response = _httpClient.Get(request);
+
+ var movieResults = response.Resource.results;
+
+ var imdbMovies = new List();
+
+ foreach (MovieResult result in movieResults)
+ {
+ var imdbMovie = new Movie();
+ imdbMovie.TmdbId = result.id;
+ try
+ {
+ imdbMovie.SortTitle = result.title;
+ imdbMovie.Title = result.title;
+ string titleSlug = result.title;
+ imdbMovie.TitleSlug = titleSlug.ToLower().Replace(" ", "-");
+ imdbMovie.Year = DateTime.Parse(result.release_date).Year;
+ imdbMovie.Images = new List();
+ 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);
+ continue;
+ }
+
+ imdbMovies.Add(imdbMovie);
+ }
+ catch
+ {
+
+ }
+
+ }
+
+ return imdbMovies;
+ }
+
public List SearchForNewSeries(string title)
{
try
@@ -84,11 +301,15 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}
}
+
+
var httpRequest = _requestBuilder.Create()
.SetSegment("route", "search")
.AddQueryParam("term", title.ToLower().Trim())
.Build();
+
+
var httpResponse = _httpClient.Get>(httpRequest);
return httpResponse.Resource.SelectList(MapSeries);
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 _configurationCache;
+ private readonly IHttpClient _httpClient;
+ private readonly IHttpRequestBuilderFactory _tmdbBuilder;
+
+ public TmdbConfigService(ICacheManager cacheManager, IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder)
+ {
+ _configurationCache = cacheManager.GetCache(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(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 { 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 MovieStatistics();
+ List MovieStatistics(int movieId);
+ }
+
+ public class MovieStatisticsRepository : IMovieStatisticsRepository
+ {
+ private readonly IMainDatabase _database;
+
+ public MovieStatisticsRepository(IMainDatabase database)
+ {
+ _database = database;
+ }
+
+ public List 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();
+
+ return mapper.Query(queryText);
+ }
+
+ public List