From 4f6380a73cd14c7e68367f1b5e2e3d27f1d8fdc1 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 11:59:03 +0100 Subject: [PATCH 01/12] Automatically downloading the best movie release works now. --- .../Migration/054_rename_profiles.cs | 5 +- .../Download/CompletedDownloadService.cs | 21 ++- .../Download/Pending/PendingRelease.cs | 2 + .../Download/Pending/PendingReleaseService.cs | 2 +- .../Download/ProcessDownloadDecisions.cs | 129 +++++++++++++----- .../IndexerSearch/MoviesSearchCommand.cs | 11 ++ .../IndexerSearch/MoviesSearchService.cs | 46 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + src/NzbDrone.Core/Parser/ParsingService.cs | 23 +++- src/UI/Commands/CommandController.js | 2 +- src/UI/Movies/Details/MoviesDetailsLayout.js | 5 +- 11 files changed, 203 insertions(+), 45 deletions(-) create mode 100644 src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs create mode 100644 src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs diff --git a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs index e665c14a4..5535a1bd9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs +++ b/src/NzbDrone.Core/Datastore/Migration/054_rename_profiles.cs @@ -21,11 +21,12 @@ namespace NzbDrone.Core.Datastore.Migration //Add HeldReleases Create.TableForModel("PendingReleases") - .WithColumn("SeriesId").AsInt32() + .WithColumn("SeriesId").AsInt32().WithDefaultValue(0) .WithColumn("Title").AsString() .WithColumn("Added").AsDateTime() .WithColumn("ParsedEpisodeInfo").AsString() - .WithColumn("Release").AsString(); + .WithColumn("Release").AsString() + .WithColumn("MovieId").AsInt32().WithDefaultValue(0); } } } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index c4fbe11a2..024a41c8b 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download private readonly IHistoryService _historyService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; private readonly IParsingService _parsingService; + private readonly IMovieService _movieService; private readonly Logger _logger; private readonly ISeriesService _seriesService; @@ -37,6 +38,7 @@ namespace NzbDrone.Core.Download IDownloadedEpisodesImportService downloadedEpisodesImportService, IParsingService parsingService, ISeriesService seriesService, + IMovieService movieService, Logger logger) { _configService = configService; @@ -44,6 +46,7 @@ namespace NzbDrone.Core.Download _historyService = historyService; _downloadedEpisodesImportService = downloadedEpisodesImportService; _parsingService = parsingService; + _movieService = movieService; _logger = logger; _seriesService = seriesService; } @@ -88,19 +91,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; } } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs index a713fe48c..504db7e36 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingRelease.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingRelease.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Download.Pending public class PendingRelease : ModelBase { public int SeriesId { get; set; } + public int MovieId { get; set; } public string Title { get; set; } public DateTime Added { get; set; } public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } @@ -14,5 +15,6 @@ namespace NzbDrone.Core.Download.Pending //Not persisted public RemoteEpisode RemoteEpisode { get; set; } + public RemoteMovie RemoteMovie { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index d5fbff7d8..5a53e2d18 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -344,7 +344,7 @@ namespace NzbDrone.Core.Download.Pending public void Handle(MovieGrabbedEvent message) { - + //RemoveGrabbed(message.Movie); } public void Handle(RssSyncCompleteEvent message) diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 05719587d..adadebdee 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -32,53 +32,112 @@ namespace NzbDrone.Core.Download public ProcessedDecisions ProcessDecisions(List decisions) { - var qualifiedReports = GetQualifiedReports(decisions); - var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(qualifiedReports); + //var qualifiedReports = GetQualifiedReports(decisions); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); var grabbed = new List(); var pending = new List(); foreach (var report in prioritizedDecisions) { - var remoteEpisode = report.RemoteEpisode; - var episodeIds = remoteEpisode.Episodes.Select(e => e.Id).ToList(); - - //Skip if already grabbed - if (grabbed.SelectMany(r => r.RemoteEpisode.Episodes) - .Select(e => e.Id) - .ToList() - .Intersect(episodeIds) - .Any()) + if (report.IsForMovie) { - continue; - } + var remoteMovie = report.RemoteMovie; - if (report.TemporarilyRejected) - { - _pendingReleaseService.Add(report); - pending.Add(report); - continue; - } + if (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; - } + if (remoteMovie == null || remoteMovie.Movie == null) + { + continue; + } - try - { - _downloadService.DownloadReport(remoteEpisode); - grabbed.Add(report); + List movieIds = new List { remoteMovie.Movie.Id }; + + + //Skip if already grabbed + if (grabbed.Select(r => r.RemoteMovie.Movie) + .Select(e => e.Id) + .ToList() + .Intersect(movieIds) + .Any()) + { + continue; + } + + if (pending.Select(r => r.RemoteMovie.Movie) + .Select(e => e.Id) + .ToList() + .Intersect(movieIds) + .Any()) + { + continue; + } + + try + { + _downloadService.DownloadReport(remoteMovie); + grabbed.Add(report); + } + catch (Exception e) + { + //TODO: support for store & forward + //We'll need to differentiate between a download client error and an indexer error + _logger.Warn(e, "Couldn't add report to download queue. " + remoteMovie); + } } - catch (Exception e) + else { - //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); + var remoteEpisode = report.RemoteEpisode; + + if (remoteEpisode == null || remoteEpisode.Episodes == null) + { + continue; + } + + 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()) + { + continue; + } + + if (report.TemporarilyRejected) + { + _pendingReleaseService.Add(report); + pending.Add(report); + continue; + } + + if (pending.SelectMany(r => r.RemoteEpisode.Episodes) + .Select(e => e.Id) + .ToList() + .Intersect(episodeIds) + .Any()) + { + continue; + } + + try + { + _downloadService.DownloadReport(remoteEpisode); + grabbed.Add(report); + } + catch (Exception e) + { + //TODO: support for store & forward + //We'll need to differentiate between a download client error and an indexer error + _logger.Warn(e, "Couldn't add report to download queue. " + remoteEpisode); + } } } diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs new file mode 100644 index 000000000..da0b9a8c1 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchCommand.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class MoviesSearchCommand : Command + { + public int MovieId { get; set; } + + public override bool SendUpdatesToClient => true; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs new file mode 100644 index 000000000..656423178 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/MoviesSearchService.cs @@ -0,0 +1,46 @@ +using System.Linq; +using NLog; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.IndexerSearch +{ + public class MovieSearchService : IExecute + { + private readonly IMovieService _seriesService; + private readonly ISearchForNzb _nzbSearchService; + private readonly IProcessDownloadDecisions _processDownloadDecisions; + private readonly Logger _logger; + + public MovieSearchService(IMovieService seriesService, + ISearchForNzb nzbSearchService, + IProcessDownloadDecisions processDownloadDecisions, + Logger logger) + { + _seriesService = seriesService; + _nzbSearchService = nzbSearchService; + _processDownloadDecisions = processDownloadDecisions; + _logger = logger; + } + + public void Execute(MoviesSearchCommand message) + { + var series = _seriesService.GetMovie(message.MovieId); + + var downloadedCount = 0; + + if (!series.Monitored) + { + _logger.Debug("Movie {0} is not monitored, skipping search", series.Title); + } + + var decisions = _nzbSearchService.MovieSearch(message.MovieId, false);//_nzbSearchService.SeasonSearch(message.MovieId, season.SeasonNumber, false, message.Trigger == CommandTrigger.Manual); + downloadedCount += _processDownloadDecisions.ProcessDecisions(decisions).Grabbed.Count; + + + _logger.ProgressInfo("Movie search completed. {0} reports downloaded.", downloadedCount); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4bc218fe3..9dd64feb2 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -564,6 +564,8 @@ + + diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 937c9cf94..cc9ca8447 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Parser LocalEpisode GetLocalEpisode(string filename, Series series); LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); Series GetSeries(string title); + Movie GetMovie(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable episodeIds); RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null); @@ -104,7 +105,7 @@ namespace NzbDrone.Core.Parser if (parsedEpisodeInfo == null) { - return _seriesService.FindByTitle(title); //Here we have a problem since it is not possible for movies to find a scene mapping, so these releases are always rejected :( + return _seriesService.FindByTitle(title); } var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); @@ -118,6 +119,26 @@ namespace NzbDrone.Core.Parser return series; } + public Movie GetMovie(string title) + { + var parsedEpisodeInfo = Parser.ParseTitle(title); + + if (parsedEpisodeInfo == null) + { + return _movieService.FindByTitle(title); + } + + var series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + + if (series == null) + { + series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.SeriesTitleInfo.Year); + } + + return series; + } + public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) { var remoteEpisode = new RemoteEpisode diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js index 2232d45ae..929870107 100644 --- a/src/UI/Commands/CommandController.js +++ b/src/UI/Commands/CommandController.js @@ -86,7 +86,7 @@ var singleton = function() { } } }); - + console.warn(options) options.element.startSpin(); } }; diff --git a/src/UI/Movies/Details/MoviesDetailsLayout.js b/src/UI/Movies/Details/MoviesDetailsLayout.js index 6dd34e360..4396c22de 100644 --- a/src/UI/Movies/Details/MoviesDetailsLayout.js +++ b/src/UI/Movies/Details/MoviesDetailsLayout.js @@ -32,7 +32,7 @@ module.exports = Marionette.Layout.extend({ edit : '.x-edit', refresh : '.x-refresh', rename : '.x-rename', - search : '.x-search', + searchAuto : '.x-search', poster : '.x-movie-poster', manualSearch : '.x-manual-search', history : '.x-movie-history', @@ -86,8 +86,9 @@ module.exports = Marionette.Layout.extend({ name : 'refreshMovie' } }); + CommandController.bindToCommand({ - element : this.ui.search, + element : this.ui.searchAuto, command : { name : 'moviesSearch' } From 329786365dd8fa37e471b4fc38ecf0abdae9bc92 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 12:52:09 +0100 Subject: [PATCH 02/12] Fixed adding multiple movies. --- src/UI/AddMovies/AddMoviesView.js | 8 +++++--- src/UI/AddMovies/SearchResultCollectionView.js | 14 +++++++++++++- src/UI/AddMovies/SearchResultViewTemplate.hbs | 6 +++++- src/UI/Handlebars/Helpers/Series.js | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/UI/AddMovies/AddMoviesView.js b/src/UI/AddMovies/AddMoviesView.js index 1694a9ffc..e83aad42f 100644 --- a/src/UI/AddMovies/AddMoviesView.js +++ b/src/UI/AddMovies/AddMoviesView.js @@ -120,16 +120,17 @@ module.exports = Marionette.Layout.extend({ }, _onMoviesAdded : function(options) { - if (this.isExisting && options.movies.get('path') === this.model.get('folder').path) { + if (this.isExisting && options.movie.get('path') === this.model.get('folder').path) { this.close(); } else if (!this.isExisting) { - this.collection.term = ''; + this.resultCollectionView.setExisting(options.movie.get('imdbId')) + /*this.collection.term = ''; this.collection.reset(); this._clearResults(); this.ui.moviesSearch.val(''); - this.ui.moviesSearch.focus(); + this.ui.moviesSearch.focus();*/ //TODO: Maybe add option wheter to clear search result. } }, @@ -143,6 +144,7 @@ module.exports = Marionette.Layout.extend({ }, _clearResults : function() { + if (!this.isExisting) { this.searchResult.show(new EmptyView()); } else { diff --git a/src/UI/AddMovies/SearchResultCollectionView.js b/src/UI/AddMovies/SearchResultCollectionView.js index e533085ac..ac1257697 100644 --- a/src/UI/AddMovies/SearchResultCollectionView.js +++ b/src/UI/AddMovies/SearchResultCollectionView.js @@ -21,9 +21,21 @@ module.exports = Marionette.CollectionView.extend({ return this.showing >= this.collection.length; }, + setExisting : function(imdbid) { + var movies = this.collection.where({ imdbId : imdbid }); + console.warn(movies) + //debugger; + if (movies.length > 0) { + this.children.findByModel(movies[0])._configureTemplateHelpers(); + //this.children.findByModel(movies[0])._configureTemplateHelpers(); + this.children.findByModel(movies[0]).render(); + //this.templateHelpers.existing = existingMovies[0].toJSON(); + } + }, + appendHtml : function(collectionView, itemView, index) { if (!this.isExisting || index < this.showing || index === 0) { collectionView.$el.append(itemView.el); } } -}); \ No newline at end of file +}); diff --git a/src/UI/AddMovies/SearchResultViewTemplate.hbs b/src/UI/AddMovies/SearchResultViewTemplate.hbs index 41845cdee..87e1167da 100644 --- a/src/UI/AddMovies/SearchResultViewTemplate.hbs +++ b/src/UI/AddMovies/SearchResultViewTemplate.hbs @@ -2,7 +2,11 @@
@@ -74,7 +78,7 @@ -
diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index fc96495a0..016279e6e 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -19,6 +19,22 @@ Handlebars.registerHelper('poster', function() { return new Handlebars.SafeString(''.format(placeholder)); }); +Handlebars.registerHelper('remotePoster', function() { + var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; + var poster = this.remotePoster; + + if (poster) { + if (!poster.match(/^https?:\/\//)) { + return new Handlebars.SafeString(''.format(Handlebars.helpers.defaultImg.call(null, poster, 250))); + } else { + var url = poster.replace(/^https?\:/, 'https://'); //IMDb posters need https to work, k? + return new Handlebars.SafeString(''.format(Handlebars.helpers.defaultImg.call(null, url))); + } + } + + return new Handlebars.SafeString(''.format(placeholder)); +}) + Handlebars.registerHelper('traktUrl', function() { return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; }); From d835c168d3315e297ad61f4bc85320bb4f375817 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 13:26:09 +0100 Subject: [PATCH 03/12] Searching for movies directly when adding them is now working. --- src/NzbDrone.Api/Series/MovieResource.cs | 4 ++ .../Datastore/Migration/001_initial_setup.cs | 3 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Tv/Movie.cs | 7 ++- src/NzbDrone.Core/Tv/MovieScannedHandler.cs | 57 +++++++++++++++++++ src/NzbDrone.Core/Tv/MovieService.cs | 5 ++ src/UI/AddMovies/SearchResultView.js | 9 +-- 7 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 src/NzbDrone.Core/Tv/MovieScannedHandler.cs diff --git a/src/NzbDrone.Api/Series/MovieResource.cs b/src/NzbDrone.Api/Series/MovieResource.cs index eed694e05..a35b2d210 100644 --- a/src/NzbDrone.Api/Series/MovieResource.cs +++ b/src/NzbDrone.Api/Series/MovieResource.cs @@ -48,6 +48,7 @@ namespace NzbDrone.Api.Movie public List Genres { get; set; } public HashSet Tags { get; set; } public DateTime Added { get; set; } + public AddMovieOptions AddOptions { get; set; } public Ratings Ratings { get; set; } //TODO: Add series statistics as a property of the series (instead of individual properties) @@ -110,6 +111,7 @@ namespace NzbDrone.Api.Movie Genres = model.Genres, Tags = model.Tags, Added = model.Added, + AddOptions = model.AddOptions, Ratings = model.Ratings }; } @@ -152,6 +154,7 @@ namespace NzbDrone.Api.Movie Genres = resource.Genres, Tags = resource.Tags, Added = resource.Added, + AddOptions = resource.AddOptions, Ratings = resource.Ratings }; } @@ -167,6 +170,7 @@ namespace NzbDrone.Api.Movie movie.RootFolderPath = resource.RootFolderPath; movie.Tags = resource.Tags; + movie.AddOptions = resource.AddOptions; return movie; } diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index 2fc722cb4..0042de064 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -63,7 +63,8 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Ratings").AsString().Nullable() .WithColumn("Genres").AsString().Nullable() .WithColumn("Tags").AsString().Nullable() - .WithColumn("Certification").AsString().Nullable(); + .WithColumn("Certification").AsString().Nullable() + .WithColumn("AddOptions").AsString().Nullable(); Create.TableForModel("Seasons") diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9dd64feb2..18f0b1ae7 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1096,6 +1096,7 @@ + diff --git a/src/NzbDrone.Core/Tv/Movie.cs b/src/NzbDrone.Core/Tv/Movie.cs index ccbe93510..9409ae23e 100644 --- a/src/NzbDrone.Core/Tv/Movie.cs +++ b/src/NzbDrone.Core/Tv/Movie.cs @@ -40,11 +40,16 @@ namespace NzbDrone.Core.Tv public DateTime? InCinemas { get; set; } public LazyLoaded Profile { get; set; } public HashSet Tags { get; set; } -// public AddMovieOptions AddOptions { get; set; } + public AddMovieOptions AddOptions { get; set; } public override string ToString() { return string.Format("[{0}][{1}]", ImdbId, Title.NullSafe()); } } + + public class AddMovieOptions : MonitoringOptions + { + public bool SearchForMovie { get; set; } + } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieScannedHandler.cs b/src/NzbDrone.Core/Tv/MovieScannedHandler.cs new file mode 100644 index 000000000..151ef0559 --- /dev/null +++ b/src/NzbDrone.Core/Tv/MovieScannedHandler.cs @@ -0,0 +1,57 @@ +using NLog; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Tv +{ + public class MovieScannedHandler : IHandle, + IHandle + { + + private readonly IMovieService _movieService; + private readonly IManageCommandQueue _commandQueueManager; + + private readonly Logger _logger; + + public MovieScannedHandler( IMovieService movieService, + IManageCommandQueue commandQueueManager, + Logger logger) + { + _movieService = movieService; + _commandQueueManager = commandQueueManager; + _logger = logger; + } + + private void HandleScanEvents(Movie movie) + { + if (movie.AddOptions == null) + { + //_episodeAddedService.SearchForRecentlyAdded(movie.Id); + return; + } + + _logger.Info("[{0}] was recently added, performing post-add actions", movie.Title); + //_episodeMonitoredService.SetEpisodeMonitoredStatus(movie, movie.AddOptions); + + if (movie.AddOptions.SearchForMovie) + { + _commandQueueManager.Push(new MoviesSearchCommand { MovieId = movie.Id}); + } + + movie.AddOptions = null; + _movieService.RemoveAddOptions(movie); + } + + public void Handle(MovieScannedEvent message) + { + HandleScanEvents(message.Movie); + } + + public void Handle(MovieScanSkippedEvent message) + { + HandleScanEvents(message.Movie); + } + } +} diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index 546442f48..d3ec007cb 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Core.Tv Movie UpdateMovie(Movie movie); List UpdateMovie(List movie); bool MoviePathExists(string folder); + void RemoveAddOptions(Movie movie); } public class MovieService : IMovieService @@ -190,5 +191,9 @@ namespace NzbDrone.Core.Tv return _movieRepository.MoviePathExists(folder); } + public void RemoveAddOptions(Movie movie) + { + _movieRepository.SetFields(movie, s => s.AddOptions); + } } } diff --git a/src/UI/AddMovies/SearchResultView.js b/src/UI/AddMovies/SearchResultView.js index 839b2d1ee..a01a41e3e 100644 --- a/src/UI/AddMovies/SearchResultView.js +++ b/src/UI/AddMovies/SearchResultView.js @@ -153,14 +153,14 @@ var view = Marionette.ItemView.extend({ }, _addWithoutSearch : function() { - this._addMovies(true); + this._addMovies(false); }, _addAndSearch : function() { this._addMovies(true); }, - _addMovies : function(searchForMissingEpisodes) { + _addMovies : function(searchForMovie) { var addButton = this.ui.addButton; var addSearchButton = this.ui.addSearchButton; @@ -171,7 +171,8 @@ var view = Marionette.ItemView.extend({ var rootFolderPath = this.ui.rootFolder.children(':selected').text(); var options = this._getAddMoviesOptions(); - options.searchForMissingEpisodes = searchForMissingEpisodes; + options.searchForMovie = searchForMovie; + console.warn(searchForMovie); this.model.set({ profileId : profile, @@ -186,7 +187,7 @@ var view = Marionette.ItemView.extend({ console.log(this.model.save); console.log(promise); - if (searchForMissingEpisodes) { + if (searchForMovie) { this.ui.addSearchButton.spinForPromise(promise); } From 6ea9b4b94a1ea2658c9b9bc0cca28a8e1197f2b6 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 14:18:13 +0100 Subject: [PATCH 04/12] Added Script for easier packaging. --- .gitignore | 3 +++ package.sh | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 package.sh diff --git a/.gitignore b/.gitignore index d57a525c8..6e272c56c 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,9 @@ bin obj output/* +#Packages +Radarr_*/ +Radarr_*.zip #OS X metadata files ._* diff --git a/package.sh b/package.sh new file mode 100644 index 000000000..6b2ddcbb2 --- /dev/null +++ b/package.sh @@ -0,0 +1,9 @@ +VERSION=$1 +outputFolder='./_output' +outputFolderMono='./_output_mono' +outputFolderOsx='./_output_osx' +outputFolderOsxApp='./_output_osx_app' + +cp -r $outputFolder Radarr_Windows_$VERSION +cp -r $outputFolderMono Radarr_Mono_$VERSION +cp -r $outputFolderOsxApp Radarr_OSX_$VERSION From 631cf776f6c14cdf20b39c1787fa502e56348fe2 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 15:54:15 +0100 Subject: [PATCH 05/12] Travis now automatically pushes a build to a server. --- .DS_Store | Bin 10244 -> 10244 bytes .travis.yml | 2 ++ package.sh | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.DS_Store b/.DS_Store index 474ee832f549d58148405d309218030bf93b61f6..8dbe7f846234e8292410da784aa237ab9a294cb7 100644 GIT binary patch delta 14 VcmZn(XbIR5BgM$DIaX>pF90Tm1g-!8 delta 14 VcmZn(XbIR5BgM$LIaX>pF90Ts1g`)9 diff --git a/.travis.yml b/.travis.yml index 87ea22b9b..1eb93889f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,5 @@ script: # the following commands are just examples, use whatever your build p install: - sudo apt-get install nodejs - sudo apt-get install npm +after_success: + - ./package.sh diff --git a/package.sh b/package.sh index 6b2ddcbb2..69c065395 100644 --- a/package.sh +++ b/package.sh @@ -1,4 +1,18 @@ -VERSION=$1 +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`" + VERSION="15-11-15" + YEAR="`date +%Y`" + MONTH="`date +%m`" + DAY="`date +%d`" +else + VERSION=$1 +fi outputFolder='./_output' outputFolderMono='./_output_mono' outputFolderOsx='./_output_osx' @@ -7,3 +21,25 @@ outputFolderOsxApp='./_output_osx_app' 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 +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 From 6ca88f98affb149fe9b6da9ab7ad9aa6ea3c6b7f Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 16:04:41 +0100 Subject: [PATCH 06/12] Fix package.sh permissions for travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1eb93889f..f61c413f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,5 @@ install: - sudo apt-get install nodejs - sudo apt-get install npm after_success: + - chmod +x package.sh - ./package.sh From e9f9f66b2fff0e1194f83306a3931b88c41464ff Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 16:06:06 +0100 Subject: [PATCH 07/12] Allow Sonarr and Radarr to run together. Also changes default port of Radarr to 7878. However, now multiple instances of Radarr can also be run. This should be fixed in the future. --- src/NzbDrone.Automation.Test/AutomationTest.cs | 2 +- src/NzbDrone.Common.Test/ConfigFileProviderTest.cs | 6 +++--- src/NzbDrone.Console/ConsoleApp.cs | 2 +- src/NzbDrone.Core/Configuration/ConfigFileProvider.cs | 2 +- src/NzbDrone.Host/SingleInstancePolicy.cs | 4 ++-- src/NzbDrone.Integration.Test/IntegrationTest.cs | 2 +- src/NzbDrone.Integration.Test/IntegrationTestBase.cs | 2 +- src/NzbDrone.Test.Common/NzbDroneRunner.cs | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index 9f493d824..0e0fea564 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Automation.Test _runner.KillAll(); _runner.Start(); - driver.Url = "http://localhost:8989"; + driver.Url = "http://localhost:7878"; var page = new PageBase(driver); page.WaitForNoSpinner(); diff --git a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs index 92df06ded..7d0e0442f 100644 --- a/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs +++ b/src/NzbDrone.Common.Test/ConfigFileProviderTest.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Common.Test public void GetValue_Success() { const string key = "Port"; - const string value = "8989"; + const string value = "7878"; var result = Subject.GetValue(key, value); @@ -60,7 +60,7 @@ namespace NzbDrone.Common.Test public void GetInt_Success() { const string key = "Port"; - const int value = 8989; + const int value = 7878; var result = Subject.GetValueInt(key, value); @@ -95,7 +95,7 @@ namespace NzbDrone.Common.Test [Test] public void GetPort_Success() { - const int value = 8989; + const int value = 7878; var result = Subject.Port; diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 6f935887f..8740b4298 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -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 Sonarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions"); System.Console.WriteLine("Press enter to exit..."); System.Console.ReadLine(); Environment.Exit(1); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index fa6d8a914..fc429c91b 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -133,7 +133,7 @@ namespace NzbDrone.Core.Configuration } } - public int Port => GetValueInt("Port", 8989); + public int Port => GetValueInt("Port", 7878); public int SslPort => GetValueInt("SslPort", 9898); diff --git a/src/NzbDrone.Host/SingleInstancePolicy.cs b/src/NzbDrone.Host/SingleInstancePolicy.cs index 75b8bb13e..18de28fb1 100644 --- a/src/NzbDrone.Host/SingleInstancePolicy.cs +++ b/src/NzbDrone.Host/SingleInstancePolicy.cs @@ -31,9 +31,9 @@ namespace NzbDrone.Host { if (IsAlreadyRunning()) { - _logger.Warn("Another instance of Sonarr is already running."); + _logger.Warn("Another instance of Sonarr or Radarr is already running."); _browserService.LaunchWebUI(); - throw new TerminateApplicationException("Another instance is already running"); + //throw new TerminateApplicationException("Another instance is already running"); TODO: detect only radarr } } diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index bd36562c8..94defd951 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test public override string SeriesRootFolder => GetTempDirectory("SeriesRootFolder"); - protected override string RootUrl => "http://localhost:8989/"; + protected override string RootUrl => "http://localhost:7878/"; protected override string ApiKey => _runner.ApiKey; diff --git a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs index cf6593d04..e8be666bf 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTestBase.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTestBase.cs @@ -159,7 +159,7 @@ namespace NzbDrone.Integration.Test protected void ConnectSignalR() { _signalRReceived = new List(); - _signalrConnection = new Connection("http://localhost:8989/signalr"); + _signalrConnection = new Connection("http://localhost:7878/signalr"); _signalrConnection.Start(new LongPollingTransport()).ContinueWith(task => { if (task.IsFaulted) diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 2f1d8e3f5..87f3f49e9 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -22,10 +22,10 @@ namespace NzbDrone.Test.Common public string AppData { get; private set; } public string ApiKey { get; private set; } - public NzbDroneRunner(Logger logger, int port = 8989) + public NzbDroneRunner(Logger logger, int port = 7878) { _processProvider = new ProcessProvider(logger); - _restClient = new RestClient("http://localhost:8989/api"); + _restClient = new RestClient("http://localhost:7878/api"); } public void Start() From 6d7ff628d86bfa8217eb589282591eba091807bd Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 16:15:13 +0100 Subject: [PATCH 08/12] Updated package.sh for Travis --- package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.sh b/package.sh index 69c065395..aefcf3bf5 100644 --- a/package.sh +++ b/package.sh @@ -26,7 +26,7 @@ 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 +ftp -p -n ftp.leonardogalli.ch << END_SCRIPT quote USER $FTP_USER quote PASS $FTP_PASS mkdir builds From 7a72f4a05b551cb445b7457e3cd7865e4f0be715 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 16:29:49 +0100 Subject: [PATCH 09/12] Fix package.sh --- package.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.sh b/package.sh index aefcf3bf5..f880223d0 100644 --- a/package.sh +++ b/package.sh @@ -5,8 +5,7 @@ if [ $# -eq 0 ]; then fi if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - #VERSION="`date +%H:%M:%S`" - VERSION="15-11-15" + VERSION="`date +%H:%M:%S`" YEAR="`date +%Y`" MONTH="`date +%m`" DAY="`date +%d`" @@ -26,7 +25,8 @@ 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 -p -n ftp.leonardogalli.ch << END_SCRIPT +ftp -n ftp.leonardogalli.ch << END_SCRIPT +passive quote USER $FTP_USER quote PASS $FTP_PASS mkdir builds From 80f2adad50bffe993e83c9e885666bc61016297d Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 17:35:31 +0100 Subject: [PATCH 10/12] Changes name to Radarr in system tray icon. --- .DS_Store | Bin 10244 -> 10244 bytes .../Clients/QBittorrent/QBittorrent.cs | 4 ++-- .../Download/TorrentClientBase.cs | 4 ++-- src/NzbDrone/SysTray/SysTrayApp.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.DS_Store b/.DS_Store index 8dbe7f846234e8292410da784aa237ab9a294cb7..e7ed6672e6d119438b43b45af2ca308f7610a91a 100644 GIT binary patch delta 59 zcmZn(XbG6$&&ag8p7nkQuDNfEw%FoYXn7mg^X)~L|HTKQy J3KL=SvH))`6(|4z delta 66 zcmZn(XbG6$&&aniU^hP_-)0_x9(E2BOG_OELldLPhEnd6pNq?HHk7!=KCx7LGrPh> H3|UzKi2V~8 diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index d8c7e825a..4d4dc47db 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -250,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." }; } @@ -260,7 +260,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { return new NzbDroneValidationFailure(String.Empty, "QBittorrent is configured to remove torrents when they reach their Share Ratio Limit") { - DetailedDescription = "Sonarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." + DetailedDescription = "Radarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'." }; } } diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index 83c013ad7..ce3767c39 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -271,7 +271,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); } @@ -302,7 +302,7 @@ namespace NzbDrone.Core.Download if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) { _logger.Debug( - "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", Definition.Implementation, remoteEpisode.Release.DownloadUrl); } diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 6325593e1..5e3359bcb 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -38,7 +38,7 @@ namespace NzbDrone.SysTray _trayMenu.MenuItems.Add("-"); _trayMenu.MenuItems.Add("Exit", OnExit); - _trayIcon.Text = string.Format("Sonarr - {0}", BuildInfo.Version); + _trayIcon.Text = string.Format("Radarr - {0}", BuildInfo.Version); _trayIcon.Icon = Properties.Resources.NzbDroneIcon; _trayIcon.ContextMenu = _trayMenu; From d9d8cbacec10ba3420f6da53dc7e3036fcc46075 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 17:35:47 +0100 Subject: [PATCH 11/12] Fixes package.sh for OSX builds --- package.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.sh b/package.sh index f880223d0..207006c97 100644 --- a/package.sh +++ b/package.sh @@ -17,6 +17,11 @@ 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 From ad95fbfd4a81c97f29d41bd65577b0280b99b916 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Tue, 3 Jan 2017 20:24:55 +0100 Subject: [PATCH 12/12] Implemented importing movies. This is still in early stages, however it should work pretty well. --- package.sh | 6 +- src/NzbDrone.Console/ConsoleApp.cs | 2 +- .../Configuration/ConfigFileProvider.cs | 6 +- .../Migration/104_add_moviefiles_table.cs | 31 ++ .../Framework/MigrationController.cs | 1 - src/NzbDrone.Core/Datastore/TableMapping.cs | 20 +- .../Clients/Blackhole/UsenetBlackhole.cs | 2 +- .../Download/CompletedDownloadService.cs | 71 +++-- .../Download/DownloadClientBase.cs | 4 +- .../Download/TorrentClientBase.cs | 4 +- .../DownloadedMovieCommandService.cs | 265 ++++++++++++++++++ .../MediaFiles/EpisodeImport/DetectSample.cs | 57 ++++ .../IImportDecisionEngineSpecification.cs | 2 + .../EpisodeImport/ImportApprovedMovie.cs | 172 ++++++++++++ .../EpisodeImport/ImportDecision.cs | 16 ++ .../EpisodeImport/ImportDecisionMaker.cs | 101 +++++-- .../Specifications/FreeSpaceSpecification.cs | 43 +++ .../Specifications/FullSeasonSpecification.cs | 9 +- .../MatchesFolderSpecification.cs | 34 ++- .../Specifications/NotSampleSpecification.cs | 22 ++ .../NotUnpackingSpecification.cs | 35 +++ .../SameEpisodesImportSpecification.cs | 6 + .../UnverifiedSceneNumberingSpecification.cs | 8 +- .../Specifications/UpgradeSpecification.cs | 5 + .../MediaFiles/Events/MovieDownloadedEvent.cs | 20 ++ .../MediaFiles/Events/MovieFileAddedEvent.cs | 14 + .../Events/MovieFileDeletedEvent.cs | 16 ++ .../Events/MovieFolderCreatedEvent.cs | 20 ++ .../MediaFiles/Events/MovieImportedEvent.cs | 32 +++ .../MediaFiles/MediaFileService.cs | 37 ++- src/NzbDrone.Core/MediaFiles/MovieFile.cs | 29 ++ .../MediaFiles/MovieFileMoveResult.cs | 15 + .../MediaFiles/MovieFileMovingService.cs | 191 +++++++++++++ .../MediaFiles/MovieFileRepository.cs | 32 +++ .../MediaFiles/UpdateMovieFileService.cs | 147 ++++++++++ .../MediaFiles/UpgradeMediaFileService.cs | 38 +++ src/NzbDrone.Core/NzbDrone.Core.csproj | 14 + .../Organizer/FileNameBuilder.cs | 93 +++++- src/NzbDrone.Core/Parser/Model/LocalMovie.cs | 29 ++ src/NzbDrone.Core/Parser/ParsingService.cs | 42 +++ src/NzbDrone.Core/Tv/Movie.cs | 3 + src/NzbDrone.Core/Tv/MovieRepository.cs | 17 +- src/NzbDrone.Core/Tv/MovieService.cs | 25 +- 43 files changed, 1673 insertions(+), 63 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs create mode 100644 src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MovieFile.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs create mode 100644 src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs create mode 100644 src/NzbDrone.Core/Parser/Model/LocalMovie.cs diff --git a/package.sh b/package.sh index 207006c97..65357f083 100644 --- a/package.sh +++ b/package.sh @@ -22,9 +22,9 @@ 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 +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 diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 8740b4298..83040c67c 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -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: 7878) or the user has insufficient permissions"); + Logger.Fatal(exception.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions"); System.Console.WriteLine("Press enter to exit..."); System.Console.ReadLine(); Environment.Exit(1); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index fc429c91b..2eeaf6463 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -303,12 +303,12 @@ namespace NzbDrone.Core.Configuration if (contents.IsNullOrWhiteSpace()) { - throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Sonarr will recreate it."); + throw new InvalidConfigFileException($"{_configFile} is empty. Please delete the config file and Radarr will recreate it."); } if (contents.All(char.IsControl)) { - throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it."); + throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Radarr will recreate it."); } return XDocument.Parse(_diskProvider.ReadAllText(_configFile)); @@ -323,7 +323,7 @@ namespace NzbDrone.Core.Configuration catch (XmlException ex) { - throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Sonarr will recreate it.", ex); + throw new InvalidConfigFileException($"{_configFile} is corrupt is invalid. Please delete the config file and Radarr will recreate it.", ex); } } diff --git a/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs b/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs new file mode 100644 index 000000000..34a455683 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/104_add_moviefiles_table.cs @@ -0,0 +1,31 @@ +using FluentMigrator; +using Marr.Data.Mapping; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Datastore.Extensions; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(104)] + public class add_moviefiles_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("MovieFiles") + .WithColumn("MovieId").AsInt32() + .WithColumn("Path").AsString().Unique() + .WithColumn("Quality").AsString() + .WithColumn("Size").AsInt64() + .WithColumn("DateAdded").AsDateTime() + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("RelativePath").AsString().Nullable(); + + Alter.Table("Movies").AddColumn("MovieFileId").AsInt32().WithDefaultValue(0); + + + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs index 793725e9f..310628715 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs @@ -60,7 +60,6 @@ namespace NzbDrone.Core.Datastore.Migration.Framework sw.Stop(); - _announcer.ElapsedTime(sw.Elapsed); } } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 397703127..65a82948b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -76,10 +76,7 @@ namespace NzbDrone.Core.Datastore .Relationship() .HasOne(s => s.Profile, s => s.ProfileId); - Mapper.Entity().RegisterModel("Movies") - .Ignore(s => s.RootFolderPath) - .Relationship() - .HasOne(s => s.Profile, s => s.ProfileId); + Mapper.Entity().RegisterModel("EpisodeFiles") .Ignore(f => f.Path) @@ -89,6 +86,21 @@ namespace NzbDrone.Core.Datastore query: (db, parent) => db.Query().Where(c => c.EpisodeFileId == parent.Id).ToList()) .HasOne(file => file.Series, file => file.SeriesId); + Mapper.Entity().RegisterModel("MovieFiles") + .Ignore(f => f.Path) + .Relationships.AutoMapICollectionOrComplexProperties() + .For("Movie") + .LazyLoad(condition: parent => parent.Id > 0, + query: (db, parent) => db.Query().Where(c => c.MovieFileId == parent.Id).ToList()) + .HasOne(file => file.Movie, file => file.MovieId); + + Mapper.Entity().RegisterModel("Movies") + .Ignore(s => s.RootFolderPath) + .Relationship() + .HasOne(s => s.Profile, s => s.ProfileId) + .HasOne(m => m.MovieFile, m => m.MovieFileId); + + Mapper.Entity().RegisterModel("Episodes") .Ignore(e => e.SeriesTitle) .Ignore(e => e.Series) diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs index 2cc13a235..8a82fe93a 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/UsenetBlackhole.cs @@ -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, diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 024a41c8b..4e2bebe59 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -27,6 +27,7 @@ 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; @@ -36,6 +37,7 @@ namespace NzbDrone.Core.Download IEventAggregator eventAggregator, IHistoryService historyService, IDownloadedEpisodesImportService downloadedEpisodesImportService, + IDownloadedMovieImportService downloadedMovieImportService, IParsingService parsingService, ISeriesService seriesService, IMovieService movieService, @@ -45,6 +47,7 @@ namespace NzbDrone.Core.Download _eventAggregator = eventAggregator; _historyService = historyService; _downloadedEpisodesImportService = downloadedEpisodesImportService; + _downloadedMovieImportService = downloadedMovieImportService; _parsingService = parsingService; _movieService = movieService; _logger = logger; @@ -64,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; } @@ -126,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.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.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(); - if (importResults.Any(c => c.Result != ImportResultType.Imported)) + trackedDownload.Warn(statusMessages); + } + } + else { - var statusMessages = importResults - .Where(v => v.Result != ImportResultType.Imported) - .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) - .ToArray(); + var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); + + if (importResults.Empty()) + { + trackedDownload.Warn("No files found are eligible for import in {0}", outputPath); + return; + } - trackedDownload.Warn(statusMessages); + if (importResults.Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) + { + trackedDownload.State = TrackedDownloadStage.Imported; + _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); + return; + } + + if (importResults.Any(c => c.Result != ImportResultType.Imported)) + { + var statusMessages = importResults + .Where(v => v.Result != ImportResultType.Imported) + .Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)) + .ToArray(); + + trackedDownload.Warn(statusMessages); + } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 0e48207ba..14f7f1b71 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -133,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) }; } @@ -142,7 +142,7 @@ 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) }; } diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index ce3767c39..70681f992 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -371,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); } @@ -402,7 +402,7 @@ namespace NzbDrone.Core.Download if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) { _logger.Debug( - "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + "{0} did not return the expected InfoHash for '{1}', Radarr could potentially lose track of the download in progress.", Definition.Implementation, remoteEpisode.Release.DownloadUrl); } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs new file mode 100644 index 000000000..fcda97d24 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/DownloadedMovieCommandService.cs @@ -0,0 +1,265 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IDownloadedMovieImportService + { + List ProcessRootFolder(DirectoryInfo directoryInfo); + List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null); + bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie); + } + + public class DownloadedMovieImportService : IDownloadedMovieImportService + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + private readonly IMovieService _movieService; + private readonly IParsingService _parsingService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly IImportApprovedMovie _importApprovedMovie; + private readonly IDetectSample _detectSample; + private readonly Logger _logger; + + public DownloadedMovieImportService(IDiskProvider diskProvider, + IDiskScanService diskScanService, + IMovieService movieService, + IParsingService parsingService, + IMakeImportDecision importDecisionMaker, + IImportApprovedMovie importApprovedMovie, + IDetectSample detectSample, + Logger logger) + { + _diskProvider = diskProvider; + _diskScanService = diskScanService; + _movieService = movieService; + _parsingService = parsingService; + _importDecisionMaker = importDecisionMaker; + _importApprovedMovie = importApprovedMovie; + _detectSample = detectSample; + _logger = logger; + } + + public List ProcessRootFolder(DirectoryInfo directoryInfo) + { + var results = new List(); + + foreach (var subFolder in _diskProvider.GetDirectories(directoryInfo.FullName)) + { + var folderResults = ProcessFolder(new DirectoryInfo(subFolder), ImportMode.Auto, null); + results.AddRange(folderResults); + } + + foreach (var videoFile in _diskScanService.GetVideoFiles(directoryInfo.FullName, false)) + { + var fileResults = ProcessFile(new FileInfo(videoFile), ImportMode.Auto, null); + results.AddRange(fileResults); + } + + return results; + } + + public List ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null) + { + if (_diskProvider.FolderExists(path)) + { + var directoryInfo = new DirectoryInfo(path); + + if (movie == null) + { + return ProcessFolder(directoryInfo, importMode, downloadClientItem); + } + + return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem); + } + + if (_diskProvider.FileExists(path)) + { + var fileInfo = new FileInfo(path); + + if (movie == null) + { + return ProcessFile(fileInfo, importMode, downloadClientItem); + } + + return ProcessFile(fileInfo, importMode, movie, downloadClientItem); + } + + _logger.Error("Import failed, path does not exist or is not accessible by Sonarr: {0}", path); + return new List(); + } + + public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie) + { + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); + + foreach (var videoFile in videoFiles) + { + var episodeParseResult = Parser.Parser.ParseTitle(Path.GetFileName(videoFile)); + + if (episodeParseResult == null) + { + _logger.Warn("Unable to parse file on import: [{0}]", videoFile); + return false; + } + + var size = _diskProvider.GetFileSize(videoFile); + var quality = QualityParser.ParseQuality(videoFile); + + if (!_detectSample.IsSample(movie, quality, videoFile, size, episodeParseResult.IsPossibleSpecialEpisode)) + { + _logger.Warn("Non-sample file detected: [{0}]", videoFile); + return false; + } + } + + if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes())) + { + _logger.Warn("RAR file detected, will require manual cleanup"); + return false; + } + + return true; + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var movie = _parsingService.GetMovie(cleanedUpName); + + if (movie == null) + { + _logger.Debug("Unknown Movie {0}", cleanedUpName); + + return new List + { + UnknownMovieResult("Unknown Movie") + }; + } + + return ProcessFolder(directoryInfo, importMode, movie, downloadClientItem); + } + + private List ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem) + { + if (_movieService.MoviePathExists(directoryInfo.FullName)) + { + _logger.Warn("Unable to process folder that is mapped to an existing show"); + return new List(); + } + + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var folderInfo = Parser.Parser.ParseTitle(directoryInfo.Name); + + if (folderInfo != null) + { + _logger.Debug("{0} folder quality: {1}", cleanedUpName, folderInfo.Quality); + } + + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + + if (downloadClientItem == null) + { + foreach (var videoFile in videoFiles) + { + if (_diskProvider.IsFileLocked(videoFile)) + { + return new List + { + FileIsLockedResult(videoFile) + }; + } + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), movie, folderInfo, true); + var importResults = _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode); + + if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && + importResults.Any(i => i.Result == ImportResultType.Imported) && + ShouldDeleteFolder(directoryInfo, movie)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(directoryInfo.FullName, true); + } + + return importResults; + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, DownloadClientItem downloadClientItem) + { + var movie = _parsingService.GetMovie(Path.GetFileNameWithoutExtension(fileInfo.Name)); + + if (movie == null) + { + _logger.Debug("Unknown Movie for file: {0}", fileInfo.Name); + + return new List + { + UnknownMovieResult(string.Format("Unknown Movie for file: {0}", fileInfo.Name), fileInfo.FullName) + }; + } + + return ProcessFile(fileInfo, importMode, movie, downloadClientItem); + } + + private List ProcessFile(FileInfo fileInfo, ImportMode importMode, Movie movie, DownloadClientItem downloadClientItem) + { + if (Path.GetFileNameWithoutExtension(fileInfo.Name).StartsWith("._")) + { + _logger.Debug("[{0}] starts with '._', skipping", fileInfo.FullName); + + return new List + { + new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, new Rejection("Invalid video file, filename starts with '._'")), "Invalid video file, filename starts with '._'") + }; + } + + if (downloadClientItem == null) + { + if (_diskProvider.IsFileLocked(fileInfo.FullName)) + { + return new List + { + FileIsLockedResult(fileInfo.FullName) + }; + } + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List() { fileInfo.FullName }, movie, null, true); + + return _importApprovedMovie.Import(decisions, true, downloadClientItem, importMode); + } + + private string GetCleanedUpFolderName(string folder) + { + folder = folder.Replace("_UNPACK_", "") + .Replace("_FAILED_", ""); + + return folder; + } + + private ImportResult FileIsLockedResult(string videoFile) + { + _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); + return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, new Rejection("Locked file, try again later")), "Locked file, try again later"); + } + + private ImportResult UnknownMovieResult(string message, string videoFile = null) + { + var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile }; + + return new ImportResult(new ImportDecision(localEpisode, new Rejection("Unknown Movie")), message); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index b517cd76c..27756cf4f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IDetectSample { bool IsSample(Series series, QualityModel quality, string path, long size, bool isSpecial); + bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial); } public class DetectSample : IDetectSample @@ -79,6 +80,57 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return false; } + public bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial) + { + if (isSpecial) + { + _logger.Debug("Special, skipping sample check"); + return false; + } + + var extension = Path.GetExtension(path); + + if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Skipping sample check for .flv file"); + return false; + } + + if (extension != null && extension.Equals(".strm", StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Skipping sample check for .strm file"); + return false; + } + + try + { + var runTime = _videoFileInfoReader.GetRunTime(path); + var minimumRuntime = GetMinimumAllowedRuntime(movie); + + if (runTime.TotalMinutes.Equals(0)) + { + _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path); + return true; + } + + if (runTime.TotalSeconds < minimumRuntime) + { + _logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime); + return true; + } + } + + catch (DllNotFoundException) + { + _logger.Debug("Falling back to file size detection"); + + return CheckSize(size, quality); + } + + _logger.Debug("Runtime is over 90 seconds"); + return false; + } + private bool CheckSize(long size, QualityModel quality) { if (_largeSampleSizeQualities.Contains(quality.Quality)) @@ -99,6 +151,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return false; } + private int GetMinimumAllowedRuntime(Movie movie) + { + return 120; //2 minutes + } + private int GetMinimumAllowedRuntime(Series series) { //Webisodes - 90 seconds diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs index 86abb87b7..4dc6bcaf6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/IImportDecisionEngineSpecification.cs @@ -6,5 +6,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public interface IImportDecisionEngineSpecification { Decision IsSatisfiedBy(LocalEpisode localEpisode); + + Decision IsSatisfiedBy(LocalMovie localMovie); } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs new file mode 100644 index 000000000..6d766a0e3 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedMovie.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Download; +using NzbDrone.Core.Extras; + + +namespace NzbDrone.Core.MediaFiles.EpisodeImport +{ + public interface IImportApprovedMovie + { + List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto); + } + + public class ImportApprovedMovie : IImportApprovedMovie + { + private readonly IUpgradeMediaFiles _episodeFileUpgrader; + private readonly IMediaFileService _mediaFileService; + private readonly IExtraService _extraService; + private readonly IDiskProvider _diskProvider; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + public ImportApprovedMovie(IUpgradeMediaFiles episodeFileUpgrader, + IMediaFileService mediaFileService, + IExtraService extraService, + IDiskProvider diskProvider, + IEventAggregator eventAggregator, + Logger logger) + { + _episodeFileUpgrader = episodeFileUpgrader; + _mediaFileService = mediaFileService; + _extraService = extraService; + _diskProvider = diskProvider; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public List Import(List decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto) + { + var qualifiedImports = decisions.Where(c => c.Approved) + .GroupBy(c => c.LocalMovie.Movie.Id, (i, s) => s + .OrderByDescending(c => c.LocalMovie.Quality, new QualityModelComparer(s.First().LocalMovie.Movie.Profile)) + .ThenByDescending(c => c.LocalMovie.Size)) + .SelectMany(c => c) + .ToList(); + + var importResults = new List(); + + foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalMovie.Size) + .ThenByDescending(e => e.LocalMovie.Size)) + { + var localMovie = importDecision.LocalMovie; + var oldFiles = new List(); + + try + { + //check if already imported + if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie) + .Select(e => e.Id).Contains(localMovie.Movie.Id)) + { + importResults.Add(new ImportResult(importDecision, "Movie has already been imported")); + continue; + } + + var episodeFile = new MovieFile(); + episodeFile.DateAdded = DateTime.UtcNow; + episodeFile.MovieId = localMovie.Movie.Id; + episodeFile.Path = localMovie.Path.CleanFilePath(); + episodeFile.Size = _diskProvider.GetFileSize(localMovie.Path); + episodeFile.Quality = localMovie.Quality; + episodeFile.MediaInfo = localMovie.MediaInfo; + episodeFile.Movie = localMovie.Movie; + episodeFile.ReleaseGroup = localMovie.ParsedEpisodeInfo.ReleaseGroup; + + bool copyOnly; + switch (importMode) + { + default: + case ImportMode.Auto: + copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly; + break; + case ImportMode.Move: + copyOnly = false; + break; + case ImportMode.Copy: + copyOnly = true; + break; + } + + if (newDownload) + { + episodeFile.SceneName = GetSceneName(downloadClientItem, localMovie); + + var moveResult = _episodeFileUpgrader.UpgradeMovieFile(episodeFile, localMovie, copyOnly); + oldFiles = moveResult.OldFiles; + } + else + { + episodeFile.RelativePath = localMovie.Movie.Path.GetRelativePath(episodeFile.Path); + } + + _mediaFileService.Add(episodeFile); + importResults.Add(new ImportResult(importDecision)); + + if (newDownload) + { + //_extraService.ImportExtraFiles(localMovie, episodeFile, copyOnly); TODO update for movie + } + + if (downloadClientItem != null) + { + _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly)); + } + else + { + _eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, episodeFile, newDownload)); + } + + if (newDownload) + { + _eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, episodeFile, oldFiles)); + } + } + catch (Exception e) + { + _logger.Warn(e, "Couldn't import episode " + localMovie); + importResults.Add(new ImportResult(importDecision, "Failed to import episode")); + } + } + + //Adding all the rejected decisions + importResults.AddRange(decisions.Where(c => !c.Approved) + .Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray()))); + + return importResults; + } + + private string GetSceneName(DownloadClientItem downloadClientItem, LocalMovie localMovie) + { + if (downloadClientItem != null) + { + var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title); + + var parsedTitle = Parser.Parser.ParseTitle(title); + + if (parsedTitle != null && !parsedTitle.FullSeason) + { + return title; + } + } + + var fileName = Path.GetFileNameWithoutExtension(localMovie.Path.CleanFilePath()); + + if (SceneChecker.IsSceneTitle(fileName)) + { + return fileName; + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs index 5e4e2ede2..8bb5e78ea 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecision.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public class ImportDecision { public LocalEpisode LocalEpisode { get; private set; } + public LocalMovie LocalMovie { get; private set; } public IEnumerable Rejections { get; private set; } public bool Approved => Rejections.Empty(); @@ -18,5 +19,20 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport LocalEpisode = localEpisode; Rejections = rejections.ToList(); } + + public ImportDecision(LocalMovie localMovie, params Rejection[] rejections) + { + LocalMovie = localMovie; + Rejections = rejections.ToList(); + LocalEpisode = new LocalEpisode + { + Quality = localMovie.Quality, + ExistingFile = localMovie.ExistingFile, + MediaInfo = localMovie.MediaInfo, + ParsedEpisodeInfo = localMovie.ParsedEpisodeInfo, + Path = localMovie.Path, + Size = localMovie.Size + }; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index 5594ebe97..52ffdfa07 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -98,36 +98,32 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { ImportDecision decision = null; - /*try + try { - var localEpisode = _parsingService.GetLocalEpisode(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); + var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource); - if (localEpisode != null) + if (localMovie != null) { - localEpisode.Quality = GetQuality(folderInfo, localEpisode.Quality, movie); - localEpisode.Size = _diskProvider.GetFileSize(file); + localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie); + localMovie.Size = _diskProvider.GetFileSize(file); - _logger.Debug("Size: {0}", localEpisode.Size); + _logger.Debug("Size: {0}", localMovie.Size); //TODO: make it so media info doesn't ruin the import process of a new series if (sceneSource) { - localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); - } - - if (localEpisode.Episodes.Empty()) - { - decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); + localMovie.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); + decision = GetDecision(localMovie); } else { - decision = GetDecision(localEpisode); + decision = GetDecision(localMovie); } } else { - localEpisode = new LocalEpisode(); + var localEpisode = new LocalEpisode(); localEpisode.Path = file; decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); @@ -139,13 +135,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport var localEpisode = new LocalEpisode { Path = file }; decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); - }*/ + } + + //LocalMovie nullMovie = null; - decision = new ImportDecision(null, new Rejection("IMPLEMENTATION MISSING!!!")); + //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; @@ -204,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 @@ -292,6 +325,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport }) == 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)) @@ -303,6 +347,31 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport return fileQuality; } + private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Movie movie) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.Quality.Quality == Quality.Unknown) + { + return false; + } + + if (fileQuality.QualitySource == QualitySource.Extension) + { + return true; + } + + if (new QualityModelComparer(movie.Profile).Compare(folderInfo.Quality, fileQuality) > 0) + { + return true; + } + + return false; + } + private bool UseFolderQuality(ParsedEpisodeInfo folderInfo, QualityModel fileQuality, Series series) { if (folderInfo == null) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs index 158059e29..1e8432e84 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FreeSpaceSpecification.cs @@ -63,5 +63,48 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + if (_configService.SkipFreeSpaceCheckWhenImporting) + { + _logger.Debug("Skipping free space check when importing"); + return Decision.Accept(); + } + + try + { + if (localMovie.ExistingFile) + { + _logger.Debug("Skipping free space check for existing episode"); + return Decision.Accept(); + } + + var path = Directory.GetParent(localMovie.Movie.Path); + var freeSpace = _diskProvider.GetAvailableSpace(path.FullName); + + if (!freeSpace.HasValue) + { + _logger.Debug("Free space check returned an invalid result for: {0}", path); + return Decision.Accept(); + } + + if (freeSpace < localMovie.Size + 100.Megabytes()) + { + _logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size); + return Decision.Reject("Not enough free space"); + } + } + catch (DirectoryNotFoundException ex) + { + _logger.Error("Unable to check free disk space while importing. " + ex.Message); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to check free disk space while importing: " + localMovie.Path); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 853f6b1b7..2daeda6cb 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -17,11 +17,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { if (localEpisode.ParsedEpisodeInfo.FullSeason) { - //_logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah - //return Decision.Reject("Single episode file contains all episodes in seasons"); + _logger.Debug("Single episode file detected as containing all episodes in the season"); //Not needed for Movies mwhahahahah + return Decision.Reject("Single episode file contains all episodes in seasons"); } return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs index 79ef96f88..55a8da073 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesFolderSpecification.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; @@ -14,6 +15,37 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications { _logger = logger; } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + if (localMovie.ExistingFile) + { + return Decision.Accept(); + } + + var dirInfo = new FileInfo(localMovie.Path).Directory; + + if (dirInfo == null) + { + return Decision.Accept(); + } + + var folderInfo = Parser.Parser.ParseTitle(dirInfo.Name); + + if (folderInfo == null) + { + return Decision.Accept(); + } + + if (folderInfo.FullSeason) + { + return Decision.Accept(); + } + + return Decision.Accept(); + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index c7b61d802..536ea093a 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -37,5 +37,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + if (localEpisode.ExistingFile) + { + _logger.Debug("Existing file, skipping sample check"); + return Decision.Accept(); + } + + var sample = _detectSample.IsSample(localEpisode.Movie, + localEpisode.Quality, + localEpisode.Path, + localEpisode.Size, + false); + + if (sample) + { + return Decision.Reject("Sample"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index 2260ed71a..a19359457 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -56,5 +56,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + if (localEpisode.ExistingFile) + { + _logger.Debug("{0} is in series folder, skipping unpacking check", localEpisode.Path); + return Decision.Accept(); + } + + foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) + { + DirectoryInfo parent = Directory.GetParent(localEpisode.Path); + while (parent != null) + { + if (parent.Name.StartsWith(workingFolder)) + { + if (OsInfo.IsNotWindows) + { + _logger.Debug("{0} is still being unpacked", localEpisode.Path); + return Decision.Reject("File is still being unpacked"); + } + + if (_diskProvider.FileGetLastWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) + { + _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); + return Decision.Reject("File is still being unpacked"); + } + } + + parent = parent.Parent; + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs index ee6c02c53..c24d62aaa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -27,5 +28,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger.Debug("Episode file on disk contains more episodes than this file contains"); return Decision.Reject("Episode file on disk contains more episodes than this file contains"); } + + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs index ce65eb304..85becc0ba 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UnverifiedSceneNumberingSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; @@ -13,6 +14,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications _logger = logger; } + public Decision IsSatisfiedBy(LocalMovie localMovie) + { + return Decision.Accept(); + } + public Decision IsSatisfiedBy(LocalEpisode localEpisode) { if (localEpisode.ExistingFile) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs index 3d07306af..b2d2c2c33 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/UpgradeSpecification.cs @@ -26,5 +26,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(LocalMovie localEpisode) + { + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs new file mode 100644 index 000000000..427996088 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieDownloadedEvent.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieDownloadedEvent : IEvent + { + public LocalMovie Movie { get; private set; } + public MovieFile MovieFile { get; private set; } + public List OldFiles { get; private set; } + + public MovieDownloadedEvent(LocalMovie episode, MovieFile episodeFile, List oldFiles) + { + Movie = episode; + MovieFile = episodeFile; + OldFiles = oldFiles; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs new file mode 100644 index 000000000..17f93dc0b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFileAddedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFileAddedEvent : IEvent + { + public MovieFile MovieFile { get; private set; } + + public MovieFileAddedEvent(MovieFile episodeFile) + { + MovieFile = episodeFile; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs new file mode 100644 index 000000000..232f1686f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFileDeletedEvent.cs @@ -0,0 +1,16 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFileDeletedEvent : IEvent + { + public MovieFile MovieFile { get; private set; } + public DeleteMediaFileReason Reason { get; private set; } + + public MovieFileDeletedEvent(MovieFile episodeFile, DeleteMediaFileReason reason) + { + MovieFile = episodeFile; + Reason = reason; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs new file mode 100644 index 000000000..a26031413 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieFolderCreatedEvent.cs @@ -0,0 +1,20 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieFolderCreatedEvent : IEvent + { + public Movie Movie { get; private set; } + public MovieFile MovieFile { get; private set; } + public string SeriesFolder { get; set; } + public string SeasonFolder { get; set; } + public string MovieFolder { get; set; } + + public MovieFolderCreatedEvent(Movie movie, MovieFile episodeFile) + { + Movie = movie; + MovieFile = episodeFile; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs new file mode 100644 index 000000000..91df27e3c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieImportedEvent.cs @@ -0,0 +1,32 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieImportedEvent : IEvent + { + public LocalMovie MovieInfo { get; private set; } + public MovieFile ImportedMovie { get; private set; } + public bool NewDownload { get; private set; } + public string DownloadClient { get; private set; } + public string DownloadId { get; private set; } + public bool IsReadOnly { get; set; } + + public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload) + { + MovieInfo = episodeInfo; + ImportedMovie = importedMovie; + NewDownload = newDownload; + } + + public MovieImportedEvent(LocalMovie episodeInfo, MovieFile importedMovie, bool newDownload, string downloadClient, string downloadId, bool isReadOnly) + { + MovieInfo = episodeInfo; + ImportedMovie = importedMovie; + NewDownload = newDownload; + DownloadClient = downloadClient; + DownloadId = downloadId; + IsReadOnly = isReadOnly; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index 051e85863..e3e82daa5 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -7,16 +7,20 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Tv.Events; using NzbDrone.Common; +using System; namespace NzbDrone.Core.MediaFiles { public interface IMediaFileService { + MovieFile Add(MovieFile episodeFile); + void Update(MovieFile episodeFile); + void Delete(MovieFile episodeFile, DeleteMediaFileReason reason); EpisodeFile Add(EpisodeFile episodeFile); void Update(EpisodeFile episodeFile); void Delete(EpisodeFile episodeFile, DeleteMediaFileReason reason); List GetFilesBySeries(int seriesId); - List GetFilesByMovie(int movieId); + List GetFilesByMovie(int movieId); List GetFilesBySeason(int seriesId, int seasonNumber); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Series series); @@ -30,12 +34,14 @@ namespace NzbDrone.Core.MediaFiles { private readonly IEventAggregator _eventAggregator; private readonly IMediaFileRepository _mediaFileRepository; + private readonly IMovieFileRepository _movieFileRepository; private readonly Logger _logger; - public MediaFileService(IMediaFileRepository mediaFileRepository, IEventAggregator eventAggregator, Logger logger) + public MediaFileService(IMediaFileRepository mediaFileRepository, IMovieFileRepository movieFileRepository, IEventAggregator eventAggregator, Logger logger) { _mediaFileRepository = mediaFileRepository; _eventAggregator = eventAggregator; + _movieFileRepository = movieFileRepository; _logger = logger; } @@ -61,14 +67,24 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new EpisodeFileDeletedEvent(episodeFile, reason)); } + public void Delete(MovieFile episodeFile, DeleteMediaFileReason reason) + { + //Little hack so we have the episodes and series attached for the event consumers + episodeFile.Movie.LazyLoad(); + episodeFile.Path = Path.Combine(episodeFile.Movie.Value.Path, episodeFile.RelativePath); + + _movieFileRepository.Delete(episodeFile); + _eventAggregator.PublishEvent(new MovieFileDeletedEvent(episodeFile, reason)); + } + public List GetFilesBySeries(int seriesId) { return _mediaFileRepository.GetFilesBySeries(seriesId); } - public List GetFilesByMovie(int movieId) + public List GetFilesByMovie(int movieId) { - return _mediaFileRepository.GetFilesBySeries(movieId); //TODO: Update implementation for movie files. + return _movieFileRepository.GetFilesByMovie(movieId); //TODO: Update implementation for movie files. } public List GetFilesBySeason(int seriesId, int seasonNumber) @@ -114,5 +130,18 @@ namespace NzbDrone.Core.MediaFiles var files = GetFilesBySeries(message.Series.Id); _mediaFileRepository.DeleteMany(files); } + + public MovieFile Add(MovieFile episodeFile) + { + var addedFile = _movieFileRepository.Insert(episodeFile); + _eventAggregator.PublishEvent(new MovieFileAddedEvent(addedFile)); + return addedFile; + } + + public void Update(MovieFile episodeFile) + { + _movieFileRepository.Update(episodeFile); + } + } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs new file mode 100644 index 000000000..dfb753ab6 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Marr.Data; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; + +namespace NzbDrone.Core.MediaFiles +{ + public class MovieFile : ModelBase + { + public int MovieId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public LazyLoaded Movie { get; set; } + + public override string ToString() + { + return string.Format("[{0}] {1}", Id, RelativePath); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs new file mode 100644 index 000000000..a52faed61 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMoveResult.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.MediaFiles +{ + public class MovieFileMoveResult + { + public MovieFileMoveResult() + { + OldFiles = new List(); + } + + public MovieFile MovieFile { get; set; } + public List OldFiles { get; set; } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs new file mode 100644 index 000000000..cf8acd6f9 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IMoveMovieFiles + { + MovieFile MoveMovieFile(MovieFile movieFile, Movie movie); + MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie); + MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie); + } + + public class MovieFileMovingService : IMoveMovieFiles + { + private readonly IMovieService _movieService; + private readonly IUpdateMovieFileService _updateMovieFileService; + private readonly IBuildFileNames _buildFileNames; + private readonly IDiskTransferService _diskTransferService; + private readonly IDiskProvider _diskProvider; + private readonly IMediaFileAttributeService _mediaFileAttributeService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public MovieFileMovingService(IMovieService movieService, + IUpdateMovieFileService updateMovieFileService, + IBuildFileNames buildFileNames, + IDiskTransferService diskTransferService, + IDiskProvider diskProvider, + IMediaFileAttributeService mediaFileAttributeService, + IEventAggregator eventAggregator, + IConfigService configService, + Logger logger) + { + _movieService = movieService; + _updateMovieFileService = updateMovieFileService; + _buildFileNames = buildFileNames; + _diskTransferService = diskTransferService; + _diskProvider = diskProvider; + _mediaFileAttributeService = mediaFileAttributeService; + _eventAggregator = eventAggregator; + _configService = configService; + _logger = logger; + } + + public MovieFile MoveMovieFile(MovieFile movieFile, Movie movie) + { + var newFileName = _buildFileNames.BuildFileName(movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(movie, newFileName, Path.GetExtension(movieFile.RelativePath)); + + EnsureMovieFolder(movieFile, movie, filePath); + + _logger.Debug("Renaming movie file: {0} to {1}", movieFile, filePath); + + return TransferFile(movieFile, movie, filePath, TransferMode.Move); + } + + public MovieFile MoveMovieFile(MovieFile movieFile, LocalMovie localMovie) + { + var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path)); + + EnsureMovieFolder(movieFile, localMovie, filePath); + + _logger.Debug("Moving movie file: {0} to {1}", movieFile.Path, filePath); + + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Move); + } + + public MovieFile CopyMovieFile(MovieFile movieFile, LocalMovie localMovie) + { + var newFileName = _buildFileNames.BuildFileName(localMovie.Movie, movieFile); + var filePath = _buildFileNames.BuildFilePath(localMovie.Movie, newFileName, Path.GetExtension(localMovie.Path)); + + EnsureMovieFolder(movieFile, localMovie, filePath); + + if (_configService.CopyUsingHardlinks) + { + _logger.Debug("Hardlinking movie file: {0} to {1}", movieFile.Path, filePath); + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.HardLinkOrCopy); + } + + _logger.Debug("Copying movie file: {0} to {1}", movieFile.Path, filePath); + return TransferFile(movieFile, localMovie.Movie, filePath, TransferMode.Copy); + } + + private MovieFile TransferFile(MovieFile movieFile, Movie movie, string destinationFilePath, TransferMode mode) + { + Ensure.That(movieFile, () => movieFile).IsNotNull(); + Ensure.That(movie,() => movie).IsNotNull(); + Ensure.That(destinationFilePath, () => destinationFilePath).IsValidPath(); + + var movieFilePath = movieFile.Path ?? Path.Combine(movie.Path, movieFile.RelativePath); + + if (!_diskProvider.FileExists(movieFilePath)) + { + throw new FileNotFoundException("Movie file path does not exist", movieFilePath); + } + + if (movieFilePath == destinationFilePath) + { + throw new SameFilenameException("File not moved, source and destination are the same", movieFilePath); + } + + _diskTransferService.TransferFile(movieFilePath, destinationFilePath, mode); + + movieFile.RelativePath = movie.Path.GetRelativePath(destinationFilePath); + + _updateMovieFileService.ChangeFileDateForFile(movieFile, movie); + + try + { + _mediaFileAttributeService.SetFolderLastWriteTime(movie.Path, movieFile.DateAdded); + } + + catch (Exception ex) + { + _logger.Warn(ex, "Unable to set last write time"); + } + + _mediaFileAttributeService.SetFilePermissions(destinationFilePath); + + return movieFile; + } + + private void EnsureMovieFolder(MovieFile movieFile, LocalMovie localMovie, string filePath) + { + EnsureMovieFolder(movieFile, localMovie.Movie, filePath); + } + + private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath) + { + var movieFolder = Path.GetDirectoryName(filePath); + var rootFolder = new OsPath(movieFolder).Directory.FullPath; + + if (!_diskProvider.FolderExists(rootFolder)) + { + throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + } + + var changed = false; + var newEvent = new MovieFolderCreatedEvent(movie, movieFile); + + if (!_diskProvider.FolderExists(movieFolder)) + { + CreateFolder(movieFolder); + newEvent.SeriesFolder = movieFolder; + changed = true; + } + + if (changed) + { + _eventAggregator.PublishEvent(newEvent); + } + } + + private void CreateFolder(string directoryName) + { + Ensure.That(directoryName, () => directoryName).IsNotNullOrWhiteSpace(); + + var parentFolder = new OsPath(directoryName).Directory.FullPath; + if (!_diskProvider.FolderExists(parentFolder)) + { + CreateFolder(parentFolder); + } + + try + { + _diskProvider.CreateFolder(directoryName); + } + catch (IOException ex) + { + _logger.Error(ex, "Unable to create directory: " + directoryName); + } + + _mediaFileAttributeService.SetFolderPermissions(directoryName); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs b/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs new file mode 100644 index 000000000..9ed89b85f --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieFileRepository.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + + +namespace NzbDrone.Core.MediaFiles +{ + public interface IMovieFileRepository : IBasicRepository + { + List GetFilesByMovie(int movieId); + List GetFilesWithoutMediaInfo(); + } + + + public class MovieFileRepository : BasicRepository, IMovieFileRepository + { + public MovieFileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public List GetFilesByMovie(int movieId) + { + return Query.Where(c => c.MovieId == movieId).ToList(); + } + + public List GetFilesWithoutMediaInfo() + { + return Query.Where(c => c.MediaInfo == null).ToList(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs new file mode 100644 index 000000000..af45d8831 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Exceptron; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles +{ + public interface IUpdateMovieFileService + { + void ChangeFileDateForFile(MovieFile movieFile, Movie movie); + } + + public class UpdateMovieFileService : IUpdateMovieFileService, + IHandle + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly IMovieService _movieService; + private readonly Logger _logger; + + public UpdateMovieFileService(IDiskProvider diskProvider, + IConfigService configService, + IMovieService movieService, + Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _movieService = movieService; + _logger = logger; + } + + public void ChangeFileDateForFile(MovieFile movieFile, Movie movie) + { + ChangeFileDate(movieFile, movie); + } + + private bool ChangeFileDate(MovieFile movieFile, Movie movie) + { + var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath); + + return false; + } + + public void Handle(SeriesScannedEvent message) + { + if (_configService.FileDate == FileDateType.None) + { + return; + } + + /* var movies = _movieService.MoviesWithFiles(message.Series.Id); + + var movieFiles = new List(); + var updated = new List(); + + foreach (var group in movies.GroupBy(e => e.MovieFileId)) + { + var moviesInFile = group.Select(e => e).ToList(); + var movieFile = moviesInFile.First().MovieFile; + + movieFiles.Add(movieFile); + + if (ChangeFileDate(movieFile, message.Series, moviesInFile)) + { + updated.Add(movieFile); + } + } + + if (updated.Any()) + { + _logger.ProgressDebug("Changed file date for {0} files of {1} in {2}", updated.Count, movieFiles.Count, message.Series.Title); + } + + else + { + _logger.ProgressDebug("No file dates changed for {0}", message.Series.Title); + }*/ + } + + private bool ChangeFileDateToLocalAirDate(string filePath, string fileDate, string fileTime) + { + DateTime airDate; + + if (DateTime.TryParse(fileDate + ' ' + fileTime, out airDate)) + { + // avoiding false +ve checks and set date skewing by not using UTC (Windows) + DateTime oldDateTime = _diskProvider.FileGetLastWrite(filePath); + + if (!DateTime.Equals(airDate, oldDateTime)) + { + try + { + _diskProvider.FileSetLastWriteTime(filePath, airDate); + _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldDateTime, airDate); + + return true; + } + + catch (Exception ex) + { + _logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); + } + } + } + + else + { + _logger.Debug("Could not create valid date to change file [{0}]", filePath); + } + + return false; + } + + private bool ChangeFileDateToUtcAirDate(string filePath, DateTime airDateUtc) + { + DateTime oldLastWrite = _diskProvider.FileGetLastWrite(filePath); + + if (!DateTime.Equals(airDateUtc, oldLastWrite)) + { + try + { + _diskProvider.FileSetLastWriteTime(filePath, airDateUtc); + _logger.Debug("Date of file [{0}] changed from '{1}' to '{2}'", filePath, oldLastWrite, airDateUtc); + + return true; + } + + catch (Exception ex) + { + ex.ExceptronIgnoreOnMono(); + _logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); + } + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 95f245e3e..b8cd9f36d 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles public interface IUpgradeMediaFiles { EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); + MovieFileMoveResult UpgradeMovieFile(MovieFile movieFile, LocalMovie localMovie, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -16,22 +17,59 @@ namespace NzbDrone.Core.MediaFiles private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; private readonly IMoveEpisodeFiles _episodeFileMover; + private readonly IMoveMovieFiles _movieFileMover; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; public UpgradeMediaFileService(IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, IMoveEpisodeFiles episodeFileMover, + IMoveMovieFiles movieFileMover, IDiskProvider diskProvider, Logger logger) { _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; _episodeFileMover = episodeFileMover; + _movieFileMover = movieFileMover; _diskProvider = diskProvider; _logger = logger; } + public MovieFileMoveResult UpgradeMovieFile(MovieFile episodeFile, LocalMovie localEpisode, bool copyOnly = false) + { + var moveFileResult = new MovieFileMoveResult(); + var existingFile = localEpisode.Movie.MovieFile; + + if (existingFile.IsLoaded) + { + var file = existingFile.Value; + var episodeFilePath = Path.Combine(localEpisode.Movie.Path, file.RelativePath); + + if (_diskProvider.FileExists(episodeFilePath)) + { + _logger.Debug("Removing existing episode file: {0}", file); + _recycleBinProvider.DeleteFile(episodeFilePath); + } + + moveFileResult.OldFiles.Add(file); + _mediaFileService.Delete(file, DeleteMediaFileReason.Upgrade); + } + + + + if (copyOnly) + { + moveFileResult.MovieFile = _movieFileMover.CopyMovieFile(episodeFile, localEpisode); + } + else + { + moveFileResult.MovieFile= _movieFileMover.MoveMovieFile(episodeFile, localEpisode); + } + + return moveFileResult; + } + public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) { var moveFileResult = new EpisodeFileMoveResult(); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 18f0b1ae7..cb3dfcba2 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -249,6 +249,7 @@ + @@ -700,6 +701,17 @@ + + + + + + + + + + + @@ -762,6 +774,7 @@ + @@ -875,6 +888,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 4d7773ad7..bac486ec1 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -17,6 +17,8 @@ namespace NzbDrone.Core.Organizer public interface IBuildFileNames { string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); + string BuildFilePath(Movie movie, string fileName, string extension); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); string BuildSeasonPath(Series series, int seasonNumber); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); @@ -137,6 +139,66 @@ namespace NzbDrone.Core.Organizer return fileName; } + public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(movieFile); + } + + /*if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) + { + throw new NamingFormatException("Standard episode format cannot be empty"); + } + + if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) + { + throw new NamingFormatException("Daily episode format cannot be empty"); + } + + if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) + { + throw new NamingFormatException("Anime episode format cannot be empty"); + }*/ + + /*var pattern = namingConfig.StandardEpisodeFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); + + if (series.SeriesType == SeriesTypes.Daily) + { + pattern = namingConfig.DailyEpisodeFormat; + } + + if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) + { + pattern = namingConfig.AnimeEpisodeFormat; + } + + pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddSeriesTokens(tokenHandlers, series); + AddEpisodeTokens(tokenHandlers, episodes); + AddEpisodeFileTokens(tokenHandlers, episodeFile); + AddQualityTokens(tokenHandlers, series, episodeFile); + AddMediaInfoTokens(tokenHandlers, episodeFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty);*/ + + //TODO: Update namingConfig for Movies! + + return GetOriginalTitle(movieFile); + } + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) { Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); @@ -146,6 +208,15 @@ namespace NzbDrone.Core.Organizer return Path.Combine(path, fileName + extension); } + public string BuildFilePath(Movie movie, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = movie.Path; + + return Path.Combine(path, fileName + extension); + } + public string BuildSeasonPath(Series series, int seasonNumber) { var path = series.Path; @@ -246,7 +317,7 @@ namespace NzbDrone.Core.Organizer public string GetMovieFolder(Movie movie) { - return CleanFolderName(Parser.Parser.CleanSeriesTitle(movie.Title)); + return CleanFolderName(movie.Title); } public static string CleanTitle(string title) @@ -774,6 +845,26 @@ namespace NzbDrone.Core.Organizer return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); } + + private string GetOriginalTitle(MovieFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(MovieFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } } internal sealed class TokenMatch diff --git a/src/NzbDrone.Core/Parser/Model/LocalMovie.cs b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs new file mode 100644 index 000000000..3f5c2344c --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/LocalMovie.cs @@ -0,0 +1,29 @@ +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.MediaFiles.MediaInfo; + +namespace NzbDrone.Core.Parser.Model +{ + public class LocalMovie + { + public LocalMovie() + { + } + + public string Path { get; set; } + public long Size { get; set; } + public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } + public Movie Movie { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public bool ExistingFile { get; set; } + + + public override string ToString() + { + return Path; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index cc9ca8447..5466bcb5a 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -15,6 +15,8 @@ namespace NzbDrone.Core.Parser { LocalEpisode GetLocalEpisode(string filename, Series series); LocalEpisode GetLocalEpisode(string filename, Series series, ParsedEpisodeInfo folderInfo, bool sceneSource); + LocalMovie GetLocalMovie(string filename, Movie movie); + LocalMovie GetLocalMovie(string filename, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource); Series GetSeries(string title); Movie GetMovie(string title); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); @@ -99,6 +101,46 @@ namespace NzbDrone.Core.Parser }; } + public LocalMovie GetLocalMovie(string filename, Movie movie) + { + return GetLocalMovie(filename, movie, null, false); + } + + public LocalMovie GetLocalMovie(string filename, Movie movie, ParsedEpisodeInfo folderInfo, bool sceneSource) + { + ParsedEpisodeInfo parsedEpisodeInfo; + + if (folderInfo != null) + { + parsedEpisodeInfo = folderInfo.JsonClone(); + parsedEpisodeInfo.Quality = QualityParser.ParseQuality(Path.GetFileName(filename)); + } + + else + { + parsedEpisodeInfo = Parser.ParsePath(filename); + } + + if (parsedEpisodeInfo == null) + { + if (MediaFileExtensions.Extensions.Contains(Path.GetExtension(filename))) + { + _logger.Warn("Unable to parse episode info from path {0}", filename); + } + + return null; + } + + return new LocalMovie + { + Movie = movie, + Quality = parsedEpisodeInfo.Quality, + Path = filename, + ParsedEpisodeInfo = parsedEpisodeInfo, + ExistingFile = movie.Path.IsParentPath(filename) + }; + } + public Series GetSeries(string title) { var parsedEpisodeInfo = Parser.ParseTitle(title); diff --git a/src/NzbDrone.Core/Tv/Movie.cs b/src/NzbDrone.Core/Tv/Movie.cs index 9409ae23e..d5b49cda0 100644 --- a/src/NzbDrone.Core/Tv/Movie.cs +++ b/src/NzbDrone.Core/Tv/Movie.cs @@ -4,6 +4,7 @@ using Marr.Data; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Profiles; +using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Tv { @@ -41,6 +42,8 @@ namespace NzbDrone.Core.Tv public LazyLoaded Profile { get; set; } public HashSet Tags { get; set; } public AddMovieOptions AddOptions { get; set; } + public LazyLoaded MovieFile { get; set; } + public int MovieFileId { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs index 281152b05..f6c4b0ceb 100644 --- a/src/NzbDrone.Core/Tv/MovieRepository.cs +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Linq; +using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -11,6 +13,8 @@ namespace NzbDrone.Core.Tv Movie FindByTitle(string cleanTitle); Movie FindByTitle(string cleanTitle, int year); Movie FindByImdbId(string imdbid); + List GetMoviesByFileId(int fileId); + void SetFileId(int fileId, int movieId); } public class MovieRepository : BasicRepository, IMovieRepository @@ -46,5 +50,16 @@ namespace NzbDrone.Core.Tv { return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault(); } + + public List GetMoviesByFileId(int fileId) + { + return Query.Where(m => m.MovieFileId == fileId).ToList(); + } + + public void SetFileId(int episodeId, int fileId) + { + SetFields(new Movie { Id = episodeId, MovieFileId = fileId }, movie => movie.MovieFileId); + } + } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index d3ec007cb..e255cb215 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -10,6 +10,8 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; namespace NzbDrone.Core.Tv { @@ -22,6 +24,7 @@ namespace NzbDrone.Core.Tv Movie FindByTitle(string title); Movie FindByTitle(string title, int year); Movie FindByTitleInexact(string title); + Movie GetMovieByFileId(int fileId); void DeleteMovie(int movieId, bool deleteFiles); List GetAllMovies(); Movie UpdateMovie(Movie movie); @@ -30,7 +33,8 @@ namespace NzbDrone.Core.Tv void RemoveAddOptions(Movie movie); } - public class MovieService : IMovieService + public class MovieService : IMovieService, IHandle, + IHandle { private readonly IMovieRepository _movieRepository; private readonly IEventAggregator _eventAggregator; @@ -195,5 +199,24 @@ namespace NzbDrone.Core.Tv { _movieRepository.SetFields(movie, s => s.AddOptions); } + + public void Handle(MovieFileAddedEvent message) + { + _movieRepository.SetFileId(message.MovieFile.Id, message.MovieFile.Movie.Value.Id); + _logger.Debug("Linking [{0}] > [{1}]", message.MovieFile.RelativePath, message.MovieFile.Movie.Value); + } + + public void Handle(MovieFileDeletedEvent message) + { + var movie = _movieRepository.GetMoviesByFileId(message.MovieFile.Id).First(); + movie.MovieFileId = 0; + + UpdateMovie(movie); + } + + public Movie GetMovieByFileId(int fileId) + { + return _movieRepository.GetMoviesByFileId(fileId).First(); + } } }