1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-04 10:02:40 +01:00

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!

This commit is contained in:
Leonardo Galli 2016-12-29 16:04:01 +01:00
parent 74ca6149e3
commit 5ebfac6cc8
25 changed files with 938 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,34 @@ protected override void MainDbUpgrade()
.WithColumn("FirstAired").AsDateTime().Nullable() .WithColumn("FirstAired").AsDateTime().Nullable()
.WithColumn("NextAiring").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") Create.TableForModel("Seasons")
.WithColumn("SeriesId").AsInt32() .WithColumn("SeriesId").AsInt32()
.WithColumn("SeasonNumber").AsInt32() .WithColumn("SeasonNumber").AsInt32()

View File

@ -76,6 +76,11 @@ public static void Map()
.Relationship() .Relationship()
.HasOne(s => s.Profile, s => s.ProfileId); .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") Mapper.Entity<EpisodeFile>().RegisterModel("EpisodeFiles")
.Ignore(f => f.Path) .Ignore(f => f.Path)
.Relationships.AutoMapICollectionOrComplexProperties() .Relationships.AutoMapICollectionOrComplexProperties()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ public interface IBuildFileNames
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
string GetSeriesFolder(Series series, NamingConfig namingConfig = null); string GetSeriesFolder(Series series, NamingConfig namingConfig = null);
string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null);
string GetMovieFolder(Movie movie);
} }
public class FileNameBuilder : IBuildFileNames public class FileNameBuilder : IBuildFileNames
@ -243,6 +244,11 @@ public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig nami
return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); 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) public static string CleanTitle(string title)
{ {
title = title.Replace("&", "and"); title = title.Replace("&", "and");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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