Compare commits

...

44 commits

Author SHA1 Message Date
Leonardo Galli
28857e7fac Fixes issue with History table. 2017-01-03 23:59:41 +01:00
Leonardo Galli
6d23fb1b61 Fixes issue with History table not having a movie id field. 2017-01-03 23:57:02 +01:00
Leonardo Galli
ad95fbfd4a Implemented importing movies. This is still in early stages, however it should work pretty well. 2017-01-03 23:18:51 +01:00
Leonardo Galli
d9d8cbacec Fixes package.sh for OSX builds 2017-01-03 17:35:47 +01:00
Leonardo Galli
80f2adad50 Changes name to Radarr in system tray icon. 2017-01-03 17:35:31 +01:00
Leonardo Galli
7a72f4a05b Fix package.sh 2017-01-03 16:29:49 +01:00
Leonardo Galli
6d7ff628d8 Updated package.sh for Travis 2017-01-03 16:15:13 +01:00
Leonardo Galli
e9f9f66b2f Allow Sonarr and Radarr to run together.
Also changes default port of Radarr to 7878.
However, now multiple instances of Radarr can also be run. This should
be fixed in the future.
2017-01-03 16:06:06 +01:00
Leonardo Galli
6ca88f98af Fix package.sh permissions for travis 2017-01-03 16:04:41 +01:00
Leonardo Galli
631cf776f6 Travis now automatically pushes a build to a server. 2017-01-03 15:54:15 +01:00
Leonardo Galli
6ea9b4b94a Added Script for easier packaging. 2017-01-03 14:18:13 +01:00
Leonardo Galli
d835c168d3 Searching for movies directly when adding them is now working. 2017-01-03 13:26:09 +01:00
Leonardo Galli
329786365d Fixed adding multiple movies. 2017-01-03 12:52:09 +01:00
Leonardo Galli
4f6380a73c Automatically downloading the best movie release works now. 2017-01-03 11:59:03 +01:00
Leonardo Galli
cde1217356 Movies now show up in the Queue. 2017-01-02 22:01:11 +01:00
Leonardo Galli
2eedfca78a Movie history now fully implemented. 2017-01-02 21:02:54 +01:00
Leonardo Galli
0d65984991 Fix History for Movie Details page. 2017-01-02 20:41:44 +01:00
Leonardo Galli
2a932fe7e8 First implementation of History for Movies. However, nothing is returned from the Database query misteriously. 2017-01-02 20:15:13 +01:00
Leonardo Galli
0e02171938 Fixes downloading a movie. However, now all downloaders except QBittorrent are non functional until they get fixed. See #14 2017-01-02 19:20:32 +01:00
Leonardo Galli
2a3b0304cb Fixed a few things with displaying the Movie Details Page. Implemented the first round of Searching for and Downloading movie Releases. ATM this is still a bit hacky and alot of things need to be cleaned up. However, one can now manually search for and download (only in qBittorrent) a movie torrent. 2017-01-02 18:05:55 +01:00
Leonardo Galli
16e35f68bb Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2017-01-02 13:21:11 +01:00
Leonardo Galli
74b1c846a5 Fixed css for movies. 2017-01-02 13:20:50 +01:00
Leonardo Galli
1f930c18e4 Update readme.md 2017-01-02 11:45:11 +01:00
Leonardo Galli
d9e60eff6b Update .travis.yml
Removed tests since they take far too long
2017-01-02 11:25:28 +01:00
Leonardo Galli
097982334c Update .travis.yml
Fixed syntax for test.sh
2017-01-02 10:40:09 +01:00
Leonardo Galli
be6e6b910e Update .travis.yml
symlinking node was already done in apt-get and therefore resulted in an error.
2017-01-02 10:32:23 +01:00
Leonardo Galli
31b9ec1116 Create .travis.yml
Should enable automatically testing.
2017-01-02 10:27:59 +01:00
Leonardo Galli
ff6c3b70d3 Merge pull request #12 from fedoranimus/develop
Fix template references and 'movie' strings
2017-01-02 10:12:49 +01:00
Tim Turner
0fd0b31a60 Fix template references and 'movie' strings 2016-12-31 14:42:32 -05:00
Leonardo Galli
76e6ebc63c Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2016-12-30 11:58:43 +01:00
Leonardo Galli
2ea35adb98 Fixed issue with Homepage movies not loading correctly. 2016-12-30 11:58:39 +01:00
Leonardo Galli
782f63f510 Update readme.md 2016-12-30 11:34:11 +01:00
Leonardo Galli
c874122fc0 Use different folder to store sqlite database. Fixes #10. 2016-12-30 11:25:25 +01:00
Leonardo Galli
2efda4933d Changed the name in the UI to Radarr. 2016-12-29 17:44:51 +01:00
Leonardo Galli
b7c70d750a Movies should now show on the main page. However, a lot has to be done to the detail controller before it is really going to work. 2016-12-29 17:38:54 +01:00
Leonardo Galli
5ebfac6cc8 First implementation of custom database table for movies.Some things are not yet working quite well (e.g. search clears when movies are added.). Also movies cannot yet be looked up! 2016-12-29 16:04:01 +01:00
Leonardo Galli
74ca6149e3 Merge branch 'develop' of https://github.com/galli-leo/Radarr into develop 2016-12-29 14:07:43 +01:00
Leonardo Galli
40d7590f80 First implementation of completely rewriting the way Radarr handles movies. Searching for new movies is now mostly feature complete. 2016-12-29 14:06:51 +01:00
Leonardo Galli
2cb27240dc Update readme.md 2016-12-28 22:37:06 +01:00
Leonardo Galli
837c2683df Update readme.md 2016-12-28 18:43:31 +01:00
Leonardo Galli
0b278c7db8 Searching for movie now works with downloading. They also get imported fine.
Additionally, a whole series (or movie in this case) can now be
downloaded manually.
Note: It probably won't start downloading missed releases. Only manually
clicking search for is working ATM.
2016-12-28 17:13:18 +01:00
Leonardo Galli
0b765d10fe Updated some text to say movies instead of series 2016-12-27 18:48:59 +01:00
Leonardo Galli
20dbdfb344 Added first iteration of adding movies.
Currently working:
- Searching for new Movies on IMDb (very hacky)
- Adding movie as a series with one season and episode (very hacky)
- Rarbg.to indexer. (somewhat hacky)

TODO:
- Tweak release specifications so that they do not cause exceptions.
- Add Movie struct so that searching for ones is not so hacky.
- rework movies UI.
2016-12-27 18:31:38 +01:00
Leonardo Galli
426448ed98 Update readme.md 2016-12-25 12:43:05 +01:00
260 changed files with 9144 additions and 231 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

4
.gitignore vendored
View file

@ -127,9 +127,13 @@ bin
obj
output/*
#Packages
Radarr_*/
Radarr_*.zip
#OS X metadata files
._*
.DS_Store
_start
_temp_*/**/*

12
.travis.yml Normal file
View file

@ -0,0 +1,12 @@
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

View file

@ -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)

50
package.sh Normal file
View file

@ -0,0 +1,50 @@
if [ $# -eq 0 ]; then
if [ "$TRAVIS_PULL_REQUEST" != false ]; then
echo "Need to supply version argument" && exit;
fi
fi
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/Sonarr.app/Contents/MacOS/Sonarr > $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2
rm $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr
chmod +x $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2
mv $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr2 $outputFolderOsxApp/Sonarr.app/Contents/MacOS/Sonarr >& error.log
cp -r $outputFolder/ Radarr_Windows_$VERSION
cp -r $outputFolderMono/ Radarr_Mono_$VERSION
cp -r $outputFolderOsxApp/ Radarr_OSX_$VERSION
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
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

View file

@ -1,7 +1,20 @@
# Sonarr #
# Radarr [![Build Status](https://travis-ci.org/galli-leo/Radarr.svg?branch=develop)](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 (Note: Movies are currently added as one series with one season and one episode. This will change in the future)
* Manually searching for releases of movies.
* Automatically searching for releases.
* Rarbg.to indexer (Other indexers are coming, I just need to find the right categories)
* Everything that has nothing to do with series from Sonarr should be working as well.
## Planned Features:
* Scanning PreDB to know when a new release is available.
* Fixing the other Indexers.
* Fixing how movies are stored and displayed.
* Importing of Sonarr config.
* New TorrentPotato Indexer.
## Major Features Include: ##
@ -17,6 +30,8 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
* Full support for specials and multi-episode releases
* And a beautiful UI
## Download
The latest precompiled binary versions can be found here: https://github.com/galli-leo/Radarr/releases.
## Configuring Development Environment: ##

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -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<HistoryResource, Core.History.History>("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);
}

View file

@ -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<string, string> 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

View file

@ -26,6 +26,7 @@ namespace NzbDrone.Api.Indexers
private readonly Logger _logger;
private readonly ICached<RemoteEpisode> _remoteEpisodeCache;
private readonly ICached<RemoteMovie> _remoteMovieCache;
public ReleaseModule(IFetchAndParseRss rssFetcherAndParser,
ISearchForNzb nzbSearchService,
@ -49,6 +50,7 @@ namespace NzbDrone.Api.Indexers
PostValidator.RuleFor(s => s.Guid).NotEmpty();
_remoteEpisodeCache = cacheManager.GetCache<RemoteEpisode>(GetType(), "remoteEpisodes");
_remoteMovieCache = cacheManager.GetCache<RemoteMovie>(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<ReleaseResource>();
}
private List<ReleaseResource> 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<ReleaseResource>();
}
private List<ReleaseResource> 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);
}
}

View file

@ -86,6 +86,11 @@ 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;
}
// 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 +124,7 @@ namespace NzbDrone.Api.Indexers
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = remoteEpisode.DownloadAllowed,
DownloadAllowed = downloadAllowed,
//ReleaseWeight
MagnetUrl = torrentInfo.MagnetUrl,

View file

@ -231,8 +231,11 @@
<Compile Include="Series\SeasonResource.cs" />
<Compile Include="SeasonPass\SeasonPassModule.cs" />
<Compile Include="Series\SeriesEditorModule.cs" />
<Compile Include="Series\MovieLookupModule.cs" />
<Compile Include="Series\SeriesLookupModule.cs" />
<Compile Include="Series\MovieModule.cs" />
<Compile Include="Series\SeriesModule.cs" />
<Compile Include="Series\MovieResource.cs" />
<Compile Include="Series\SeriesResource.cs" />
<Compile Include="Series\SeasonStatisticsResource.cs" />
<Compile Include="System\Backup\BackupModule.cs" />

View file

@ -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()
};
}

View file

@ -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<MovieResource>
{
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<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> 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;
}
}
}
}

View file

@ -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<MovieResource, Core.Tv.Movie>,
IHandle<EpisodeImportedEvent>,
IHandle<EpisodeFileDeletedEvent>,
IHandle<MovieUpdatedEvent>,
IHandle<MovieEditedEvent>,
IHandle<MovieDeletedEvent>,
IHandle<MovieRenamedEvent>,
IHandle<MediaCoversUpdatedEvent>
{
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.ImdbId).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<MovieResource> 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<MovieResource> resources, List<MovieStatistics> 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<MovieResource> 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);
}
}
}

View file

@ -0,0 +1,183 @@
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<AlternateTitleResource> 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<MediaCover> Images { 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 string TitleSlug { get; set; }
public string RootFolderPath { get; set; }
public string Certification { get; set; }
public List<string> Genres { get; set; }
public HashSet<int> Tags { get; set; }
public DateTime Added { get; set; }
public AddMovieOptions AddOptions { get; set; }
public Ratings Ratings { 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,
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,
Genres = model.Genres,
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,
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,
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,
Genres = resource.Genres,
Tags = resource.Tags,
Added = resource.Added,
AddOptions = resource.AddOptions,
Ratings = resource.Ratings
};
}
public static Core.Tv.Movie ToModel(this MovieResource resource, Core.Tv.Movie movie)
{
movie.ImdbId = resource.ImdbId;
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<MovieResource> ToResource(this IEnumerable<Core.Tv.Movie> movies)
{
return movies.Select(ToResource).ToList();
}
}
}

View file

@ -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();

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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<DownloadDecision>();
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();
}

View file

@ -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);
}
}

View file

@ -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()

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
sw.Stop();
_announcer.ElapsedTime(sw.Elapsed);
}
}

View file

@ -76,6 +76,8 @@ namespace NzbDrone.Core.Datastore
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId);
Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
.Ignore(f => f.Path)
.Relationships.AutoMapICollectionOrComplexProperties()
@ -84,6 +86,21 @@ namespace NzbDrone.Core.Datastore
query: (db, parent) => db.Query<Episode>().Where(c => c.EpisodeFileId == parent.Id).ToList())
.HasOne(file => file.Series, file => file.SeriesId);
Mapper.Entity<MovieFile>().RegisterModel("MovieFiles")
.Ignore(f => f.Path)
.Relationships.AutoMapICollectionOrComplexProperties()
.For("Movie")
.LazyLoad(condition: parent => parent.Id > 0,
query: (db, parent) => db.Query<Movie>().Where(c => c.MovieFileId == parent.Id).ToList())
.HasOne(file => file.Movie, file => file.MovieId);
Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath)
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId)
.HasOne(m => m.MovieFile, m => m.MovieFileId);
Mapper.Entity<Episode>().RegisterModel("Episodes")
.Ignore(e => e.SeriesTitle)
.Ignore(e => e.Series)

View file

@ -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<Rejection> 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();
}

View file

@ -37,9 +37,83 @@ namespace NzbDrone.Core.DecisionEngine
public List<DownloadDecision> GetSearchDecision(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteriaBase)
{
if (searchCriteriaBase.Movie != null)
{
return GetMovieDecisions(reports, searchCriteriaBase).ToList();
}
return GetDecisions(reports, searchCriteriaBase).ToList();
}
private IEnumerable<DownloadDecision> GetMovieDecisions(List<ReleaseInfo> 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.ParseTitle(report.Title);
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.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
{
remoteEpisode.DownloadAllowed = true;
//decision = GetDecisionForReport(remoteEpisode, searchCriteria); TODO: Rewrite this for movies!
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<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, SearchCriteriaBase searchCriteria = null)
{
if (reports.Any())
@ -80,7 +154,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())
{
@ -143,8 +219,9 @@ 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;

View file

@ -7,6 +7,7 @@ namespace NzbDrone.Core.DecisionEngine
public interface IPrioritizeDownloadDecision
{
List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisions);
List<DownloadDecision> PrioritizeDecisionsForMovies(List<DownloadDecision> decisions);
}
public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision
@ -29,5 +30,17 @@ namespace NzbDrone.Core.DecisionEngine
.Union(decisions.Where(c => c.RemoteEpisode.Series == null))
.ToList();
}
public List<DownloadDecision> PrioritizeDecisionsForMovies(List<DownloadDecision> 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();
}
}
}

View file

@ -30,7 +30,7 @@ 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.");
}
}

View file

@ -28,7 +28,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();

View file

@ -29,19 +29,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();

View file

@ -60,7 +60,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,

View file

@ -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<DownloadClientItem> GetItems()

View file

@ -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<DownloadClientItem> 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'."
};
}
}

View file

@ -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);
}
}
}

View file

@ -57,6 +57,7 @@ namespace NzbDrone.Core.Download
get;
}
public abstract string Download(RemoteEpisode remoteEpisode);
public abstract IEnumerable<DownloadClientItem> 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);
}
}

View file

@ -15,6 +15,7 @@ namespace NzbDrone.Core.Download
public interface IDownloadService
{
void DownloadReport(RemoteEpisode remoteEpisode);
void DownloadReport(RemoteMovie remoteMovie);
}
@ -41,8 +42,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 +92,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);
}
}
}

View file

@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download
DownloadProtocol Protocol { get; }
string Download(RemoteEpisode remoteEpisode);
string Download(RemoteMovie remoteMovie);
IEnumerable<DownloadClientItem> GetItems();
void RemoveItem(string downloadId, bool deleteData);
DownloadClientStatus GetStatus();

View file

@ -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;
}
}
}

View file

@ -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; }
}
}

View file

@ -33,6 +33,7 @@ namespace NzbDrone.Core.Download.Pending
public class PendingReleaseService : IPendingReleaseService,
IHandle<SeriesDeletedEvent>,
IHandle<EpisodeGrabbedEvent>,
IHandle<MovieGrabbedEvent>,
IHandle<RssSyncCompleteEvent>
{
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);

View file

@ -32,53 +32,112 @@ namespace NzbDrone.Core.Download
public ProcessedDecisions ProcessDecisions(List<DownloadDecision> decisions)
{
var qualifiedReports = GetQualifiedReports(decisions);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports);
//var qualifiedReports = GetQualifiedReports(decisions);
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
var grabbed = new List<DownloadDecision>();
var pending = new List<DownloadDecision>();
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<int> movieIds = new List<int> { 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);
}
}
}

View file

@ -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);
}

View file

@ -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; }

View file

@ -62,6 +62,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
if (parsedEpisodeInfo != null)
{
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
trackedDownload.RemoteMovie = _parsingService.Map(parsedEpisodeInfo, "", null);
}
if (historyItems.Any())
@ -69,10 +70,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 +86,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
}
}
if (trackedDownload.RemoteEpisode == null)
if (trackedDownload.RemoteEpisode == null && trackedDownload.RemoteMovie == null)
{
return null;
}

View file

@ -1,4 +1,5 @@
using System.Net;
using System;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Http;
using NzbDrone.Core.Exceptions;
@ -31,6 +32,11 @@ namespace NzbDrone.Core.Download
protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent);
protected virtual string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents)
{
throw new NotImplementedException();
}
public override string Download(RemoteEpisode remoteEpisode)
{
var url = remoteEpisode.Release.DownloadUrl;
@ -67,5 +73,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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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; }

View file

@ -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<History> FindByDownloadId(string downloadId);
List<History> FindDownloadHistory(int idSeriesId, QualityModel quality);
void DeleteForSeries(int seriesId);
History MostRecentForMovie(int movieId);
}
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
@ -71,10 +73,20 @@ namespace NzbDrone.Core.History
protected override SortBuilder<History> GetPagedQuery(QueryBuilder<History> query, PagingSpec<History> pagingSpec)
{
var baseQuery = query.Join<History, Series>(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id)
.Join<History, Episode>(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id);
var baseQuery = query/*.Join<History, Series>(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id)
.Join<History, Episode>(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id)*/
.Join<History, Movie>(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();
}
}
}

View file

@ -20,6 +20,7 @@ namespace NzbDrone.Core.History
{
QualityModel GetBestQualityInHistory(Profile profile, int episodeId);
PagingSpec<History> Paged(PagingSpec<History> pagingSpec);
History MostRecentForMovie(int movieId);
History MostRecentForEpisode(int episodeId);
History MostRecentForDownloadId(string downloadId);
History Get(int historyId);
@ -29,6 +30,7 @@ namespace NzbDrone.Core.History
public class HistoryService : IHistoryService,
IHandle<EpisodeGrabbedEvent>,
IHandle<MovieGrabbedEvent>,
IHandle<EpisodeImportedEvent>,
IHandle<DownloadFailedEvent>,
IHandle<EpisodeFileDeletedEvent>,
@ -53,6 +55,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 +145,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 +180,50 @@ namespace NzbDrone.Core.History
}
}
public void Handle(MovieGrabbedEvent message)
{
var history = new History
{
EventType = HistoryEventType.Grabbed,
Date = DateTime.UtcNow,
Quality = message.Movie.ParsedEpisodeInfo.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.ParsedEpisodeInfo.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.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
{
history.Data.Add("ReleaseHash", message.Movie.ParsedEpisodeInfo.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 +241,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());
@ -249,6 +304,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());

View file

@ -0,0 +1,11 @@
namespace NzbDrone.Core.IndexerSearch.Definitions
{
public class MovieSearchCriteria : SearchCriteriaBase
{
public override string ToString()
{
return string.Format("[{0}]", Movie.Title);
}
}
}

View file

@ -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<string> SceneTitles { get; set; }
public List<Episode> Episodes { get; set; }
public virtual bool MonitoredEpisodesOnly { get; set; }

View file

@ -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;
}
}

View file

@ -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<MoviesSearchCommand>
{
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);
}
}
}

View file

@ -19,6 +19,8 @@ namespace NzbDrone.Core.IndexerSearch
{
List<DownloadDecision> EpisodeSearch(int episodeId, bool userInvokedSearch);
List<DownloadDecision> EpisodeSearch(Episode episode, bool userInvokedSearch);
List<DownloadDecision> MovieSearch(int movieId, bool userInvokedSearch);
List<DownloadDecision> MovieSearch(Movie movie, bool userInvokedSearch);
List<DownloadDecision> 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<DownloadDecision> MovieSearch(int movieId, bool userInvokedSearch)
{
var movie = _movieService.GetMovie(movieId);
return MovieSearch(movie, userInvokedSearch);
}
public List<DownloadDecision> MovieSearch(Movie movie, bool userInvokedSearch)
{
var searchSpec = Get<MovieSearchCriteria>(movie, userInvokedSearch);
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}
public List<DownloadDecision> EpisodeSearch(Episode episode, bool userInvokedSearch)
{
var series = _seriesService.GetSeries(episode.SeriesId);
@ -245,6 +264,23 @@ namespace NzbDrone.Core.IndexerSearch
return spec;
}
private TSpec Get<TSpec>(Movie movie, bool userInvokedSearch) where TSpec : SearchCriteriaBase, new()
{
var spec = new TSpec();
spec.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);*/
spec.UserInvokedSearch = userInvokedSearch;
return spec;
}
private List<DownloadDecision> Dispatch(Func<IIndexer, IEnumerable<ReleaseInfo>> searchAction, SearchCriteriaBase criteriaBase)
{
var indexers = _indexerFactory.SearchEnabled();

View file

@ -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();

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -111,6 +111,18 @@ namespace NzbDrone.Core.Indexers
return FetchReleases(generator.GetSearchRequests(searchCriteria));
}
public override IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria)
{
if (!SupportsSearch)
{
return new List<ReleaseInfo>();
}
var generator = GetRequestGenerator();
return FetchReleases(generator.GetSearchRequests(searchCriteria));
}
protected virtual IList<ReleaseInfo> FetchReleases(IndexerPageableRequestChain pageableRequestChain, bool isRecent = false)
{
var releases = new List<ReleaseInfo>();

View file

@ -17,5 +17,6 @@ namespace NzbDrone.Core.Indexers
IList<ReleaseInfo> Fetch(DailyEpisodeSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(AnimeEpisodeSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(SpecialEpisodeSearchCriteria searchCriteria);
IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
}
}

View file

@ -10,5 +10,6 @@ namespace NzbDrone.Core.Indexers
IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria);
IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria);
IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria);
IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria);
}
}

View file

@ -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();

View file

@ -67,6 +67,7 @@ namespace NzbDrone.Core.Indexers
public abstract IList<ReleaseInfo> Fetch(DailyEpisodeSearchCriteria searchCriteria);
public abstract IList<ReleaseInfo> Fetch(AnimeEpisodeSearchCriteria searchCriteria);
public abstract IList<ReleaseInfo> Fetch(SpecialEpisodeSearchCriteria searchCriteria);
public abstract IList<ReleaseInfo> Fetch(MovieSearchCriteria searchCriteria);
protected virtual IList<ReleaseInfo> CleanupReleases(IEnumerable<ReleaseInfo> releases)
{

View file

@ -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();
}
}
}

View file

@ -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;
@ -273,5 +274,10 @@ namespace NzbDrone.Core.Indexers.Newznab
{
return title.Replace("+", "%20");
}
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
{
return new IndexerPageableRequestChain();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<IndexerRequest> 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;
}
}
}

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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");
}
}
}
}

View file

@ -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

View file

@ -22,6 +22,8 @@ namespace NzbDrone.Core.MediaCover
public class MediaCoverService :
IHandleAsync<SeriesUpdatedEvent>,
IHandleAsync<MovieUpdatedEvent>,
IHandleAsync<MovieAddedEvent>,
IHandleAsync<SeriesDeletedEvent>,
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);

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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<string> FilterFiles(Series series, IEnumerable<string> files);
@ -30,6 +31,8 @@ namespace NzbDrone.Core.MediaFiles
public class DiskScanService :
IDiskScanService,
IHandle<SeriesUpdatedEvent>,
IHandle<MovieUpdatedEvent>,
IExecute<RescanMovieCommand>,
IExecute<RescanSeriesCommand>
{
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<string> FilterFiles(Movie movie, IEnumerable<string> 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)

View file

@ -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<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
List<ImportResult> 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<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo)
{
var results = new List<ImportResult>();
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<ImportResult> 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<ImportResult>();
}
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<ImportResult> 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<ImportResult>
{
UnknownMovieResult("Unknown Movie")
};
}
return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem);
}
private List<ImportResult> 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<ImportResult>();
}
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<ImportResult>
{
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<ImportResult> 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<ImportResult>
{
UnknownMovieResult(string.Format("Unknown Movie for file: {0}", fileInfo.Name), fileInfo.FullName)
};
}
return ProcessFile(fileInfo, importMode, movie, downloadClientItem);
}
private List<ImportResult> 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<ImportResult>
{
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<ImportResult>
{
FileIsLockedResult(fileInfo.FullName)
};
}
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { 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);
}
}
}

View file

@ -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

View file

@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IImportDecisionEngineSpecification
{
Decision IsSatisfiedBy(LocalEpisode localEpisode);
Decision IsSatisfiedBy(LocalMovie localMovie);
}
}

View file

@ -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<ImportResult> Import(List<ImportDecision> 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<ImportResult> Import(List<ImportDecision> 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<ImportResult>();
foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalMovie.Size)
.ThenByDescending(e => e.LocalMovie.Size))
{
var localMovie = importDecision.LocalMovie;
var oldFiles = new List<MovieFile>();
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;
}
}
}

View file

@ -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<Rejection> 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
};
}
}
}

View file

@ -18,6 +18,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
public interface IMakeImportDecision
{
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource); //TODO: Needs changing to ParsedMovieInfo!!
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource);
}
@ -53,6 +55,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
return GetImportDecisions(videoFiles, series, null, false);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie)
{
return GetImportDecisions(videoFiles, movie, null, false);
}
public List<ImportDecision> GetImportDecisions(List<string> 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<ImportDecision> GetImportDecisions(List<string> 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<ImportDecision>();
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<string> 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)

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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)

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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)

View file

@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
return Decision.Accept();
}
public Decision IsSatisfiedBy(LocalMovie localEpisode)
{
return Decision.Accept();
}
}
}

View file

@ -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<MovieFile> OldFiles { get; private set; }
public MovieDownloadedEvent(LocalMovie episode, MovieFile episodeFile, List<MovieFile> oldFiles)
{
Movie = episode;
MovieFile = episodeFile;
OldFiles = oldFiles;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

Some files were not shown because too many files have changed in this diff Show more