First implementation of custom database table for movies.Some things are not yet working quite well (e.g. search clears when movies are added.). Also movies cannot yet be looked up!

pull/12/head
Leonardo Galli 8 years ago
parent 74ca6149e3
commit 5ebfac6cc8

@ -233,6 +233,7 @@
<Compile Include="Series\SeriesEditorModule.cs" />
<Compile Include="Series\MovieLookupModule.cs" />
<Compile Include="Series\SeriesLookupModule.cs" />
<Compile Include="Series\MovieModule.cs" />
<Compile Include="Series\SeriesModule.cs" />
<Compile Include="Series\MovieResource.cs" />
<Compile Include="Series\SeriesResource.cs" />

@ -5,7 +5,7 @@ using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource;
using System.Linq;
namespace NzbDrone.Api.Series
namespace NzbDrone.Api.Movie
{
public class MovieLookupModule : NzbDroneRestModule<MovieResource>
{

@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MovieStats;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Validation;
using NzbDrone.SignalR;
namespace NzbDrone.Api.Movie
{
public class MovieModule : NzbDroneRestModuleWithSignalR<MovieResource, Core.Tv.Movie>,
IHandle<EpisodeImportedEvent>,
IHandle<EpisodeFileDeletedEvent>,
IHandle<MovieUpdatedEvent>,
IHandle<MovieEditedEvent>,
IHandle<MovieDeletedEvent>,
IHandle<MovieRenamedEvent>,
IHandle<MediaCoversUpdatedEvent>
{
private readonly IMovieService _moviesService;
private readonly IMovieStatisticsService _moviesStatisticsService;
private readonly IMapCoversToLocal _coverMapper;
public MovieModule(IBroadcastSignalRMessage signalRBroadcaster,
IMovieService moviesService,
IMovieStatisticsService moviesStatisticsService,
ISceneMappingService sceneMappingService,
IMapCoversToLocal coverMapper,
RootFolderValidator rootFolderValidator,
MoviePathValidator moviesPathValidator,
MovieExistsValidator moviesExistsValidator,
DroneFactoryValidator droneFactoryValidator,
MovieAncestorValidator moviesAncestorValidator,
ProfileExistsValidator profileExistsValidator
)
: base(signalRBroadcaster)
{
_moviesService = moviesService;
_moviesStatisticsService = moviesStatisticsService;
_coverMapper = coverMapper;
GetResourceAll = AllMovie;
GetResourceById = GetMovie;
CreateResource = AddMovie;
UpdateResource = UpdateMovie;
DeleteResource = DeleteMovie;
Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId));
SharedValidator.RuleFor(s => s.Path)
.Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
.SetValidator(rootFolderValidator)
.SetValidator(moviesPathValidator)
.SetValidator(droneFactoryValidator)
.SetValidator(moviesAncestorValidator)
.When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Title).NotEmpty();
PostValidator.RuleFor(s => s.ImdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator);
PutValidator.RuleFor(s => s.Path).IsValidPath();
}
private MovieResource GetMovie(int id)
{
var movies = _moviesService.GetMovie(id);
return MapToResource(movies);
}
private MovieResource MapToResource(Core.Tv.Movie movies)
{
if (movies == null) return null;
var resource = movies.ToResource();
MapCoversToLocal(resource);
FetchAndLinkMovieStatistics(resource);
PopulateAlternateTitles(resource);
return resource;
}
private List<MovieResource> AllMovie()
{
var moviesStats = _moviesStatisticsService.MovieStatistics();
var moviesResources = _moviesService.GetAllMovies().ToResource();
MapCoversToLocal(moviesResources.ToArray());
LinkMovieStatistics(moviesResources, moviesStats);
PopulateAlternateTitles(moviesResources);
return moviesResources;
}
private int AddMovie(MovieResource moviesResource)
{
var model = moviesResource.ToModel();
return _moviesService.AddMovie(model).Id;
}
private void UpdateMovie(MovieResource moviesResource)
{
var model = moviesResource.ToModel(_moviesService.GetMovie(moviesResource.Id));
_moviesService.UpdateMovie(model);
BroadcastResourceChange(ModelAction.Updated, moviesResource);
}
private void DeleteMovie(int id)
{
var deleteFiles = false;
var deleteFilesQuery = Request.Query.deleteFiles;
if (deleteFilesQuery.HasValue)
{
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value);
}
_moviesService.DeleteMovie(id, deleteFiles);
}
private void MapCoversToLocal(params MovieResource[] movies)
{
foreach (var moviesResource in movies)
{
_coverMapper.ConvertToLocalUrls(moviesResource.Id, moviesResource.Images);
}
}
private void FetchAndLinkMovieStatistics(MovieResource resource)
{
LinkMovieStatistics(resource, _moviesStatisticsService.MovieStatistics(resource.Id));
}
private void LinkMovieStatistics(List<MovieResource> resources, List<MovieStatistics> moviesStatistics)
{
var dictMovieStats = moviesStatistics.ToDictionary(v => v.MovieId);
foreach (var movies in resources)
{
var stats = dictMovieStats.GetValueOrDefault(movies.Id);
if (stats == null) continue;
LinkMovieStatistics(movies, stats);
}
}
private void LinkMovieStatistics(MovieResource resource, MovieStatistics moviesStatistics)
{
resource.SizeOnDisk = moviesStatistics.SizeOnDisk;
}
private void PopulateAlternateTitles(List<MovieResource> resources)
{
foreach (var resource in resources)
{
PopulateAlternateTitles(resource);
}
}
private void PopulateAlternateTitles(MovieResource resource)
{
//var mappings = null;//_sceneMappingService.FindByTvdbId(resource.TvdbId);
//if (mappings == null) return;
//resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList();
}
public void Handle(EpisodeImportedEvent message)
{
//BroadcastResourceChange(ModelAction.Updated, message.ImportedEpisode.MovieId);
}
public void Handle(EpisodeFileDeletedEvent message)
{
if (message.Reason == DeleteMediaFileReason.Upgrade) return;
//BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.MovieId);
}
public void Handle(MovieUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
}
public void Handle(MovieEditedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
}
public void Handle(MovieDeletedEvent message)
{
BroadcastResourceChange(ModelAction.Deleted, message.Movie.ToResource());
}
public void Handle(MovieRenamedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
}
public void Handle(MediaCoversUpdatedEvent message)
{
//BroadcastResourceChange(ModelAction.Updated, message.Movie.Id);
}
}
}

@ -4,8 +4,9 @@ using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv;
using NzbDrone.Api.Series;
namespace NzbDrone.Api.Series
namespace NzbDrone.Api.Movie
{
public class MovieResource : RestResource
{

@ -41,6 +41,34 @@ namespace NzbDrone.Core.Datastore.Migration
.WithColumn("FirstAired").AsDateTime().Nullable()
.WithColumn("NextAiring").AsDateTime().Nullable();
Create.TableForModel("Movies")
.WithColumn("ImdbId").AsString().Unique()
.WithColumn("Title").AsString()
.WithColumn("TitleSlug").AsString().Unique()
.WithColumn("SortTitle").AsString().Nullable()
.WithColumn("CleanTitle").AsString()
.WithColumn("Status").AsInt32()
.WithColumn("Overview").AsString().Nullable()
.WithColumn("Images").AsString()
.WithColumn("Path").AsString()
.WithColumn("Monitored").AsBoolean()
.WithColumn("QualityProfileId").AsInt32()
.WithColumn("SeasonFolder").AsBoolean()
.WithColumn("LastInfoSync").AsDateTime().Nullable()
.WithColumn("LastDiskSync").AsDateTime().Nullable()
.WithColumn("Runtime").AsInt32()
.WithColumn("BacklogSetting").AsInt32()
.WithColumn("CustomStartDate").AsDateTime().Nullable()
.WithColumn("InCinemas").AsDateTime().Nullable()
.WithColumn("Year").AsInt32().Nullable()
.WithColumn("Added").AsDateTime().Nullable()
.WithColumn("Actors").AsString().Nullable()
.WithColumn("Ratings").AsString().Nullable()
.WithColumn("Genres").AsString().Nullable()
.WithColumn("Tags").AsString().Nullable()
.WithColumn("Certification").AsString().Nullable();
Create.TableForModel("Seasons")
.WithColumn("SeriesId").AsInt32()
.WithColumn("SeasonNumber").AsInt32()

@ -76,6 +76,11 @@ namespace NzbDrone.Core.Datastore
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId);
Mapper.Entity<Movie>().RegisterModel("Movies")
.Ignore(s => s.RootFolderPath)
.Relationship()
.HasOne(s => s.Profile, s => s.ProfileId);
Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
.Ignore(f => f.Path)
.Relationships.AutoMapICollectionOrComplexProperties()

@ -0,0 +1,15 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles.Events
{
public class MovieRenamedEvent : IEvent
{
public Movie Movie { get; private set; }
public MovieRenamedEvent(Movie movie)
{
Movie = movie;
}
}
}

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MovieStats
{
public class MovieStatistics : ResultSet
{
public int MovieId { get; set; }
public string NextAiringString { get; set; }
public string PreviousAiringString { get; set; }
public int EpisodeFileCount { get; set; }
public int EpisodeCount { get; set; }
public int TotalEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public List<SeasonStatistics> SeasonStatistics { get; set; }
public DateTime? NextAiring
{
get
{
DateTime nextAiring;
if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null;
return nextAiring;
}
}
public DateTime? PreviousAiring
{
get
{
DateTime previousAiring;
if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null;
return previousAiring;
}
}
}
}

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Text;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MovieStats
{
public interface IMovieStatisticsRepository
{
List<SeasonStatistics> MovieStatistics();
List<SeasonStatistics> MovieStatistics(int movieId);
}
public class MovieStatisticsRepository : IMovieStatisticsRepository
{
private readonly IMainDatabase _database;
public MovieStatisticsRepository(IMainDatabase database)
{
_database = database;
}
public List<SeasonStatistics> MovieStatistics()
{
var mapper = _database.GetDataMapper();
mapper.AddParameter("currentDate", DateTime.UtcNow);
var sb = new StringBuilder();
sb.AppendLine(GetSelectClause());
sb.AppendLine(GetEpisodeFilesJoin());
sb.AppendLine(GetGroupByClause());
var queryText = sb.ToString();
return new List<SeasonStatistics>();
return mapper.Query<SeasonStatistics>(queryText);
}
public List<SeasonStatistics> MovieStatistics(int movieId)
{
var mapper = _database.GetDataMapper();
mapper.AddParameter("currentDate", DateTime.UtcNow);
mapper.AddParameter("movieId", movieId);
var sb = new StringBuilder();
sb.AppendLine(GetSelectClause());
sb.AppendLine(GetEpisodeFilesJoin());
sb.AppendLine("WHERE Episodes.MovieId = @movieId");
sb.AppendLine(GetGroupByClause());
var queryText = sb.ToString();
return new List<SeasonStatistics>();
return mapper.Query<SeasonStatistics>(queryText);
}
private string GetSelectClause()
{
return @"SELECT Episodes.*, SUM(EpisodeFiles.Size) as SizeOnDisk FROM
(SELECT
Episodes.MovieId,
Episodes.SeasonNumber,
SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount,
SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount,
SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount,
MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString,
MAX(CASE WHEN AirDateUtc >= @currentDate OR EpisodeFileId = 0 AND Monitored = 0 THEN NULL ELSE AirDateUtc END) AS PreviousAiringString
FROM Episodes
GROUP BY Episodes.MovieId, Episodes.SeasonNumber) as Episodes";
}
private string GetGroupByClause()
{
return "GROUP BY Episodes.MovieId, Episodes.SeasonNumber";
}
private string GetEpisodeFilesJoin()
{
return @"LEFT OUTER JOIN EpisodeFiles
ON EpisodeFiles.MovieId = Episodes.MovieId
AND EpisodeFiles.SeasonNumber = Episodes.SeasonNumber";
}
}
}

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.MovieStats
{
public interface IMovieStatisticsService
{
List<MovieStatistics> MovieStatistics();
MovieStatistics MovieStatistics(int movieId);
}
public class MovieStatisticsService : IMovieStatisticsService
{
private readonly IMovieStatisticsRepository _movieStatisticsRepository;
public MovieStatisticsService(IMovieStatisticsRepository movieStatisticsRepository)
{
_movieStatisticsRepository = movieStatisticsRepository;
}
public List<MovieStatistics> MovieStatistics()
{
var seasonStatistics = _movieStatisticsRepository.MovieStatistics();
return seasonStatistics.GroupBy(s => s.MovieId).Select(s => MapMovieStatistics(s.ToList())).ToList();
}
public MovieStatistics MovieStatistics(int movieId)
{
var stats = _movieStatisticsRepository.MovieStatistics(movieId);
if (stats == null || stats.Count == 0) return new MovieStatistics();
return MapMovieStatistics(stats);
}
private MovieStatistics MapMovieStatistics(List<SeasonStatistics> seasonStatistics)
{
var movieStatistics = new MovieStatistics
{
SeasonStatistics = seasonStatistics,
MovieId = seasonStatistics.First().MovieId,
EpisodeFileCount = seasonStatistics.Sum(s => s.EpisodeFileCount),
EpisodeCount = seasonStatistics.Sum(s => s.EpisodeCount),
TotalEpisodeCount = seasonStatistics.Sum(s => s.TotalEpisodeCount),
SizeOnDisk = seasonStatistics.Sum(s => s.SizeOnDisk)
};
var nextAiring = seasonStatistics.Where(s => s.NextAiring != null)
.OrderBy(s => s.NextAiring)
.FirstOrDefault();
var previousAiring = seasonStatistics.Where(s => s.PreviousAiring != null)
.OrderBy(s => s.PreviousAiring)
.LastOrDefault();
movieStatistics.NextAiringString = nextAiring != null ? nextAiring.NextAiringString : null;
movieStatistics.PreviousAiringString = previousAiring != null ? previousAiring.PreviousAiringString : null;
return movieStatistics;
}
}
}

@ -0,0 +1,41 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MovieStats
{
public class SeasonStatistics : ResultSet
{
public int MovieId { get; set; }
public int SeasonNumber { get; set; }
public string NextAiringString { get; set; }
public string PreviousAiringString { get; set; }
public int EpisodeFileCount { get; set; }
public int EpisodeCount { get; set; }
public int TotalEpisodeCount { get; set; }
public long SizeOnDisk { get; set; }
public DateTime? NextAiring
{
get
{
DateTime nextAiring;
if (!DateTime.TryParse(NextAiringString, out nextAiring)) return null;
return nextAiring;
}
}
public DateTime? PreviousAiring
{
get
{
DateTime previousAiring;
if (!DateTime.TryParse(PreviousAiringString, out previousAiring)) return null;
return previousAiring;
}
}
}
}

@ -735,6 +735,7 @@
<Compile Include="MediaFiles\Events\EpisodeFileDeletedEvent.cs" />
<Compile Include="MediaFiles\Events\EpisodeFolderCreatedEvent.cs" />
<Compile Include="MediaFiles\Events\EpisodeImportedEvent.cs" />
<Compile Include="MediaFiles\Events\MovieRenamedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesRenamedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesScanSkippedEvent.cs" />
<Compile Include="MediaFiles\Events\SeriesScannedEvent.cs" />
@ -1024,6 +1025,10 @@
</Compile>
<Compile Include="RootFolders\UnmappedFolder.cs" />
<Compile Include="Security.cs" />
<Compile Include="MovieStats\SeasonStatistics.cs" />
<Compile Include="MovieStats\MovieStatistics.cs" />
<Compile Include="MovieStats\MovieStatisticsRepository.cs" />
<Compile Include="MovieStats\MovieStatisticsService.cs" />
<Compile Include="SeriesStats\SeasonStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatisticsRepository.cs" />
@ -1058,11 +1063,15 @@
</Compile>
<Compile Include="Tv\EpisodeService.cs" />
<Compile Include="Tv\Events\EpisodeInfoRefreshedEvent.cs" />
<Compile Include="Tv\Events\MovieAddedEvent.cs" />
<Compile Include="Tv\Events\SeriesAddedEvent.cs" />
<Compile Include="Tv\Events\MovieDeletedEvent.cs" />
<Compile Include="Tv\Events\SeriesDeletedEvent.cs" />
<Compile Include="Tv\Events\MovieEditedEvent.cs" />
<Compile Include="Tv\Events\SeriesEditedEvent.cs" />
<Compile Include="Tv\Events\SeriesMovedEvent.cs" />
<Compile Include="Tv\Events\SeriesRefreshStartingEvent.cs" />
<Compile Include="Tv\Events\MovieUpdateEvent.cs" />
<Compile Include="Tv\Events\SeriesUpdatedEvent.cs" />
<Compile Include="Tv\MonitoringOptions.cs" />
<Compile Include="Tv\MoveSeriesService.cs" />
@ -1073,14 +1082,17 @@
<Compile Include="Tv\Movie.cs" />
<Compile Include="Tv\Series.cs" />
<Compile Include="Tv\SeriesAddedHandler.cs" />
<Compile Include="Tv\MovieRepository.cs" />
<Compile Include="Tv\SeriesScannedHandler.cs" />
<Compile Include="Tv\SeriesEditedService.cs" />
<Compile Include="Tv\SeriesRepository.cs" />
<Compile Include="Tv\MovieService.cs" />
<Compile Include="Tv\SeriesService.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Tv\MovieStatusType.cs" />
<Compile Include="Tv\SeriesStatusType.cs" />
<Compile Include="Tv\MovieTitleNormalizer.cs" />
<Compile Include="Tv\SeriesTitleNormalizer.cs" />
<Compile Include="Tv\SeriesTypes.cs" />
<Compile Include="Tv\ShouldRefreshSeries.cs" />
@ -1109,6 +1121,9 @@
<Compile Include="Validation\Paths\FolderWritableValidator.cs" />
<Compile Include="Validation\Paths\PathExistsValidator.cs" />
<Compile Include="Validation\Paths\PathValidator.cs" />
<Compile Include="Validation\Paths\MoviePathValidation.cs" />
<Compile Include="Validation\Paths\MovieAncestorValidator.cs" />
<Compile Include="Validation\Paths\MovieExistsValidator.cs" />
<Compile Include="Validation\Paths\StartupFolderValidator.cs" />
<Compile Include="Validation\Paths\RootFolderValidator.cs" />
<Compile Include="Validation\Paths\SeriesAncestorValidator.cs" />

@ -22,6 +22,7 @@ namespace NzbDrone.Core.Organizer
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
string GetSeriesFolder(Series series, NamingConfig namingConfig = null);
string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null);
string GetMovieFolder(Movie movie);
}
public class FileNameBuilder : IBuildFileNames
@ -243,6 +244,11 @@ namespace NzbDrone.Core.Organizer
return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig));
}
public string GetMovieFolder(Movie movie)
{
return CleanFolderName(Parser.Parser.CleanSeriesTitle(movie.Title));
}
public static string CleanTitle(string title)
{
title = title.Replace("&", "and");

@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Events
{
public class MovieAddedEvent : IEvent
{
public Movie Movie { get; private set; }
public MovieAddedEvent(Movie movie)
{
Movie = movie;
}
}
}

@ -0,0 +1,16 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Events
{
public class MovieDeletedEvent : IEvent
{
public Movie Movie { get; private set; }
public bool DeleteFiles { get; private set; }
public MovieDeletedEvent(Movie movie, bool deleteFiles)
{
Movie = movie;
DeleteFiles = deleteFiles;
}
}
}

@ -0,0 +1,16 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Events
{
public class MovieEditedEvent : IEvent
{
public Movie Movie { get; private set; }
public Movie OldMovie { get; private set; }
public MovieEditedEvent(Movie movie, Movie oldMovie)
{
Movie = movie;
OldMovie = oldMovie;
}
}
}

@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Tv.Events
{
public class MovieUpdatedEvent : IEvent
{
public Movie Movie { get; private set; }
public MovieUpdatedEvent(Movie movie)
{
Movie = movie;
}
}
}

@ -0,0 +1,50 @@
using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Tv
{
public interface IMovieRepository : IBasicRepository<Movie>
{
bool MoviePathExists(string path);
Movie FindByTitle(string cleanTitle);
Movie FindByTitle(string cleanTitle, int year);
Movie FindByImdbId(string imdbid);
}
public class MovieRepository : BasicRepository<Movie>, IMovieRepository
{
public MovieRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
public bool MoviePathExists(string path)
{
return Query.Where(c => c.Path == path).Any();
}
public Movie FindByTitle(string cleanTitle)
{
cleanTitle = cleanTitle.ToLowerInvariant();
return Query.Where(s => s.CleanTitle == cleanTitle)
.SingleOrDefault();
}
public Movie FindByTitle(string cleanTitle, int year)
{
cleanTitle = cleanTitle.ToLowerInvariant();
return Query.Where(s => s.CleanTitle == cleanTitle)
.AndWhere(s => s.Year == year)
.SingleOrDefault();
}
public Movie FindByImdbId(string imdbid)
{
return Query.Where(s => s.ImdbId == imdbid).SingleOrDefault();
}
}
}

@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Tv
{
public interface IMovieService
{
Movie GetMovie(int movieId);
List<Movie> GetMovies(IEnumerable<int> movieIds);
Movie AddMovie(Movie newMovie);
Movie FindByImdbId(string imdbid);
Movie FindByTitle(string title);
Movie FindByTitle(string title, int year);
Movie FindByTitleInexact(string title);
void DeleteMovie(int movieId, bool deleteFiles);
List<Movie> GetAllMovies();
Movie UpdateMovie(Movie movie);
List<Movie> UpdateMovie(List<Movie> movie);
bool MoviePathExists(string folder);
}
public class MovieService : IMovieService
{
private readonly IMovieRepository _movieRepository;
private readonly IEventAggregator _eventAggregator;
private readonly IBuildFileNames _fileNameBuilder;
private readonly Logger _logger;
public MovieService(IMovieRepository movieRepository,
IEventAggregator eventAggregator,
ISceneMappingService sceneMappingService,
IEpisodeService episodeService,
IBuildFileNames fileNameBuilder,
Logger logger)
{
_movieRepository = movieRepository;
_eventAggregator = eventAggregator;
_fileNameBuilder = fileNameBuilder;
_logger = logger;
}
public Movie GetMovie(int movieId)
{
return _movieRepository.Get(movieId);
}
public List<Movie> GetMovies(IEnumerable<int> movieIds)
{
return _movieRepository.Get(movieIds).ToList();
}
public Movie AddMovie(Movie newMovie)
{
Ensure.That(newMovie, () => newMovie).IsNotNull();
if (string.IsNullOrWhiteSpace(newMovie.Path))
{
var folderName = _fileNameBuilder.GetMovieFolder(newMovie);
newMovie.Path = Path.Combine(newMovie.RootFolderPath, folderName);
}
_logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path);
newMovie.CleanTitle = newMovie.Title.CleanSeriesTitle();
newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.ImdbId);
newMovie.Added = DateTime.UtcNow;
_movieRepository.Insert(newMovie);
_eventAggregator.PublishEvent(new MovieAddedEvent(GetMovie(newMovie.Id)));
return newMovie;
}
public Movie FindByTitle(string title)
{
return _movieRepository.FindByTitle(title.CleanSeriesTitle());
}
public Movie FindByImdbId(string imdbid)
{
return _movieRepository.FindByImdbId(imdbid);
}
public Movie FindByTitleInexact(string title)
{
// find any movie clean title within the provided release title
string cleanTitle = title.CleanSeriesTitle();
var list = _movieRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList();
if (!list.Any())
{
// no movie matched
return null;
}
if (list.Count == 1)
{
// return the first movie if there is only one
return list.Single();
}
// build ordered list of movie by position in the search string
var query =
list.Select(movie => new
{
position = cleanTitle.IndexOf(movie.CleanTitle),
length = movie.CleanTitle.Length,
movie = movie
})
.Where(s => (s.position>=0))
.ToList()
.OrderBy(s => s.position)
.ThenByDescending(s => s.length)
.ToList();
// get the leftmost movie that is the longest
// movie are usually the first thing in release title, so we select the leftmost and longest match
var match = query.First().movie;
_logger.Debug("Multiple movie matched {0} from title {1}", match.Title, title);
foreach (var entry in list)
{
_logger.Debug("Multiple movie match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle);
}
return match;
}
public Movie FindByTitle(string title, int year)
{
return _movieRepository.FindByTitle(title.CleanSeriesTitle(), year);
}
public void DeleteMovie(int movieId, bool deleteFiles)
{
var movie = _movieRepository.Get(movieId);
_movieRepository.Delete(movieId);
_eventAggregator.PublishEvent(new MovieDeletedEvent(movie, deleteFiles));
}
public List<Movie> GetAllMovies()
{
return _movieRepository.All().ToList();
}
public Movie UpdateMovie(Movie movie)
{
var storedMovie = GetMovie(movie.Id);
var updatedMovie = _movieRepository.Update(movie);
_eventAggregator.PublishEvent(new MovieEditedEvent(updatedMovie, storedMovie));
return updatedMovie;
}
public List<Movie> UpdateMovie(List<Movie> movie)
{
_logger.Debug("Updating {0} movie", movie.Count);
foreach (var s in movie)
{
_logger.Trace("Updating: {0}", s.Title);
if (!s.RootFolderPath.IsNullOrWhiteSpace())
{
var folderName = new DirectoryInfo(s.Path).Name;
s.Path = Path.Combine(s.RootFolderPath, folderName);
_logger.Trace("Changing path for {0} to {1}", s.Title, s.Path);
}
else
{
_logger.Trace("Not changing path for: {0}", s.Title);
}
}
_movieRepository.UpdateMany(movie);
_logger.Debug("{0} movie updated", movie.Count);
return movie;
}
public bool MoviePathExists(string folder)
{
return _movieRepository.MoviePathExists(folder);
}
}
}

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Tv
{
public static class MovieTitleNormalizer
{
private readonly static Dictionary<string, string> PreComputedTitles = new Dictionary<string, string>
{
{ "tt_109823457098", "a to z" },
};
public static string Normalize(string title, string imdbid)
{
if (PreComputedTitles.ContainsKey(imdbid))
{
return PreComputedTitles[imdbid];
}
return Parser.Parser.NormalizeTitle(title).ToLower();
}
}
}

@ -0,0 +1,25 @@
using System.Linq;
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Validation.Paths
{
public class MovieAncestorValidator : PropertyValidator
{
private readonly IMovieService _seriesService;
public MovieAncestorValidator(IMovieService seriesService)
: base("Path is an ancestor of an existing path")
{
_seriesService = seriesService;
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null) return true;
return !_seriesService.GetAllMovies().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path));
}
}
}

@ -0,0 +1,26 @@
using System;
using FluentValidation.Validators;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Validation.Paths
{
public class MovieExistsValidator : PropertyValidator
{
private readonly IMovieService _seriesService;
public MovieExistsValidator(IMovieService seriesService)
: base("This series has already been added")
{
_seriesService = seriesService;
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null) return true;
var imdbid = context.PropertyValue.ToString();
return (!_seriesService.GetAllMovies().Exists(s => s.ImdbId == imdbid));
}
}
}

@ -0,0 +1,27 @@
using FluentValidation.Validators;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Validation.Paths
{
public class MoviePathValidator : PropertyValidator
{
private readonly IMovieService _seriesService;
public MoviePathValidator(IMovieService seriesService)
: base("Path is already configured for another series")
{
_seriesService = seriesService;
}
protected override bool IsValid(PropertyValidatorContext context)
{
if (context.PropertyValue == null) return true;
dynamic instance = context.ParentContext.InstanceToValidate;
var instanceId = (int)instance.Id;
return (!_seriesService.GetAllMovies().Exists(s => s.Path.PathEquals(context.PropertyValue.ToString()) && s.Id != instanceId));
}
}
}

@ -2,7 +2,7 @@ var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/movies',
urlRoot : window.NzbDrone.ApiRoot + '/movie',
defaults : {
episodeFileCount : 0,

@ -10,9 +10,9 @@ var moment = require('moment');
require('../Mixins/backbone.signalr.mixin');
var Collection = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/movies',
url : window.NzbDrone.ApiRoot + '/movie',
model : MovieModel,
tableName : 'movies',
tableName : 'movie',
state : {
sortKey : 'sortTitle',
@ -115,6 +115,6 @@ Collection = AsFilteredCollection.call(Collection);
Collection = AsSortedCollection.call(Collection);
Collection = AsPersistedStateCollection.call(Collection);
var data = ApiData.get('series');
var data = ApiData.get('movie');
module.exports = new Collection(data, { full : true }).bindSignalR();

Loading…
Cancel
Save