mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-22 06:23:31 -07:00
Compare commits
44 commits
Author | SHA1 | Date | |
---|---|---|---|
|
28857e7fac | ||
|
6d23fb1b61 | ||
|
ad95fbfd4a | ||
|
d9d8cbacec | ||
|
80f2adad50 | ||
|
7a72f4a05b | ||
|
6d7ff628d8 | ||
|
e9f9f66b2f | ||
|
6ca88f98af | ||
|
631cf776f6 | ||
|
6ea9b4b94a | ||
|
d835c168d3 | ||
|
329786365d | ||
|
4f6380a73c | ||
|
cde1217356 | ||
|
2eedfca78a | ||
|
0d65984991 | ||
|
2a932fe7e8 | ||
|
0e02171938 | ||
|
2a3b0304cb | ||
|
16e35f68bb | ||
|
74b1c846a5 | ||
|
1f930c18e4 | ||
|
d9e60eff6b | ||
|
097982334c | ||
|
be6e6b910e | ||
|
31b9ec1116 | ||
|
ff6c3b70d3 | ||
|
0fd0b31a60 | ||
|
76e6ebc63c | ||
|
2ea35adb98 | ||
|
782f63f510 | ||
|
c874122fc0 | ||
|
2efda4933d | ||
|
b7c70d750a | ||
|
5ebfac6cc8 | ||
|
74ca6149e3 | ||
|
40d7590f80 | ||
|
2cb27240dc | ||
|
837c2683df | ||
|
0b278c7db8 | ||
|
0b765d10fe | ||
|
20dbdfb344 | ||
|
426448ed98 |
260 changed files with 9144 additions and 231 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -127,9 +127,13 @@ bin
|
|||
obj
|
||||
output/*
|
||||
|
||||
#Packages
|
||||
Radarr_*/
|
||||
Radarr_*.zip
|
||||
|
||||
#OS X metadata files
|
||||
._*
|
||||
.DS_Store
|
||||
|
||||
_start
|
||||
_temp_*/**/*
|
||||
|
|
12
.travis.yml
Normal file
12
.travis.yml
Normal 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
|
|
@ -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
50
package.sh
Normal 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
|
19
readme.md
19
readme.md
|
@ -1,7 +1,20 @@
|
|||
# Sonarr #
|
||||
# Radarr [](https://travis-ci.org/galli-leo/Radarr)#
|
||||
|
||||
This fork of Sonarr aims to turn it into something like Couchpotato.
|
||||
|
||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
## Currently working:
|
||||
* Adding new movies (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
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
44
src/NzbDrone.Api/Series/MovieLookupModule.cs
Normal file
44
src/NzbDrone.Api/Series/MovieLookupModule.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
225
src/NzbDrone.Api/Series/MovieModule.cs
Normal file
225
src/NzbDrone.Api/Series/MovieModule.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
183
src/NzbDrone.Api/Series/MovieResource.cs
Normal file
183
src/NzbDrone.Api/Series/MovieResource.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||
|
||||
|
||||
sw.Stop();
|
||||
|
||||
_announcer.ElapsedTime(sw.Elapsed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
17
src/NzbDrone.Core/Download/MovieGrabbedEvent.cs
Normal file
17
src/NzbDrone.Core/Download/MovieGrabbedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs
Normal file
27
src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
{
|
||||
public class MovieSearchCriteria : SearchCriteriaBase
|
||||
{
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0}]", Movie.Title);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
11
src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs
Normal file
11
src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
46
src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs
Normal file
46
src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -10,5 +10,6 @@ namespace NzbDrone.Core.Indexers
|
|||
IndexerPageableRequestChain GetSearchRequests(DailyEpisodeSearchCriteria searchCriteria);
|
||||
IndexerPageableRequestChain GetSearchRequests(AnimeEpisodeSearchCriteria searchCriteria);
|
||||
IndexerPageableRequestChain GetSearchRequests(SpecialEpisodeSearchCriteria searchCriteria);
|
||||
IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
20
src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs
Normal file
20
src/NzbDrone.Core/MediaFiles/Commands/RescanMovieCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
265
src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs
Normal file
265
src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
|
|||
public interface IImportDecisionEngineSpecification
|
||||
{
|
||||
Decision IsSatisfiedBy(LocalEpisode localEpisode);
|
||||
|
||||
Decision IsSatisfiedBy(LocalMovie localMovie);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
|
|||
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
public Decision IsSatisfiedBy(LocalMovie localEpisode)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
20
src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs
Normal file
20
src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
14
src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs
Normal file
14
src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs
Normal file
16
src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
32
src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs
Normal file
32
src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue