1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-17 15:02:34 +02:00

Added: Importing extra files from downloaded movies and generate metadata such as .nfo (#2506)

Fixes #121, Fixes #167, Fixes #2262 and Fixes #1104
This commit is contained in:
Qstick 2018-02-15 13:39:01 +01:00 committed by Leonardo Galli
parent b40423f3a3
commit e7e9e2b154
78 changed files with 1381 additions and 1759 deletions

View File

@ -1,4 +1,4 @@
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -22,6 +22,7 @@ public class MediaManagementConfigResource : RestResource
public bool SkipFreeSpaceCheckWhenImporting { get; set; } public bool SkipFreeSpaceCheckWhenImporting { get; set; }
public bool CopyUsingHardlinks { get; set; } public bool CopyUsingHardlinks { get; set; }
public bool ImportExtraFiles { get; set; }
public string ExtraFileExtensions { get; set; } public string ExtraFileExtensions { get; set; }
public bool EnableMediaInfo { get; set; } public bool EnableMediaInfo { get; set; }
} }
@ -48,6 +49,7 @@ public static MediaManagementConfigResource ToResource(IConfigService model)
SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting,
CopyUsingHardlinks = model.CopyUsingHardlinks, CopyUsingHardlinks = model.CopyUsingHardlinks,
ImportExtraFiles = model.ImportExtraFiles,
ExtraFileExtensions = model.ExtraFileExtensions, ExtraFileExtensions = model.ExtraFileExtensions,
EnableMediaInfo = model.EnableMediaInfo EnableMediaInfo = model.EnableMediaInfo
}; };

View File

@ -0,0 +1,45 @@
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
namespace NzbDrone.Api.ExtraFiles
{
public class ExtraFileModule : NzbDroneRestModule<ExtraFileResource>
{
private readonly IExtraFileService<SubtitleFile> _subtitleFileService;
private readonly IExtraFileService<MetadataFile> _metadataFileService;
private readonly IExtraFileService<OtherExtraFile> _otherFileService;
public ExtraFileModule(IExtraFileService<SubtitleFile> subtitleFileService, IExtraFileService<MetadataFile> metadataFileService, IExtraFileService<OtherExtraFile> otherExtraFileService)
: base("/extrafile")
{
_subtitleFileService = subtitleFileService;
_metadataFileService = metadataFileService;
_otherFileService = otherExtraFileService;
GetResourceAll = GetFiles;
}
private List<ExtraFileResource> GetFiles()
{
if (!Request.Query.MovieId.HasValue)
{
throw new BadRequestException("MovieId is missing");
}
var extraFiles = new List<ExtraFileResource>();
List<SubtitleFile> subtitleFiles = _subtitleFileService.GetFilesByMovie(Request.Query.MovieId);
List<MetadataFile> metadataFiles = _metadataFileService.GetFilesByMovie(Request.Query.MovieId);
List<OtherExtraFile> otherExtraFiles = _otherFileService.GetFilesByMovie(Request.Query.MovieId);
extraFiles.AddRange(subtitleFiles.ToResource());
extraFiles.AddRange(metadataFiles.ToResource());
extraFiles.AddRange(otherExtraFiles.ToResource());
return extraFiles;
}
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
namespace NzbDrone.Api.ExtraFiles
{
public class ExtraFileResource : RestResource
{
public int MovieId { get; set; }
public int? MovieFileId { get; set; }
public string RelativePath { get; set; }
public string Extension { get; set; }
public ExtraFileType Type { get; set; }
}
public static class ExtraFileResourceMapper
{
public static ExtraFileResource ToResource(this MetadataFile model)
{
if (model == null) return null;
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Metadata
};
}
public static ExtraFileResource ToResource(this SubtitleFile model)
{
if (model == null) return null;
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Subtitle
};
}
public static ExtraFileResource ToResource(this OtherExtraFile model)
{
if (model == null) return null;
return new ExtraFileResource
{
Id = model.Id,
MovieId = model.MovieId,
MovieFileId = model.MovieFileId,
RelativePath = model.RelativePath,
Extension = model.Extension,
Type = ExtraFileType.Other
};
}
public static List<ExtraFileResource> ToResource(this IEnumerable<SubtitleFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<MetadataFile> movies)
{
return movies.Select(ToResource).ToList();
}
public static List<ExtraFileResource> ToResource(this IEnumerable<OtherExtraFile> movies)
{
return movies.Select(ToResource).ToList();
}
}
}

View File

@ -115,10 +115,12 @@
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" /> <Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
<Compile Include="Extensions\Pipelines\UrlBasePipeline.cs" /> <Compile Include="Extensions\Pipelines\UrlBasePipeline.cs" />
<Compile Include="Extensions\Pipelines\RequestLoggingPipeline.cs" /> <Compile Include="Extensions\Pipelines\RequestLoggingPipeline.cs" />
<Compile Include="ExtraFiles\ExtraFileResource.cs" />
<Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" /> <Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" />
<Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" /> <Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" />
<Compile Include="Indexers\ReleaseModuleBase.cs" /> <Compile Include="Indexers\ReleaseModuleBase.cs" />
<Compile Include="Indexers\ReleasePushModule.cs" /> <Compile Include="Indexers\ReleasePushModule.cs" />
<Compile Include="ExtraFiles\ExtraFileModule.cs" />
<Compile Include="Movies\AlternativeTitleModule.cs" /> <Compile Include="Movies\AlternativeTitleModule.cs" />
<Compile Include="Movies\AlternativeYearResource.cs" /> <Compile Include="Movies\AlternativeYearResource.cs" />
<Compile Include="Movies\AlternativeYearModule.cs" /> <Compile Include="Movies\AlternativeYearModule.cs" />

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -20,27 +20,27 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
public class DeleteBadMediaCoversFixture : CoreTest<DeleteBadMediaCovers> public class DeleteBadMediaCoversFixture : CoreTest<DeleteBadMediaCovers>
{ {
private List<MetadataFile> _metadata; private List<MetadataFile> _metadata;
private List<Series> _series; private List<Movie> _movies;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateListOfSize(1) _movies = Builder<Movie>.CreateListOfSize(1)
.All() .All()
.With(c => c.Path = "C:\\TV\\".AsOsAgnostic()) .With(c => c.Path = "C:\\Movie\\".AsOsAgnostic())
.Build().ToList(); .Build().ToList();
_metadata = Builder<MetadataFile>.CreateListOfSize(1) _metadata = Builder<MetadataFile>.CreateListOfSize(1)
.Build().ToList(); .Build().ToList();
Mocker.GetMock<ISeriesService>() Mocker.GetMock<IMovieService>()
.Setup(c => c.GetAllSeries()) .Setup(c => c.GetAllMovies())
.Returns(_series); .Returns(_movies);
Mocker.GetMock<IMetadataFileService>() Mocker.GetMock<IMetadataFileService>()
.Setup(c => c.GetFilesBySeries(_series.First().Id)) .Setup(c => c.GetFilesByMovie(_movies.First().Id))
.Returns(_metadata); .Returns(_metadata);
@ -51,8 +51,8 @@ public void Setup()
[Test] [Test]
public void should_not_process_non_image_files() public void should_not_process_non_image_files()
{ {
_metadata.First().RelativePath = "season\\file.xml".AsOsAgnostic(); _metadata.First().RelativePath = "extrafiles\\file.xml".AsOsAgnostic();
_metadata.First().Type = MetadataType.EpisodeMetadata; _metadata.First().Type = MetadataType.MovieMetadata;
Subject.Clean(); Subject.Clean();
@ -101,10 +101,10 @@ public void should_set_clean_flag_to_false()
public void should_delete_html_images() public void should_delete_html_images()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Movie\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "image.jpg".AsOsAgnostic();
_metadata.First().Type = MetadataType.SeriesImage; _metadata.First().Type = MetadataType.MovieImage;
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))
@ -123,10 +123,10 @@ public void should_delete_html_images()
public void should_delete_empty_images() public void should_delete_empty_images()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Movie\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().Type = MetadataType.SeasonImage; _metadata.First().Type = MetadataType.MovieImage;
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))
@ -144,9 +144,9 @@ public void should_delete_empty_images()
public void should_not_delete_non_html_files() public void should_not_delete_non_html_files()
{ {
var imagePath = "C:\\TV\\Season\\image.jpg".AsOsAgnostic(); var imagePath = "C:\\Movie\\image.jpg".AsOsAgnostic();
_metadata.First().LastUpdated = new DateTime(2014, 12, 29); _metadata.First().LastUpdated = new DateTime(2014, 12, 29);
_metadata.First().RelativePath = "Season\\image.jpg".AsOsAgnostic(); _metadata.First().RelativePath = "image.jpg".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(c => c.OpenReadStream(imagePath)) .Setup(c => c.OpenReadStream(imagePath))

View File

@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
@ -12,12 +12,12 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile> public class CleanupDuplicateMetadataFilesFixture : DbTest<CleanupDuplicateMetadataFiles, MetadataFile>
{ {
[Test] [Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_series_but_different_consumers() public void should_not_delete_metadata_files_when_they_are_for_the_same_movie_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.SeriesId = 1) .With(m => m.MovieId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -26,11 +26,11 @@ public void should_not_delete_metadata_files_when_they_are_for_the_same_series_b
} }
[Test] [Test]
public void should_not_delete_metadata_files_for_different_series() public void should_not_delete_metadata_files_for_different_movie()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -40,12 +40,12 @@ public void should_not_delete_metadata_files_for_different_series()
} }
[Test] [Test]
public void should_delete_metadata_files_when_they_are_for_the_same_series_and_consumer() public void should_delete_metadata_files_when_they_are_for_the_same_movie_and_consumer()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.SeriesMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.SeriesId = 1) .With(m => m.MovieId = 1)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -55,7 +55,7 @@ public void should_delete_metadata_files_when_they_are_for_the_same_series_and_c
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_series_and_consumer() public void should_not_delete_metadata_files_when_there_is_only_one_for_that_movie_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();
@ -66,12 +66,12 @@ public void should_not_delete_metadata_files_when_there_is_only_one_for_that_ser
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_they_are_for_the_same_episode_but_different_consumers() public void should_not_delete_metadata_files_when_they_are_for_the_same_movie_file_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.EpisodeFileId = 1) .With(m => m.MovieFileId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -80,11 +80,11 @@ public void should_not_delete_metadata_files_when_they_are_for_the_same_episode_
} }
[Test] [Test]
public void should_not_delete_metadata_files_for_different_episode() public void should_not_delete_metadata_files_for_different_movie_file()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -94,12 +94,12 @@ public void should_not_delete_metadata_files_for_different_episode()
} }
[Test] [Test]
public void should_delete_metadata_files_when_they_are_for_the_same_episode_and_consumer() public void should_delete_metadata_files_when_they_are_for_the_same_movie_file_and_consumer()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.EpisodeFileId = 1) .With(m => m.MovieFileId = 1)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -109,7 +109,7 @@ public void should_delete_metadata_files_when_they_are_for_the_same_episode_and_
} }
[Test] [Test]
public void should_not_delete_metadata_files_when_there_is_only_one_for_that_episode_and_consumer() public void should_not_delete_metadata_files_when_there_is_only_one_for_that_movie_file_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();
@ -120,12 +120,12 @@ public void should_not_delete_metadata_files_when_there_is_only_one_for_that_epi
} }
[Test] [Test]
public void should_not_delete_image_when_they_are_for_the_same_episode_but_different_consumers() public void should_not_delete_image_when_they_are_for_the_same_movie_file_but_different_consumers()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.MovieImage)
.With(m => m.EpisodeFileId = 1) .With(m => m.MovieFileId = 1)
.BuildListOfNew(); .BuildListOfNew();
Db.InsertMany(files); Db.InsertMany(files);
@ -134,11 +134,11 @@ public void should_not_delete_image_when_they_are_for_the_same_episode_but_diffe
} }
[Test] [Test]
public void should_not_delete_image_for_different_episode() public void should_not_delete_image_for_different_movie_file()
{ {
var files = Builder<MetadataFile>.CreateListOfSize(2) var files = Builder<MetadataFile>.CreateListOfSize(2)
.All() .All()
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.MovieImage)
.With(m => m.Consumer = "XbmcMetadata") .With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew(); .BuildListOfNew();
@ -148,22 +148,7 @@ public void should_not_delete_image_for_different_episode()
} }
[Test] [Test]
public void should_delete_image_when_they_are_for_the_same_episode_and_consumer() public void should_not_delete_image_when_there_is_only_one_for_that_movie_file_and_consumer()
{
var files = Builder<MetadataFile>.CreateListOfSize(2)
.All()
.With(m => m.Type = MetadataType.EpisodeImage)
.With(m => m.EpisodeFileId = 1)
.With(m => m.Consumer = "XbmcMetadata")
.BuildListOfNew();
Db.InsertMany(files);
Subject.Clean();
AllStoredModels.Count.Should().Be(1);
}
[Test]
public void should_not_delete_image_when_there_is_only_one_for_that_episode_and_consumer()
{ {
var file = Builder<MetadataFile>.CreateNew() var file = Builder<MetadataFile>.CreateNew()
.BuildNew(); .BuildNew();

View File

@ -1,4 +1,4 @@
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Extras.Metadata; using NzbDrone.Core.Extras.Metadata;
@ -15,10 +15,10 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
public class CleanupOrphanedMetadataFilesFixture : DbTest<CleanupOrphanedMetadataFiles, MetadataFile> public class CleanupOrphanedMetadataFilesFixture : DbTest<CleanupOrphanedMetadataFiles, MetadataFile>
{ {
[Test] [Test]
public void should_delete_metadata_files_that_dont_have_a_coresponding_series() public void should_delete_metadata_files_that_dont_have_a_coresponding_movie()
{ {
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.EpisodeFileId = null) .With(m => m.MovieFileId = null)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -27,16 +27,16 @@ public void should_delete_metadata_files_that_dont_have_a_coresponding_series()
} }
[Test] [Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_series() public void should_not_delete_metadata_files_that_have_a_coresponding_movie()
{ {
var series = Builder<Series>.CreateNew() var movie = Builder<Movie>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(movie);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.MovieId = movie.Id)
.With(m => m.EpisodeFileId = null) .With(m => m.MovieFileId = null)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -45,16 +45,16 @@ public void should_not_delete_metadata_files_that_have_a_coresponding_series()
} }
[Test] [Test]
public void should_delete_metadata_files_that_dont_have_a_coresponding_episode_file() public void should_delete_metadata_files_that_dont_have_a_coresponding_movie_file()
{ {
var series = Builder<Series>.CreateNew() var movie = Builder<Movie>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(movie);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.MovieId = movie.Id)
.With(m => m.EpisodeFileId = 10) .With(m => m.MovieFileId = 10)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -63,21 +63,21 @@ public void should_delete_metadata_files_that_dont_have_a_coresponding_episode_f
} }
[Test] [Test]
public void should_not_delete_metadata_files_that_have_a_coresponding_episode_file() public void should_not_delete_metadata_files_that_have_a_coresponding_movie_file()
{ {
var series = Builder<Series>.CreateNew() var movie = Builder<Movie>.CreateNew()
.BuildNew(); .BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew() var movieFile = Builder<MovieFile>.CreateNew()
.With(h => h.Quality = new QualityModel()) .With(h => h.Quality = new QualityModel())
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(movie);
Db.Insert(episodeFile); Db.Insert(movieFile);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.MovieId = movie.Id)
.With(m => m.EpisodeFileId = episodeFile.Id) .With(m => m.MovieFileId = movieFile.Id)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -86,17 +86,17 @@ public void should_not_delete_metadata_files_that_have_a_coresponding_episode_fi
} }
[Test] [Test]
public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero() public void should_delete_movie_metadata_files_that_have_moviefileid_of_zero()
{ {
var series = Builder<Series>.CreateNew() var movie = Builder<Movie>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(movie);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.MovieId = movie.Id)
.With(m => m.Type = MetadataType.EpisodeMetadata) .With(m => m.Type = MetadataType.MovieMetadata)
.With(m => m.EpisodeFileId = 0) .With(m => m.MovieFileId = 0)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);
@ -105,17 +105,17 @@ public void should_delete_episode_metadata_files_that_have_episodefileid_of_zero
} }
[Test] [Test]
public void should_delete_episode_image_files_that_have_episodefileid_of_zero() public void should_delete_movie_image_files_that_have_moviefileid_of_zero()
{ {
var series = Builder<Series>.CreateNew() var movie = Builder<Movie>.CreateNew()
.BuildNew(); .BuildNew();
Db.Insert(series); Db.Insert(movie);
var metadataFile = Builder<MetadataFile>.CreateNew() var metadataFile = Builder<MetadataFile>.CreateNew()
.With(m => m.SeriesId = series.Id) .With(m => m.MovieId = movie.Id)
.With(m => m.Type = MetadataType.EpisodeImage) .With(m => m.Type = MetadataType.MovieImage)
.With(m => m.EpisodeFileId = 0) .With(m => m.MovieFileId = 0)
.BuildNew(); .BuildNew();
Db.Insert(metadataFile); Db.Insert(metadataFile);

View File

@ -1,4 +1,4 @@
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@ -13,66 +13,56 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox
[TestFixture] [TestFixture]
public class FindMetadataFileFixture : CoreTest<RoksboxMetadata> public class FindMetadataFileFixture : CoreTest<RoksboxMetadata>
{ {
private Series _series; private Movie _movie;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateNew() _movie = Builder<Movie>.CreateNew()
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .With(s => s.Path = @"C:\Test\Movies\The.Movie.2011".AsOsAgnostic())
.Build(); .Build();
} }
[Test] [Test]
public void should_return_null_if_filename_is_not_handled() public void should_return_null_if_filename_is_not_handled()
{ {
var path = Path.Combine(_series.Path, "file.jpg"); var path = Path.Combine(_movie.Path, "file.jpg");
Subject.FindMetadataFile(_series, path).Should().BeNull(); Subject.FindMetadataFile(_movie, path).Should().BeNull();
} }
[TestCase("Specials")] [TestCase(".xml", MetadataType.MovieMetadata)]
[TestCase("specials")] [TestCase(".jpg", MetadataType.MovieImage)]
[TestCase("Season 1")] public void should_return_metadata_for_movie_if_valid_file_for_movie(string extension, MetadataType type)
public void should_return_season_image(string folder)
{ {
var path = Path.Combine(_series.Path, folder, folder + ".jpg"); var path = Path.Combine(_movie.Path, "the.movie.2011" + extension);
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); Subject.FindMetadataFile(_movie, path).Type.Should().Be(type);
}
[TestCase(".xml", MetadataType.EpisodeMetadata)]
[TestCase(".jpg", MetadataType.EpisodeImage)]
public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type)
{
var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension);
Subject.FindMetadataFile(_series, path).Type.Should().Be(type);
} }
[TestCase(".xml")] [TestCase(".xml")]
[TestCase(".jpg")] [TestCase(".jpg")]
public void should_return_null_if_not_valid_file_for_episode(string extension) public void should_return_null_if_not_valid_file_for_movie(string extension)
{ {
var path = Path.Combine(_series.Path, "the.series.episode" + extension); var path = Path.Combine(_movie.Path, "the.movie.here" + extension);
Subject.FindMetadataFile(_series, path).Should().BeNull(); Subject.FindMetadataFile(_movie, path).Should().BeNull();
} }
[Test] [Test]
public void should_not_return_metadata_if_image_file_is_a_thumb() public void should_not_return_metadata_if_image_file_is_a_thumb()
{ {
var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg"); var path = Path.Combine(_movie.Path, "the.movie.2011-thumb.jpg");
Subject.FindMetadataFile(_series, path).Should().BeNull(); Subject.FindMetadataFile(_movie, path).Should().BeNull();
} }
[Test] [Test]
public void should_return_series_image_for_folder_jpg_in_series_folder() public void should_return_movie_image_for_folder_jpg_in_movie_folder()
{ {
var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg"); var path = Path.Combine(_movie.Path, new DirectoryInfo(_movie.Path).Name + ".jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); Subject.FindMetadataFile(_movie, path).Type.Should().Be(MetadataType.MovieImage);
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.IO; using System.IO;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@ -13,58 +13,48 @@ namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv
[TestFixture] [TestFixture]
public class FindMetadataFileFixture : CoreTest<WdtvMetadata> public class FindMetadataFileFixture : CoreTest<WdtvMetadata>
{ {
private Series _series; private Movie _movie;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_series = Builder<Series>.CreateNew() _movie = Builder<Movie>.CreateNew()
.With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) .With(s => s.Path = @"C:\Test\Movies\The.Movie".AsOsAgnostic())
.Build(); .Build();
} }
[Test] [Test]
public void should_return_null_if_filename_is_not_handled() public void should_return_null_if_filename_is_not_handled()
{ {
var path = Path.Combine(_series.Path, "file.jpg"); var path = Path.Combine(_movie.Path, "file.jpg");
Subject.FindMetadataFile(_series, path).Should().BeNull(); Subject.FindMetadataFile(_movie, path).Should().BeNull();
} }
[TestCase("Specials")] [TestCase(".xml", MetadataType.MovieMetadata)]
[TestCase("specials")] [TestCase(".metathumb", MetadataType.MovieImage)]
[TestCase("Season 1")] public void should_return_metadata_for_movie_if_valid_file_for_movie(string extension, MetadataType type)
public void should_return_season_image(string folder)
{ {
var path = Path.Combine(_series.Path, folder, "folder.jpg"); var path = Path.Combine(_movie.Path, "the.movie.2011" + extension);
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); Subject.FindMetadataFile(_movie, path).Type.Should().Be(type);
}
[TestCase(".xml", MetadataType.EpisodeMetadata)]
[TestCase(".metathumb", MetadataType.EpisodeImage)]
public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type)
{
var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension);
Subject.FindMetadataFile(_series, path).Type.Should().Be(type);
} }
[TestCase(".xml")] [TestCase(".xml")]
[TestCase(".metathumb")] [TestCase(".metathumb")]
public void should_return_null_if_not_valid_file_for_episode(string extension) public void should_return_null_if_not_valid_file_for_movie(string extension)
{ {
var path = Path.Combine(_series.Path, "the.series.episode" + extension); var path = Path.Combine(_movie.Path, "the.movie" + extension);
Subject.FindMetadataFile(_series, path).Should().BeNull(); Subject.FindMetadataFile(_movie, path).Should().BeNull();
} }
[Test] [Test]
public void should_return_series_image_for_folder_jpg_in_series_folder() public void should_return_movie_image_for_folder_jpg_in_movie_folder()
{ {
var path = Path.Combine(_series.Path, "folder.jpg"); var path = Path.Combine(_movie.Path, "folder.jpg");
Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); Subject.FindMetadataFile(_movie, path).Type.Should().Be(MetadataType.MovieImage);
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -294,9 +294,16 @@ public bool EnableMediaInfo
set { SetValue("EnableMediaInfo", value); } set { SetValue("EnableMediaInfo", value); }
} }
public bool ImportExtraFiles
{
get { return GetValueBoolean("ImportExtraFiles", false); }
set { SetValue("ImportExtraFiles", value); }
}
public string ExtraFileExtensions public string ExtraFileExtensions
{ {
get { return GetValue("ExtraFileExtensions", ""); } get { return GetValue("ExtraFileExtensions", "srt"); }
set { SetValue("ExtraFileExtensions", value); } set { SetValue("ExtraFileExtensions", value); }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Common.Http.Proxy; using NzbDrone.Common.Http.Proxy;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
@ -33,6 +33,7 @@ public interface IConfigService
bool SkipFreeSpaceCheckWhenImporting { get; set; } bool SkipFreeSpaceCheckWhenImporting { get; set; }
bool CopyUsingHardlinks { get; set; } bool CopyUsingHardlinks { get; set; }
bool EnableMediaInfo { get; set; } bool EnableMediaInfo { get; set; }
bool ImportExtraFiles { get; set; }
string ExtraFileExtensions { get; set; } string ExtraFileExtensions { get; set; }
bool AutoRenameFolders { get; set; } bool AutoRenameFolders { get; set; }
bool PathsDefaultStatic { get; set; } bool PathsDefaultStatic { get; set; }

View File

@ -0,0 +1,44 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(142)]
public class movie_extras : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Delete.Table("ExtraFiles");
Delete.Table("SubtitleFiles");
Delete.Table("MetadataFiles");
Create.TableForModel("ExtraFiles")
.WithColumn("MovieId").AsInt32().NotNullable()
.WithColumn("MovieFileId").AsInt32().NotNullable()
.WithColumn("RelativePath").AsString().NotNullable()
.WithColumn("Extension").AsString().NotNullable()
.WithColumn("Added").AsDateTime().NotNullable()
.WithColumn("LastUpdated").AsDateTime().NotNullable();
Create.TableForModel("SubtitleFiles")
.WithColumn("MovieId").AsInt32().NotNullable()
.WithColumn("MovieFileId").AsInt32().NotNullable()
.WithColumn("RelativePath").AsString().NotNullable()
.WithColumn("Extension").AsString().NotNullable()
.WithColumn("Added").AsDateTime().NotNullable()
.WithColumn("LastUpdated").AsDateTime().NotNullable()
.WithColumn("Language").AsInt32().NotNullable();
Create.TableForModel("MetadataFiles")
.WithColumn("MovieId").AsInt32().NotNullable()
.WithColumn("Consumer").AsString().NotNullable()
.WithColumn("Type").AsInt32().NotNullable()
.WithColumn("RelativePath").AsString().NotNullable()
.WithColumn("LastUpdated").AsDateTime().NotNullable()
.WithColumn("MovieFileId").AsInt32().Nullable()
.WithColumn("Hash").AsString().Nullable()
.WithColumn("Added").AsDateTime().Nullable()
.WithColumn("Extension").AsString().NotNullable();
}
}
}

View File

@ -36,45 +36,7 @@
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.NetImport; using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions; using NzbDrone.Core.NetImport.ImportExclusions;
using System;
using System.Collections.Generic;
using Marr.Data;
using Marr.Data.Mapping;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Datastore.Converters;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Restrictions;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Extras.Metadata;
using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Movies.AlternativeTitles; using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.NetImport;
using NzbDrone.Core.NetImport.ImportExclusions;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
@ -252,4 +214,4 @@ private static void RegisterEmbeddedConverter()
} }
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -10,7 +10,7 @@
namespace NzbDrone.Core.Extras namespace NzbDrone.Core.Extras
{ {
public class ExistingExtraFileService : IHandle<SeriesScannedEvent> public class ExistingExtraFileService : IHandle<MovieScannedEvent>
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
@ -28,29 +28,29 @@ public ExistingExtraFileService(IDiskProvider diskProvider,
_logger = logger; _logger = logger;
} }
public void Handle(SeriesScannedEvent message) public void Handle(MovieScannedEvent message)
{ {
var series = message.Series; var movie = message.Movie;
var extraFiles = new List<ExtraFile>(); var extraFiles = new List<ExtraFile>();
if (!_diskProvider.FolderExists(series.Path)) if (!_diskProvider.FolderExists(movie.Path))
{ {
return; return;
} }
_logger.Debug("Looking for existing extra files in {0}", series.Path); _logger.Debug("Looking for existing extra files in {0}", movie.Path);
var filesOnDisk = _diskScanService.GetNonVideoFiles(series.Path); var filesOnDisk = _diskScanService.GetNonVideoFiles(movie.Path);
var possibleExtraFiles = _diskScanService.FilterFiles(series, filesOnDisk); var possibleExtraFiles = _diskScanService.FilterFiles(movie, filesOnDisk);
var filteredFiles = possibleExtraFiles; var filteredFiles = possibleExtraFiles;
var importedFiles = new List<string>(); var importedFiles = new List<string>();
foreach (var existingExtraFileImporter in _existingExtraFileImporters) foreach (var existingExtraFileImporter in _existingExtraFileImporters)
{ {
var imported = existingExtraFileImporter.ProcessFiles(series, filteredFiles, importedFiles); var imported = existingExtraFileImporter.ProcessFiles(movie, filteredFiles, importedFiles);
importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); importedFiles.AddRange(imported.Select(f => Path.Combine(movie.Path, f.RelativePath)));
} }
_logger.Info("Found {0} extra files", extraFiles.Count); _logger.Info("Found {0} extra files", extraFiles.Count);

View File

@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Marr.Data;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -18,50 +19,50 @@ namespace NzbDrone.Core.Extras
{ {
public interface IExtraService public interface IExtraService
{ {
void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly); void ImportExtraFiles(LocalMovie localMovie, MovieFile movieFile, bool isReadOnly);
} }
public class ExtraService : IExtraService, public class ExtraService : IExtraService,
IHandle<MediaCoversUpdatedEvent>, IHandle<MediaCoversUpdatedEvent>,
IHandle<EpisodeFolderCreatedEvent>, IHandle<MovieRenamedEvent>
IHandle<SeriesRenamedEvent>
{ {
private readonly IMediaFileService _mediaFileService; private readonly IMediaFileService _mediaFileService;
private readonly IEpisodeService _episodeService; private readonly IMovieService _movieService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly List<IManageExtraFiles> _extraFileManagers; private readonly List<IManageExtraFiles> _extraFileManagers;
private readonly Logger _logger; private readonly Logger _logger;
public ExtraService(IMediaFileService mediaFileService, public ExtraService(IMediaFileService mediaFileService,
IEpisodeService episodeService, IMovieService movieService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IConfigService configService, IConfigService configService,
List<IManageExtraFiles> extraFileManagers, List<IManageExtraFiles> extraFileManagers,
Logger logger) Logger logger)
{ {
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_episodeService = episodeService; _movieService = movieService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configService = configService; _configService = configService;
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList(); _extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
_logger = logger; _logger = logger;
} }
public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile, bool isReadOnly) public void ImportExtraFiles(LocalMovie localMovie, MovieFile movieFile, bool isReadOnly)
{ {
var series = localEpisode.Series; var movie = localMovie.Movie;
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.CreateAfterEpisodeImport(series, episodeFile); extraFileManager.CreateAfterMovieImport(movie, movieFile);
} }
// TODO: Remove if (!_configService.ImportExtraFiles)
// Not importing files yet, testing that parsing is working properly first {
return; return;
}
var sourcePath = localEpisode.Path; var sourcePath = localMovie.Path;
var sourceFolder = _diskProvider.GetParentFolder(sourcePath); var sourceFolder = _diskProvider.GetParentFolder(sourcePath);
var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath); var sourceFileName = Path.GetFileNameWithoutExtension(sourcePath);
var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly); var files = _diskProvider.GetFiles(sourceFolder, SearchOption.TopDirectoryOnly);
@ -70,7 +71,7 @@ public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile,
.Select(e => e.Trim(' ', '.')) .Select(e => e.Trim(' ', '.'))
.ToList(); .ToList();
var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName)); var matchingFilenames = files.Where(f => Path.GetFileNameWithoutExtension(f).StartsWith(sourceFileName, StringComparison.InvariantCultureIgnoreCase));
foreach (var matchingFilename in matchingFilenames) foreach (var matchingFilename in matchingFilenames)
{ {
@ -85,7 +86,8 @@ public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile,
{ {
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
var extraFile = extraFileManager.Import(series, episodeFile, matchingFilename, matchingExtension, isReadOnly); var extension = Path.GetExtension(matchingFilename);
var extraFile = extraFileManager.Import(movie, movieFile, matchingFilename, extension, isReadOnly);
if (extraFile != null) if (extraFile != null)
{ {
@ -102,60 +104,36 @@ public void ImportExtraFiles(LocalEpisode localEpisode, EpisodeFile episodeFile,
public void Handle(MediaCoversUpdatedEvent message) public void Handle(MediaCoversUpdatedEvent message)
{ {
//var series = message.Series; var movie = message.Movie;
//var episodeFiles = GetEpisodeFiles(series.Id); var movieFiles = GetMovieFiles(movie.Id);
//foreach (var extraFileManager in _extraFileManagers)
//{
// extraFileManager.CreateAfterSeriesScan(series, episodeFiles);
//}
}
//TODO: Implementing this will fix a lot of our warning exceptions
//public void Handle(MediaCoversUpdatedEvent message)
//{
// var movie = message.Movie;
// var movieFiles = GetMovieFiles(movie.Id);
// foreach (var extraFileManager in _extraFileManagers)
// {
// extraFileManager.CreateAfterMovieScan(movie, movieFiles);
// }
//}
public void Handle(EpisodeFolderCreatedEvent message)
{
var series = message.Series;
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.CreateAfterEpisodeImport(series, message.SeriesFolder, message.SeasonFolder); extraFileManager.CreateAfterMovieScan(movie, movieFiles);
} }
} }
public void Handle(SeriesRenamedEvent message) public void Handle(MovieRenamedEvent message)
{ {
var series = message.Series; var movie = message.Movie;
var episodeFiles = GetEpisodeFiles(series.Id); var movieFiles = GetMovieFiles(movie.Id);
foreach (var extraFileManager in _extraFileManagers) foreach (var extraFileManager in _extraFileManagers)
{ {
extraFileManager.MoveFilesAfterRename(series, episodeFiles); extraFileManager.MoveFilesAfterRename(movie, movieFiles);
} }
} }
private List<EpisodeFile> GetEpisodeFiles(int seriesId) private List<MovieFile> GetMovieFiles(int movieId)
{ {
var episodeFiles = _mediaFileService.GetFilesBySeries(seriesId); var movieFiles = _mediaFileService.GetFilesByMovie(movieId);
var episodes = _episodeService.GetEpisodeBySeries(seriesId);
foreach (var episodeFile in episodeFiles) foreach (var movieFile in movieFiles)
{ {
var localEpisodeFile = episodeFile; movieFile.Movie = new LazyLoaded<Movie>(_movieService.GetMovie(movieId));
episodeFile.Episodes = new LazyList<Episode>(episodes.Where(e => e.EpisodeFileId == localEpisodeFile.Id));
} }
return episodeFiles; return movieFiles;
} }
} }
} }

View File

@ -1,16 +1,22 @@
using System; using System;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Extras.Files namespace NzbDrone.Core.Extras.Files
{ {
public abstract class ExtraFile : ModelBase public abstract class ExtraFile : ModelBase
{ {
public int SeriesId { get; set; } public int MovieId { get; set; }
public int? EpisodeFileId { get; set; } public int? MovieFileId { get; set; }
public int? SeasonNumber { get; set; }
public string RelativePath { get; set; } public string RelativePath { get; set; }
public DateTime Added { get; set; } public DateTime Added { get; set; }
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }
public string Extension { get; set; } public string Extension { get; set; }
} }
public enum ExtraFileType
{
Subtitle = 0,
Metadata = 1,
Other = 2
}
} }

View File

@ -1,5 +1,8 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text;
using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
@ -11,11 +14,10 @@ namespace NzbDrone.Core.Extras.Files
public interface IManageExtraFiles public interface IManageExtraFiles
{ {
int Order { get; } int Order { get; }
IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); IEnumerable<ExtraFile> CreateAfterMovieScan(Movie movie, List<MovieFile> movieFiles);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile);
IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles);
IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly);
ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
} }
public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
@ -23,29 +25,40 @@ public abstract class ExtraFileManager<TExtraFile> : IManageExtraFiles
{ {
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IDiskProvider _diskProvider;
private readonly IDiskTransferService _diskTransferService; private readonly IDiskTransferService _diskTransferService;
private readonly IExtraFileService<TExtraFile> _extraFileService; private readonly Logger _logger;
public ExtraFileManager(IConfigService configService, public ExtraFileManager(IConfigService configService,
IDiskProvider diskProvider,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
IExtraFileService<TExtraFile> extraFileService) Logger logger)
{ {
_configService = configService; _configService = configService;
_diskProvider = diskProvider;
_diskTransferService = diskTransferService; _diskTransferService = diskTransferService;
_extraFileService = extraFileService; _logger = logger;
} }
public abstract int Order { get; } public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles); public abstract IEnumerable<ExtraFile> CreateAfterMovieScan(Movie movie, List<MovieFile> movieFiles);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile); public abstract IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile);
public abstract IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder); public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles);
public abstract IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles); public abstract ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly);
public abstract ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly);
protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) protected TExtraFile ImportFile(Movie movie, MovieFile movieFile, string path, bool readOnly, string extension, string fileNameSuffix = null)
{ {
var newFileName = Path.Combine(series.Path, Path.ChangeExtension(episodeFile.RelativePath, extension)); var newFolder = Path.GetDirectoryName(Path.Combine(movie.Path, movieFile.RelativePath));
var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(movieFile.RelativePath));
if (fileNameSuffix.IsNotNullOrWhiteSpace())
{
filenameBuilder.Append(fileNameSuffix);
}
filenameBuilder.Append(extension);
var newFileName = Path.Combine(newFolder, filenameBuilder.ToString());
var transferMode = TransferMode.Move; var transferMode = TransferMode.Move;
if (readOnly) if (readOnly)
@ -57,12 +70,45 @@ protected TExtraFile ImportFile(Series series, EpisodeFile episodeFile, string p
return new TExtraFile return new TExtraFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
SeasonNumber = episodeFile.SeasonNumber, MovieFileId = movieFile.Id,
EpisodeFileId = episodeFile.Id, RelativePath = movie.Path.GetRelativePath(newFileName),
RelativePath = series.Path.GetRelativePath(newFileName), Extension = extension
Extension = Path.GetExtension(path)
}; };
} }
protected TExtraFile MoveFile(Movie movie, MovieFile movieFile, TExtraFile extraFile, string fileNameSuffix = null)
{
var newFolder = Path.GetDirectoryName(Path.Combine(movie.Path, movieFile.RelativePath));
var filenameBuilder = new StringBuilder(Path.GetFileNameWithoutExtension(movieFile.RelativePath));
if (fileNameSuffix.IsNotNullOrWhiteSpace())
{
filenameBuilder.Append(fileNameSuffix);
}
filenameBuilder.Append(extraFile.Extension);
var existingFileName = Path.Combine(movie.Path, extraFile.RelativePath);
var newFileName = Path.Combine(newFolder, filenameBuilder.ToString());
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = movie.Path.GetRelativePath(newFileName);
return extraFile;
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move file after rename: {0}", existingFileName);
}
}
return null;
}
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -7,12 +7,10 @@ namespace NzbDrone.Core.Extras.Files
{ {
public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new() public interface IExtraFileRepository<TExtraFile> : IBasicRepository<TExtraFile> where TExtraFile : ExtraFile, new()
{ {
void DeleteForSeries(int seriesId); void DeleteForMovie(int movieId);
void DeleteForSeason(int seriesId, int seasonNumber); void DeleteForMovieFile(int movieFileId);
void DeleteForEpisodeFile(int episodeFileId); List<TExtraFile> GetFilesByMovie(int movieId);
List<TExtraFile> GetFilesBySeries(int seriesId); List<TExtraFile> GetFilesByMovieFile(int movieFileId);
List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId);
TExtraFile FindByPath(string path); TExtraFile FindByPath(string path);
} }
@ -24,34 +22,24 @@ public ExtraFileRepository(IMainDatabase database, IEventAggregator eventAggrega
{ {
} }
public void DeleteForSeries(int seriesId) public void DeleteForMovie(int movieId)
{ {
Delete(c => c.SeriesId == seriesId); Delete(c => c.MovieId == movieId);
} }
public void DeleteForSeason(int seriesId, int seasonNumber) public void DeleteForMovieFile(int movieFileId)
{ {
Delete(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber); Delete(c => c.MovieFileId == movieFileId);
} }
public void DeleteForEpisodeFile(int episodeFileId) public List<TExtraFile> GetFilesByMovie(int movieId)
{ {
Delete(c => c.EpisodeFileId == episodeFileId); return Query.Where(c => c.MovieId == movieId);
} }
public List<TExtraFile> GetFilesBySeries(int seriesId) public List<TExtraFile> GetFilesByMovieFile(int movieFileId)
{ {
return Query.Where(c => c.SeriesId == seriesId); return Query.Where(c => c.MovieFileId == movieFileId);
}
public List<TExtraFile> GetFilesBySeason(int seriesId, int seasonNumber)
{
return Query.Where(c => c.SeriesId == seriesId && c.SeasonNumber == seasonNumber);
}
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId)
{
return Query.Where(c => c.EpisodeFileId == episodeFileId);
} }
public TExtraFile FindByPath(string path) public TExtraFile FindByPath(string path)

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -15,8 +15,8 @@ namespace NzbDrone.Core.Extras.Files
public interface IExtraFileService<TExtraFile> public interface IExtraFileService<TExtraFile>
where TExtraFile : ExtraFile, new() where TExtraFile : ExtraFile, new()
{ {
List<TExtraFile> GetFilesBySeries(int seriesId); List<TExtraFile> GetFilesByMovie(int movieId);
List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId); List<TExtraFile> GetFilesByMovieFile(int movieFileId);
TExtraFile FindByPath(string path); TExtraFile FindByPath(string path);
void Upsert(TExtraFile extraFile); void Upsert(TExtraFile extraFile);
void Upsert(List<TExtraFile> extraFiles); void Upsert(List<TExtraFile> extraFiles);
@ -25,24 +25,24 @@ public interface IExtraFileService<TExtraFile>
} }
public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>, public abstract class ExtraFileService<TExtraFile> : IExtraFileService<TExtraFile>,
IHandleAsync<SeriesDeletedEvent>, IHandleAsync<MovieDeletedEvent>,
IHandleAsync<EpisodeFileDeletedEvent> IHandleAsync<MovieFileDeletedEvent>
where TExtraFile : ExtraFile, new() where TExtraFile : ExtraFile, new()
{ {
private readonly IExtraFileRepository<TExtraFile> _repository; private readonly IExtraFileRepository<TExtraFile> _repository;
private readonly ISeriesService _seriesService; private readonly IMovieService _movieService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IRecycleBinProvider _recycleBinProvider; private readonly IRecycleBinProvider _recycleBinProvider;
private readonly Logger _logger; private readonly Logger _logger;
public ExtraFileService(IExtraFileRepository<TExtraFile> repository, public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
ISeriesService seriesService, IMovieService movieService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IRecycleBinProvider recycleBinProvider, IRecycleBinProvider recycleBinProvider,
Logger logger) Logger logger)
{ {
_repository = repository; _repository = repository;
_seriesService = seriesService; _movieService = movieService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_recycleBinProvider = recycleBinProvider; _recycleBinProvider = recycleBinProvider;
_logger = logger; _logger = logger;
@ -50,14 +50,14 @@ public ExtraFileService(IExtraFileRepository<TExtraFile> repository,
public virtual bool PermanentlyDelete => false; public virtual bool PermanentlyDelete => false;
public List<TExtraFile> GetFilesBySeries(int seriesId) public List<TExtraFile> GetFilesByMovie(int movieId)
{ {
return _repository.GetFilesBySeries(seriesId); return _repository.GetFilesByMovie(movieId);
} }
public List<TExtraFile> GetFilesByEpisodeFile(int episodeFileId) public List<TExtraFile> GetFilesByMovieFile(int movieFileId)
{ {
return _repository.GetFilesByEpisodeFile(episodeFileId); return _repository.GetFilesByMovieFile(movieFileId);
} }
public TExtraFile FindByPath(string path) public TExtraFile FindByPath(string path)
@ -96,28 +96,28 @@ public void DeleteMany(IEnumerable<int> ids)
_repository.DeleteMany(ids); _repository.DeleteMany(ids);
} }
public void HandleAsync(SeriesDeletedEvent message) public void HandleAsync(MovieDeletedEvent message)
{ {
_logger.Debug("Deleting Extra from database for series: {0}", message.Series); _logger.Debug("Deleting Extra from database for movie: {0}", message.Movie);
_repository.DeleteForSeries(message.Series.Id); _repository.DeleteForMovie(message.Movie.Id);
} }
public void HandleAsync(EpisodeFileDeletedEvent message) public void HandleAsync(MovieFileDeletedEvent message)
{ {
var episodeFile = message.EpisodeFile; var movieFile = message.MovieFile;
if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes) if (message.Reason == DeleteMediaFileReason.NoLinkedEpisodes)
{ {
_logger.Debug("Removing episode file from DB as part of cleanup routine, not deleting extra files from disk."); _logger.Debug("Removing movie file from DB as part of cleanup routine, not deleting extra files from disk.");
} }
else else
{ {
var series = _seriesService.GetSeries(message.EpisodeFile.SeriesId); var movie = _movieService.GetMovie(message.MovieFile.MovieId);
foreach (var extra in _repository.GetFilesByEpisodeFile(episodeFile.Id)) foreach (var extra in _repository.GetFilesByMovieFile(movieFile.Id))
{ {
var path = Path.Combine(series.Path, extra.RelativePath); var path = Path.Combine(movie.Path, extra.RelativePath);
if (_diskProvider.FileExists(path)) if (_diskProvider.FileExists(path))
{ {
@ -135,8 +135,8 @@ public void HandleAsync(EpisodeFileDeletedEvent message)
} }
} }
_logger.Debug("Deleting Extra from database for episode file: {0}", episodeFile); _logger.Debug("Deleting Extra from database for movie file: {0}", movieFile);
_repository.DeleteForEpisodeFile(episodeFile.Id); _repository.DeleteForMovieFile(movieFile.Id);
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras
public interface IImportExistingExtraFiles public interface IImportExistingExtraFiles
{ {
int Order { get; } int Order { get; }
IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); IEnumerable<ExtraFile> ProcessFiles(Movie movie, List<string> filesOnDisk, List<string> importedFiles);
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NzbDrone.Common; using NzbDrone.Common;
@ -19,21 +19,21 @@ public ImportExistingExtraFilesBase(IExtraFileService<TExtraFile> extraFileServi
} }
public abstract int Order { get; } public abstract int Order { get; }
public abstract IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles); public abstract IEnumerable<ExtraFile> ProcessFiles(Movie movie, List<string> filesOnDisk, List<string> importedFiles);
public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Series series, List<string> filesOnDisk, List<string> importedFiles) public virtual ImportExistingExtraFileFilterResult<TExtraFile> FilterAndClean(Movie movie, List<string> filesOnDisk, List<string> importedFiles)
{ {
var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); var movieFiles = _extraFileService.GetFilesByMovie(movie.Id);
Clean(series, filesOnDisk, importedFiles, seriesFiles); Clean(movie, filesOnDisk, importedFiles, movieFiles);
return Filter(series, filesOnDisk, importedFiles, seriesFiles); return Filter(movie, filesOnDisk, importedFiles, movieFiles);
} }
private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles) private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Movie movie, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> movieFiles)
{ {
var previouslyImported = seriesFiles.IntersectBy(s => Path.Combine(series.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList(); var previouslyImported = movieFiles.IntersectBy(s => Path.Combine(movie.Path, s.RelativePath), filesOnDisk, f => f, PathEqualityComparer.Instance).ToList();
var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(series.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance) var filteredFiles = filesOnDisk.Except(previouslyImported.Select(f => Path.Combine(movie.Path, f.RelativePath)).ToList(), PathEqualityComparer.Instance)
.Except(importedFiles, PathEqualityComparer.Instance) .Except(importedFiles, PathEqualityComparer.Instance)
.ToList(); .ToList();
@ -42,12 +42,12 @@ private ImportExistingExtraFileFilterResult<TExtraFile> Filter(Series series, Li
return new ImportExistingExtraFileFilterResult<TExtraFile>(previouslyImported, filteredFiles); return new ImportExistingExtraFileFilterResult<TExtraFile>(previouslyImported, filteredFiles);
} }
private void Clean(Series series, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> seriesFiles) private void Clean(Movie movie, List<string> filesOnDisk, List<string> importedFiles, List<TExtraFile> movieFiles)
{ {
var alreadyImportedFileIds = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance) var alreadyImportedFileIds = movieFiles.IntersectBy(f => Path.Combine(movie.Path, f.RelativePath), importedFiles, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id); .Select(f => f.Id);
var deletedFiles = seriesFiles.ExceptBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance) var deletedFiles = movieFiles.ExceptBy(f => Path.Combine(movie.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance)
.Select(f => f.Id); .Select(f => f.Id);
_extraFileService.DeleteMany(alreadyImportedFileIds); _extraFileService.DeleteMany(alreadyImportedFileIds);

View File

@ -25,7 +25,7 @@ public MediaBrowserMetadata(
public override string Name => "Emby (Legacy)"; public override string Name => "Emby (Legacy)";
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Movie movie, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -33,28 +33,28 @@ public override MetadataFile FindMetadataFile(Series series, string path)
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = movie.Path.GetRelativePath(path)
}; };
if (filename.Equals("series.xml", StringComparison.InvariantCultureIgnoreCase)) if (filename.Equals("movie.xml", StringComparison.InvariantCultureIgnoreCase))
{ {
metadata.Type = MetadataType.SeriesMetadata; metadata.Type = MetadataType.MovieMetadata;
return metadata; return metadata;
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile)
{ {
if (!Settings.SeriesMetadata) if (!Settings.MovieMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating series.xml for: {0}", series.Title); _logger.Debug("Generating movie.xml for: {0}", movie.Title);
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
@ -62,97 +62,39 @@ public override MetadataFileResult SeriesMetadata(Series series)
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var tvShow = new XElement("Series"); var movieElement = new XElement("Movie");
tvShow.Add(new XElement("id", series.TvdbId)); movieElement.Add(new XElement("id", movie.ImdbId));
tvShow.Add(new XElement("Status", series.Status)); movieElement.Add(new XElement("Status", movie.Status));
tvShow.Add(new XElement("Network", series.Network));
tvShow.Add(new XElement("Airs_Time", series.AirTime));
if (series.FirstAired.HasValue) movieElement.Add(new XElement("Added", movie.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
{ movieElement.Add(new XElement("LockData", "false"));
tvShow.Add(new XElement("FirstAired", series.FirstAired.Value.ToString("yyyy-MM-dd"))); movieElement.Add(new XElement("Overview", movie.Overview));
} movieElement.Add(new XElement("LocalTitle", movie.Title));
tvShow.Add(new XElement("ContentRating", series.Certification)); movieElement.Add(new XElement("Rating", movie.Ratings.Value));
tvShow.Add(new XElement("Added", series.Added.ToString("MM/dd/yyyy HH:mm:ss tt"))); movieElement.Add(new XElement("ProductionYear", movie.Year));
tvShow.Add(new XElement("LockData", "false")); movieElement.Add(new XElement("RunningTime", movie.Runtime));
tvShow.Add(new XElement("Overview", series.Overview)); movieElement.Add(new XElement("IMDB", movie.ImdbId));
tvShow.Add(new XElement("LocalTitle", series.Title)); movieElement.Add(new XElement("Genres", movie.Genres.Select(genre => new XElement("Genre", genre))));
if (series.FirstAired.HasValue) var doc = new XDocument(movieElement);
{
tvShow.Add(new XElement("PremiereDate", series.FirstAired.Value.ToString("yyyy-MM-dd")));
}
tvShow.Add(new XElement("Rating", series.Ratings.Value));
tvShow.Add(new XElement("ProductionYear", series.Year));
tvShow.Add(new XElement("RunningTime", series.Runtime));
tvShow.Add(new XElement("IMDB", series.ImdbId));
tvShow.Add(new XElement("TVRageId", series.TvRageId));
tvShow.Add(new XElement("Genres", series.Genres.Select(genre => new XElement("Genre", genre))));
var persons = new XElement("Persons");
foreach (var person in series.Actors)
{
persons.Add(new XElement("Person",
new XElement("Name", person.Name),
new XElement("Type", "Actor"),
new XElement("Role", person.Character)
));
}
tvShow.Add(persons);
var doc = new XDocument(tvShow);
doc.Save(xw); doc.Save(xw);
_logger.Debug("Saving series.xml for {0}", series.Title); _logger.Debug("Saving movie.xml for {0}", movie.Title);
return new MetadataFileResult("series.xml", doc.ToString()); return new MetadataFileResult("movie.xml", doc.ToString());
} }
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{
return null;
}
public override List<ImageFileResult> SeriesImages(Series series)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) private IEnumerable<ImageFileResult> ProcessMovieImages(Movie movie)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
{
return new List<ImageFileResult>();
}
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series)
{
return new List<ImageFileResult>();
}
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season)
{
return new List<ImageFileResult>();
}
private string GetEpisodeNfoFilename(string episodeFilePath)
{
return null;
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return null;
}
} }
} }

View File

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -18,11 +18,11 @@ public class MediaBrowserMetadataSettings : IProviderConfig
public MediaBrowserMetadataSettings() public MediaBrowserMetadataSettings()
{ {
SeriesMetadata = true; MovieMetadata = true;
} }
[FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Movie Metadata", Type = FieldType.Checkbox)]
public bool SeriesMetadata { get; set; } public bool MovieMetadata { get; set; }
public bool IsValid => true; public bool IsValid => true;

View File

@ -31,30 +31,30 @@ public RoksboxMetadata(IMapCoversToLocal mediaCoverService,
_logger = logger; _logger = logger;
} }
private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" }; //Re-enable when/if we store and use mpaa certification
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); //private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" };
public override string Name => "Roksbox"; public override string Name => "Roksbox";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.MovieImage)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetMovieFileImageFilename(movieFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.MovieMetadata)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetMovieFileMetadataFilename(movieFilePath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown movie file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(movie.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Movie movie, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -63,81 +63,47 @@ public override MetadataFile FindMetadataFile(Series series, string path)
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = movie.Path.GetRelativePath(path)
}; };
//Series and season images are both named folder.jpg, only season ones sit in season folders var parseResult = Parser.Parser.ParseMovieTitle(filename, false);
if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase))
{
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
if (seasonMatch.Success) if (parseResult != null)
{
metadata.Type = MetadataType.SeasonImage;
if (seasonMatch.Groups["specials"].Success)
{
metadata.SeasonNumber = 0;
}
else
{
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value);
}
return metadata;
}
metadata.Type = MetadataType.SeriesImage;
return metadata;
}
var parseResult = Parser.Parser.ParseTitle(filename);
if (parseResult != null &&
!parseResult.FullSeason)
{ {
var extension = Path.GetExtension(filename).ToLowerInvariant(); var extension = Path.GetExtension(filename).ToLowerInvariant();
if (extension == ".xml") if (extension == ".xml")
{ {
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.MovieMetadata;
return metadata; return metadata;
} }
if (extension == ".jpg") if (extension == ".jpg")
{ {
if (!Path.GetFileNameWithoutExtension(filename).EndsWith("-thumb")) if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase))
{ {
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.MovieImage;
return metadata; return metadata;
} }
} }
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile)
{ {
//Series metadata is not supported if (!Settings.MovieMetadata)
return null;
}
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
{
if (!Settings.EpisodeMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating Episode Metadata for: {0}", episodeFile.RelativePath); _logger.Debug("Generating Movie File Metadata for: {0}", movieFile.RelativePath);
var xmlResult = string.Empty; var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
@ -148,24 +114,11 @@ public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile ep
var doc = new XDocument(); var doc = new XDocument();
var details = new XElement("video"); var details = new XElement("video");
details.Add(new XElement("title", string.Format("{0} - {1}x{2} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); details.Add(new XElement("title", movie.Title));
details.Add(new XElement("year", episode.AirDate));
details.Add(new XElement("genre", string.Join(" / ", series.Genres)));
var actors = string.Join(" , ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character).GetRange(0, Math.Min(3, series.Actors.Count)));
details.Add(new XElement("actors", actors));
details.Add(new XElement("description", episode.Overview));
details.Add(new XElement("length", series.Runtime));
if (series.Certification.IsNotNullOrWhiteSpace() && details.Add(new XElement("genre", string.Join(" / ", movie.Genres)));
ValidCertification.Contains(series.Certification.ToUpperInvariant())) details.Add(new XElement("description", movie.Overview));
{ details.Add(new XElement("length", movie.Runtime));
details.Add(new XElement("mpaa", series.Certification.ToUpperInvariant()));
}
else
{
details.Add(new XElement("mpaa", "UNRATED"));
}
doc.Add(details); doc.Add(details);
doc.Save(xw); doc.Save(xw);
@ -173,111 +126,39 @@ public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile ep
xmlResult += doc.ToString(); xmlResult += doc.ToString();
xmlResult += Environment.NewLine; xmlResult += Environment.NewLine;
} }
}
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
return new MetadataFileResult(GetMovieFileMetadataFilename(movieFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{ {
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); if (!Settings.MovieImages)
{
return new List<ImageFileResult>();
}
var image = movie.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? movie.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); _logger.Trace("Failed to find suitable Movie image for movie {0}.", movie.Title);
return null; return null;
} }
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
var destination = Path.GetFileName(series.Path) + Path.GetExtension(source); var destination = Path.GetFileName(movie.Path) + Path.GetExtension(source);
return new List<ImageFileResult>{ new ImageFileResult(destination, source) }; return new List<ImageFileResult> { new ImageFileResult(destination, source) };
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) private string GetMovieFileMetadataFilename(string movieFilePath)
{ {
var seasonFolders = GetSeasonFolders(series); return Path.ChangeExtension(movieFilePath, "xml");
string seasonFolder;
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
{
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
return new List<ImageFileResult>();
}
//Roksbox only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
return new List<ImageFileResult>();
}
var filename = Path.GetFileName(seasonFolder) + ".jpg";
var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename));
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) private string GetMovieFileImageFilename(string movieFilePath)
{ {
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); return Path.ChangeExtension(movieFilePath, "jpg");
if (screenshot == null)
{
_logger.Trace("Episode screenshot not available");
return new List<ImageFileResult>();
}
return new List<ImageFileResult> {new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)};
}
private string GetEpisodeMetadataFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "xml");
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "jpg");
}
private Dictionary<int, string> GetSeasonFolders(Series series)
{
var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in _diskProvider.GetDirectories(series.Path))
{
var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
if (seasonMatch.Success)
{
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
seasonFolderMap[0] = folder;
}
else
{
int matchedSeason;
if (int.TryParse(seasonNumber, out matchedSeason))
{
seasonFolderMap[matchedSeason] = folder;
}
else
{
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
}
}
}
else
{
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
}
}
return seasonFolderMap;
} }
} }
} }

View File

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -18,23 +18,15 @@ public class RoksboxMetadataSettings : IProviderConfig
public RoksboxMetadataSettings() public RoksboxMetadataSettings()
{ {
EpisodeMetadata = true; MovieMetadata = true;
SeriesImages = true; MovieImages = true;
SeasonImages = true;
EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Movie Metadata", Type = FieldType.Checkbox)]
public bool EpisodeMetadata { get; set; } public bool MovieMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "Movie Images", Type = FieldType.Checkbox)]
public bool SeriesImages { get; set; } public bool MovieImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
public bool SeasonImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
public bool EpisodeImages { get; set; }
public bool IsValid => true; public bool IsValid => true;

View File

@ -31,30 +31,28 @@ public WdtvMetadata(IMapCoversToLocal mediaCoverService,
_logger = logger; _logger = logger;
} }
private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override string Name => "WDTV"; public override string Name => "WDTV";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.MovieImage)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetMovieFileImageFilename(movieFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.MovieMetadata)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetMovieFileMetadataFilename(movieFilePath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown movie file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(movie.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Movie movie, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
@ -62,75 +60,47 @@ public override MetadataFile FindMetadataFile(Series series, string path)
var metadata = new MetadataFile var metadata = new MetadataFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
Consumer = GetType().Name, Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path) RelativePath = movie.Path.GetRelativePath(path)
}; };
//Series and season images are both named folder.jpg, only season ones sit in season folders
if (Path.GetFileName(filename).Equals("folder.jpg", StringComparison.InvariantCultureIgnoreCase)) if (Path.GetFileName(filename).Equals("folder.jpg", StringComparison.InvariantCultureIgnoreCase))
{ {
var parentdir = Directory.GetParent(path); metadata.Type = MetadataType.MovieImage;
var seasonMatch = SeasonImagesRegex.Match(parentdir.Name);
if (seasonMatch.Success)
{
metadata.Type = MetadataType.SeasonImage;
if (seasonMatch.Groups["specials"].Success)
{
metadata.SeasonNumber = 0;
}
else
{
metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value);
}
return metadata;
}
metadata.Type = MetadataType.SeriesImage;
return metadata; return metadata;
} }
var parseResult = Parser.Parser.ParseTitle(filename); var parseResult = Parser.Parser.ParseMovieTitle(filename, false);
if (parseResult != null && if (parseResult != null)
!parseResult.FullSeason)
{ {
switch (Path.GetExtension(filename).ToLowerInvariant()) switch (Path.GetExtension(filename).ToLowerInvariant())
{ {
case ".xml": case ".xml":
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.MovieMetadata;
return metadata; return metadata;
case ".metathumb": case ".metathumb":
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.MovieImage;
return metadata; return metadata;
} }
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile)
{ {
//Series metadata is not supported if (!Settings.MovieMetadata)
return null;
}
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
{
if (!Settings.EpisodeMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); _logger.Debug("Generating Movie File Metadata for: {0}", Path.Combine(movie.Path, movieFile.RelativePath));
var xmlResult = string.Empty; var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
@ -141,21 +111,10 @@ public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile ep
var doc = new XDocument(); var doc = new XDocument();
var details = new XElement("details"); var details = new XElement("details");
details.Add(new XElement("id", series.Id)); details.Add(new XElement("id", movie.Id));
details.Add(new XElement("title", string.Format("{0} - {1}x{2:00} - {3}", series.Title, episode.SeasonNumber, episode.EpisodeNumber, episode.Title))); details.Add(new XElement("title", movie.Title));
details.Add(new XElement("series_name", series.Title)); details.Add(new XElement("genre", string.Join(" / ", movie.Genres)));
details.Add(new XElement("episode_name", episode.Title)); details.Add(new XElement("overview", movie.Overview));
details.Add(new XElement("season_number", episode.SeasonNumber.ToString("00")));
details.Add(new XElement("episode_number", episode.EpisodeNumber.ToString("00")));
details.Add(new XElement("firstaired", episode.AirDate));
details.Add(new XElement("genre", string.Join(" / ", series.Genres)));
details.Add(new XElement("actor", string.Join(" / ", series.Actors.ConvertAll(c => c.Name + " - " + c.Character))));
details.Add(new XElement("overview", episode.Overview));
//Todo: get guest stars, writer and director
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
doc.Add(details); doc.Add(details);
doc.Save(xw); doc.Save(xw);
@ -163,29 +122,29 @@ public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile ep
xmlResult += doc.ToString(); xmlResult += doc.ToString();
xmlResult += Environment.NewLine; xmlResult += Environment.NewLine;
} }
}
var filename = GetEpisodeMetadataFilename(episodeFile.RelativePath);
var filename = GetMovieFileMetadataFilename(movieFile.RelativePath);
return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray())); return new MetadataFileResult(filename, xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeriesImages(Series series) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile moviefile)
{ {
if (!Settings.SeriesImages) if (!Settings.MovieImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
//Because we only support one image, attempt to get the Poster type, then if that fails grab the first //Because we only support one image, attempt to get the Poster type, then if that fails grab the first
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault(); var image = movie.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? movie.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title); _logger.Trace("Failed to find suitable Movie image for movie {0}.", movie.Title);
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
var destination = "folder" + Path.GetExtension(source); var destination = "folder" + Path.GetExtension(source);
return new List<ImageFileResult> return new List<ImageFileResult>
@ -194,102 +153,14 @@ public override List<ImageFileResult> SeriesImages(Series series)
}; };
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) private string GetMovieFileMetadataFilename(string movieFilePath)
{ {
if (!Settings.SeasonImages) return Path.ChangeExtension(movieFilePath, "xml");
{
return new List<ImageFileResult>();
}
var seasonFolders = GetSeasonFolders(series);
//Work out the path to this season - if we don't have a matching path then skip this season.
string seasonFolder;
if (!seasonFolders.TryGetValue(season.SeasonNumber, out seasonFolder))
{
_logger.Trace("Failed to find season folder for series {0}, season {1}.", series.Title, season.SeasonNumber);
return new List<ImageFileResult>();
}
//WDTV only supports one season image, so first of all try for poster otherwise just use whatever is first in the collection
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber);
return new List<ImageFileResult>();
}
var path = Path.Combine(seasonFolder, "folder.jpg");
return new List<ImageFileResult>{ new ImageFileResult(path, image.Url) };
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) private string GetMovieFileImageFilename(string movieFilePath)
{ {
if (!Settings.EpisodeImages) return Path.ChangeExtension(movieFilePath, "metathumb");
{
return new List<ImageFileResult>();
}
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Trace("Episode screenshot not available");
return new List<ImageFileResult>();
}
return new List<ImageFileResult>{ new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url) };
}
private string GetEpisodeMetadataFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "xml");
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "metathumb");
}
private Dictionary<int, string> GetSeasonFolders(Series series)
{
var seasonFolderMap = new Dictionary<int, string>();
foreach (var folder in _diskProvider.GetDirectories(series.Path))
{
var directoryinfo = new DirectoryInfo(folder);
var seasonMatch = SeasonImagesRegex.Match(directoryinfo.Name);
if (seasonMatch.Success)
{
var seasonNumber = seasonMatch.Groups["season"].Value;
if (seasonNumber.Contains("specials"))
{
seasonFolderMap[0] = folder;
}
else
{
int matchedSeason;
if (int.TryParse(seasonNumber, out matchedSeason))
{
seasonFolderMap[matchedSeason] = folder;
}
else
{
_logger.Debug("Failed to parse season number from {0} for series {1}.", folder, series.Title);
}
}
}
else
{
_logger.Debug("Rejecting folder {0} for series {1}.", Path.GetDirectoryName(folder), series.Title);
}
}
return seasonFolderMap;
} }
} }
} }

View File

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -18,23 +18,15 @@ public class WdtvMetadataSettings : IProviderConfig
public WdtvMetadataSettings() public WdtvMetadataSettings()
{ {
EpisodeMetadata = true; MovieMetadata = true;
SeriesImages = true; MovieImages = true;
SeasonImages = true;
EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Movie Metadata", Type = FieldType.Checkbox)]
public bool EpisodeMetadata { get; set; } public bool MovieMetadata { get; set; }
[FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "Movie Images", Type = FieldType.Checkbox)]
public bool SeriesImages { get; set; } public bool MovieImages { get; set; }
[FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)]
public bool SeasonImages { get; set; }
[FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)]
public bool EpisodeImages { get; set; }
public bool IsValid => true; public bool IsValid => true;

View File

@ -27,357 +27,227 @@ public XbmcMetadata(IMapCoversToLocal mediaCoverService,
_logger = logger; _logger = logger;
} }
private static readonly Regex SeriesImagesRegex = new Regex(@"^(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MovieImagesRegex = new Regex(@"^(?<type>poster|banner|fanart|clearart|disc|landscape|logo)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SeasonImagesRegex = new Regex(@"^season(?<season>\d{2,}|-all|-specials)-(?<type>poster|banner|fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex MovieFileImageRegex = new Regex(@"(?<type>-thumb|-poster|-banner|-fanart)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex EpisodeImageRegex = new Regex(@"-thumb\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public override string Name => "Kodi (XBMC) / Emby"; public override string Name => "Kodi (XBMC) / Emby";
public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public override string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile)
{ {
var episodeFilePath = Path.Combine(series.Path, episodeFile.RelativePath); var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath);
var metadataPath = Path.Combine(movie.Path, metadataFile.RelativePath);
if (metadataFile.Type == MetadataType.EpisodeImage) if (metadataFile.Type == MetadataType.MovieMetadata)
{ {
return GetEpisodeImageFilename(episodeFilePath); return GetMovieMetadataFilename(movieFilePath);
} }
if (metadataFile.Type == MetadataType.EpisodeMetadata) if (metadataFile.Type == MetadataType.MovieImage)
{ {
return GetEpisodeMetadataFilename(episodeFilePath); return GetMovieImageFilename(movieFilePath, metadataPath);
} }
_logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); _logger.Debug("Unknown movie file metadata: {0}", metadataFile.RelativePath);
return Path.Combine(series.Path, metadataFile.RelativePath); return Path.Combine(movie.Path, metadataFile.RelativePath);
} }
public override MetadataFile FindMetadataFile(Series series, string path) public override MetadataFile FindMetadataFile(Movie movie, string path)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);
if (filename == null) return null; if (filename == null) return null;
var metadata = new MetadataFile var metadata = new MetadataFile
{
SeriesId = series.Id,
Consumer = GetType().Name,
RelativePath = series.Path.GetRelativePath(path)
};
if (SeriesImagesRegex.IsMatch(filename))
{ {
metadata.Type = MetadataType.SeriesImage; MovieId = movie.Id,
Consumer = GetType().Name,
RelativePath = movie.Path.GetRelativePath(path)
};
if (MovieImagesRegex.IsMatch(filename))
{
metadata.Type = MetadataType.MovieImage;
return metadata; return metadata;
} }
var seasonMatch = SeasonImagesRegex.Match(filename); if (MovieFileImageRegex.IsMatch(filename))
if (seasonMatch.Success)
{ {
metadata.Type = MetadataType.SeasonImage; metadata.Type = MetadataType.MovieImage;
var seasonNumberMatch = seasonMatch.Groups["season"].Value;
int seasonNumber;
if (seasonNumberMatch.Contains("specials"))
{
metadata.SeasonNumber = 0;
}
else if (int.TryParse(seasonNumberMatch, out seasonNumber))
{
metadata.SeasonNumber = seasonNumber;
}
else
{
return null;
}
return metadata; return metadata;
} }
if (EpisodeImageRegex.IsMatch(filename)) if (filename.Equals("movie.nfo", StringComparison.OrdinalIgnoreCase))
{ {
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.MovieMetadata;
return metadata; return metadata;
} }
if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase)) var parseResult = Parser.Parser.ParseMovieTitle(filename, false);
{
metadata.Type = MetadataType.SeriesMetadata;
return metadata;
}
var parseResult = Parser.Parser.ParseTitle(filename);
if (parseResult != null && if (parseResult != null &&
!parseResult.FullSeason && Path.GetExtension(filename).Equals(".nfo", StringComparison.OrdinalIgnoreCase))
Path.GetExtension(filename) == ".nfo")
{ {
metadata.Type = MetadataType.EpisodeMetadata; metadata.Type = MetadataType.MovieMetadata;
return metadata; return metadata;
} }
return null; return null;
} }
public override MetadataFileResult SeriesMetadata(Series series) public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile)
{ {
if (!Settings.SeriesMetadata) if (!Settings.MovieMetadata)
{ {
return null; return null;
} }
_logger.Debug("Generating tvshow.nfo for: {0}", series.Title); _logger.Debug("Generating Movie Metadata for: {0}", Path.Combine(movie.Path, movieFile.RelativePath));
var xmlResult = string.Empty;
var sb = new StringBuilder(); var sb = new StringBuilder();
var xws = new XmlWriterSettings(); var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true; xws.OmitXmlDeclaration = true;
xws.Indent = false; xws.Indent = false;
var episodeGuideUrl = string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/en.zip", series.TvdbId);
using (var xw = XmlWriter.Create(sb, xws)) using (var xw = XmlWriter.Create(sb, xws))
{ {
var tvShow = new XElement("tvshow"); var doc = new XDocument();
var image = movie.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
tvShow.Add(new XElement("title", series.Title)); var details = new XElement("movie");
if (series.Ratings != null && series.Ratings.Votes > 0) details.Add(new XElement("title", movie.Title));
if (movie.Ratings != null && movie.Ratings.Votes > 0)
{ {
tvShow.Add(new XElement("rating", series.Ratings.Value)); details.Add(new XElement("rating", movie.Ratings.Value));
} }
tvShow.Add(new XElement("plot", series.Overview)); details.Add(new XElement("plot", movie.Overview));
tvShow.Add(new XElement("episodeguide", new XElement("url", episodeGuideUrl))); details.Add(new XElement("id", movie.ImdbId));
tvShow.Add(new XElement("episodeguideurl", episodeGuideUrl)); details.Add(new XElement("year", movie.Year));
tvShow.Add(new XElement("mpaa", series.Certification));
tvShow.Add(new XElement("id", series.TvdbId));
foreach (var genre in series.Genres) if (movie.InCinemas.HasValue)
{ {
tvShow.Add(new XElement("genre", genre)); details.Add(new XElement("premiered", movie.InCinemas.Value.ToString()));
} }
if (series.FirstAired.HasValue) foreach (var genre in movie.Genres)
{ {
tvShow.Add(new XElement("premiered", series.FirstAired.Value.ToString("yyyy-MM-dd"))); details.Add(new XElement("genre", genre));
} }
tvShow.Add(new XElement("studio", series.Network)); details.Add(new XElement("studio", movie.Studio));
foreach (var actor in series.Actors) if (image == null)
{ {
var xmlActor = new XElement("actor", details.Add(new XElement("thumb"));
new XElement("name", actor.Name), }
new XElement("role", actor.Character));
if (actor.Images.Any()) else
{
details.Add(new XElement("thumb", image.Url));
}
details.Add(new XElement("watched", "false"));
if (movieFile.MediaInfo != null)
{
var fileInfo = new XElement("fileinfo");
var streamDetails = new XElement("streamdetails");
var video = new XElement("video");
video.Add(new XElement("aspect", (float)movieFile.MediaInfo.Width / (float)movieFile.MediaInfo.Height));
video.Add(new XElement("bitrate", movieFile.MediaInfo.VideoBitrate));
video.Add(new XElement("codec", movieFile.MediaInfo.VideoCodec));
video.Add(new XElement("framerate", movieFile.MediaInfo.VideoFps));
video.Add(new XElement("height", movieFile.MediaInfo.Height));
video.Add(new XElement("scantype", movieFile.MediaInfo.ScanType));
video.Add(new XElement("width", movieFile.MediaInfo.Width));
if (movieFile.MediaInfo.RunTime != null)
{ {
xmlActor.Add(new XElement("thumb", actor.Images.First().Url)); video.Add(new XElement("duration", movieFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", movieFile.MediaInfo.RunTime.TotalSeconds));
} }
tvShow.Add(xmlActor); streamDetails.Add(video);
var audio = new XElement("audio");
audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", movieFile.MediaInfo.AudioChannels));
audio.Add(new XElement("codec", GetAudioCodec(movieFile.MediaInfo.AudioFormat)));
audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Length > 0)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", movieFile.MediaInfo.Subtitles));
streamDetails.Add(subtitle);
}
fileInfo.Add(streamDetails);
details.Add(fileInfo);
} }
var doc = new XDocument(tvShow); doc.Add(details);
doc.Save(xw); doc.Save(xw);
_logger.Debug("Saving tvshow.nfo for {0}", series.Title); xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
return new MetadataFileResult("tvshow.nfo", doc.ToString());
} }
var metadataFileName = GetMovieMetadataFilename(movieFile.RelativePath);
if (Settings.UseMovieNfo)
{
metadataFileName = "movie.nfo";
}
return new MetadataFileResult(metadataFileName, xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{ {
if (!Settings.EpisodeMetadata) if (!Settings.MovieImages)
{
return null;
}
_logger.Debug("Generating Episode Metadata for: {0}", Path.Combine(series.Path, episodeFile.RelativePath));
var xmlResult = string.Empty;
foreach (var episode in episodeFile.Episodes.Value)
{
var sb = new StringBuilder();
var xws = new XmlWriterSettings();
xws.OmitXmlDeclaration = true;
xws.Indent = false;
using (var xw = XmlWriter.Create(sb, xws))
{
var doc = new XDocument();
var image = episode.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
var details = new XElement("episodedetails");
details.Add(new XElement("title", episode.Title));
details.Add(new XElement("season", episode.SeasonNumber));
details.Add(new XElement("episode", episode.EpisodeNumber));
details.Add(new XElement("aired", episode.AirDate));
details.Add(new XElement("plot", episode.Overview));
//If trakt ever gets airs before information for specials we should add set it
details.Add(new XElement("displayseason"));
details.Add(new XElement("displayepisode"));
if (image == null)
{
details.Add(new XElement("thumb"));
}
else
{
details.Add(new XElement("thumb", image.Url));
}
details.Add(new XElement("watched", "false"));
if (episode.Ratings != null && episode.Ratings.Votes > 0)
{
details.Add(new XElement("rating", episode.Ratings.Value));
}
if (episodeFile.MediaInfo != null)
{
var fileInfo = new XElement("fileinfo");
var streamDetails = new XElement("streamdetails");
var video = new XElement("video");
video.Add(new XElement("aspect", (float) episodeFile.MediaInfo.Width / (float) episodeFile.MediaInfo.Height));
video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate));
video.Add(new XElement("codec", episodeFile.MediaInfo.VideoCodec));
video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps));
video.Add(new XElement("height", episodeFile.MediaInfo.Height));
video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType));
video.Add(new XElement("width", episodeFile.MediaInfo.Height));
if (episodeFile.MediaInfo.RunTime != null)
{
video.Add(new XElement("duration", episodeFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", episodeFile.MediaInfo.RunTime.TotalSeconds));
}
streamDetails.Add(video);
var audio = new XElement("audio");
audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", episodeFile.MediaInfo.AudioChannels));
audio.Add(new XElement("codec", GetAudioCodec(episodeFile.MediaInfo.AudioFormat)));
audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Length > 0)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles));
streamDetails.Add(subtitle);
}
fileInfo.Add(streamDetails);
details.Add(fileInfo);
}
//Todo: get guest stars, writer and director
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault()));
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
doc.Add(details);
doc.Save(xw);
xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
}
}
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
}
public override List<ImageFileResult> SeriesImages(Series series)
{
if (!Settings.SeriesImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
return ProcessSeriesImages(series).ToList(); return ProcessMovieImages(movie).ToList();
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) private IEnumerable<ImageFileResult> ProcessMovieImages(Movie movie)
{ {
if (!Settings.SeasonImages) foreach (var image in movie.Images)
{ {
return new List<ImageFileResult>(); var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
} var destination = Path.ChangeExtension(movie.MovieFile.RelativePath,"").TrimEnd(".") + "-" + image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
return ProcessSeasonImages(series, season).ToList();
}
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
{
if (!Settings.EpisodeImages)
{
return new List<ImageFileResult>();
}
try
{
var screenshot = episodeFile.Episodes.Value.First().Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot);
if (screenshot == null)
{
_logger.Debug("Episode screenshot not available");
return new List<ImageFileResult>();
}
return new List<ImageFileResult>
{
new ImageFileResult(GetEpisodeImageFilename(episodeFile.RelativePath), screenshot.Url)
};
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to process episode image for file: " + Path.Combine(series.Path, episodeFile.RelativePath));
return new List<ImageFileResult>();
}
}
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series)
{
foreach (var image in series.Images)
{
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
yield return new ImageFileResult(destination, source); yield return new ImageFileResult(destination, source);
} }
} }
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) private string GetMovieMetadataFilename(string movieFilePath)
{ {
foreach (var image in season.Images) return Path.ChangeExtension(movieFilePath, "nfo");
}
private string GetMovieImageFilename(string movieFilePath, string existingImageName)
{
var fileExtention = Path.GetExtension(existingImageName);
var match = MovieFileImageRegex.Matches(existingImageName);
if (match.Count > 0)
{ {
var filename = string.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower()); var imageType = match[0].Groups["type"].Value;
return Parser.Parser.RemoveFileExtension(movieFilePath) + imageType + fileExtention;
if (season.SeasonNumber == 0)
{
filename = string.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower());
}
yield return new ImageFileResult(filename, image.Url);
} }
}
private string GetEpisodeMetadataFilename(string episodeFilePath) return existingImageName;
{
return Path.ChangeExtension(episodeFilePath, "nfo");
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return Path.ChangeExtension(episodeFilePath, "").Trim('.') + "-thumb.jpg";
} }
private string GetAudioCodec(string audioCodec) private string GetAudioCodec(string audioCodec)

View File

@ -1,4 +1,4 @@
using FluentValidation; using FluentValidation;
using NzbDrone.Core.Annotations; using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation; using NzbDrone.Core.Validation;
@ -18,28 +18,20 @@ public class XbmcMetadataSettings : IProviderConfig
public XbmcMetadataSettings() public XbmcMetadataSettings()
{ {
SeriesMetadata = true; MovieMetadata = true;
EpisodeMetadata = true; MovieImages = true;
SeriesImages = true; UseMovieNfo = false;
SeasonImages = true;
EpisodeImages = true;
} }
[FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] [FieldDefinition(0, Label = "Movie Metadata", Type = FieldType.Checkbox)]
public bool SeriesMetadata { get; set; } public bool MovieMetadata { get; set; }
[FieldDefinition(1, Label = "Episode Metadata", Type = FieldType.Checkbox)] [FieldDefinition(1, Label = "Movie Images", Type = FieldType.Checkbox)]
public bool EpisodeMetadata { get; set; } public bool MovieImages { get; set; }
[FieldDefinition(2, Label = "Series Images", Type = FieldType.Checkbox)] [FieldDefinition(2, Label = "Use Movie.nfo", Type = FieldType.Checkbox, HelpText = "Radarr will write metadata to movie.nfo instead of the default <movie-filename>.nfo")]
public bool SeriesImages { get; set; } public bool UseMovieNfo { get; set; }
[FieldDefinition(3, Label = "Season Images", Type = FieldType.Checkbox)]
public bool SeasonImages { get; set; }
[FieldDefinition(4, Label = "Episode Images", Type = FieldType.Checkbox)]
public bool EpisodeImages { get; set; }
public bool IsValid => true; public bool IsValid => true;
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -32,12 +32,12 @@ public ExistingMetadataImporter(IExtraFileService<MetadataFile> metadataFileServ
public override int Order => 0; public override int Order => 0;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Movie movie, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing metadata in {0}", series.Path); _logger.Debug("Looking for existing metadata in {0}", movie.Path);
var metadataFiles = new List<MetadataFile>(); var metadataFiles = new List<MetadataFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(movie, filesOnDisk, importedFiles);
foreach (var possibleMetadataFile in filterResult.FilesOnDisk) foreach (var possibleMetadataFile in filterResult.FilesOnDisk)
{ {
@ -50,38 +50,31 @@ public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string>
foreach (var consumer in _consumers) foreach (var consumer in _consumers)
{ {
var metadata = consumer.FindMetadataFile(series, possibleMetadataFile); var metadata = consumer.FindMetadataFile(movie, possibleMetadataFile);
if (metadata == null) if (metadata == null)
{ {
continue; continue;
} }
if (metadata.Type == MetadataType.EpisodeImage || if (metadata.Type == MetadataType.MovieImage ||
metadata.Type == MetadataType.EpisodeMetadata) metadata.Type == MetadataType.MovieMetadata)
{ {
var localEpisode = _parsingService.GetLocalEpisode(possibleMetadataFile, series); var localMovie = _parsingService.GetLocalMovie(possibleMetadataFile, movie);
if (localEpisode == null) if (localMovie == null)
{ {
_logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile); _logger.Debug("Unable to parse extra file: {0}", possibleMetadataFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localMovie.Movie == null)
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleMetadataFile); _logger.Debug("Cannot find related movie for: {0}", possibleMetadataFile);
continue; continue;
} }
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) metadata.MovieFileId = localMovie.Movie.MovieFileId;
{
_logger.Debug("Extra file: {0} does not match existing files.", possibleMetadataFile);
continue;
}
metadata.SeasonNumber = localEpisode.SeasonNumber;
metadata.EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId;
} }
metadata.Extension = Path.GetExtension(possibleMetadataFile); metadata.Extension = Path.GetExtension(possibleMetadataFile);

View File

@ -1,4 +1,4 @@
using System.IO; using System.IO;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -7,7 +7,7 @@ namespace NzbDrone.Core.Extras.Metadata.Files
{ {
public interface ICleanMetadataService public interface ICleanMetadataService
{ {
void Clean(Series series); void Clean(Movie movie);
} }
public class CleanExtraFileService : ICleanMetadataService public class CleanExtraFileService : ICleanMetadataService
@ -25,15 +25,15 @@ public CleanExtraFileService(IMetadataFileService metadataFileService,
_logger = logger; _logger = logger;
} }
public void Clean(Series series) public void Clean(Movie movie)
{ {
_logger.Debug("Cleaning missing metadata files for series: {0}", series.Title); _logger.Debug("Cleaning missing metadata files for movie: {0}", movie.Title);
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByMovie(movie.Id);
foreach (var metadataFile in metadataFiles) foreach (var metadataFile in metadataFiles)
{ {
if (!_diskProvider.FileExists(Path.Combine(series.Path, metadataFile.RelativePath))) if (!_diskProvider.FileExists(Path.Combine(movie.Path, metadataFile.RelativePath)))
{ {
_logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath); _logger.Debug("Deleting metadata file from database: {0}", metadataFile.RelativePath);
_metadataFileService.Delete(metadataFile.Id); _metadataFileService.Delete(metadataFile.Id);

View File

@ -1,4 +1,4 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -12,8 +12,8 @@ public interface IMetadataFileService : IExtraFileService<MetadataFile>
public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService public class MetadataFileService : ExtraFileService<MetadataFile>, IMetadataFileService
{ {
public MetadataFileService(IExtraFileRepository<MetadataFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public MetadataFileService(IExtraFileRepository<MetadataFile> repository, IMovieService movieService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, movieService, diskProvider, recycleBinProvider, logger)
{ {
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.Extras.Metadata.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@ -8,12 +8,9 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
public interface IMetadata : IProvider public interface IMetadata : IProvider
{ {
string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile); string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile);
MetadataFile FindMetadataFile(Series series, string path); MetadataFile FindMetadataFile(Movie movie, string path);
MetadataFileResult SeriesMetadata(Series series); MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile);
MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile);
List<ImageFileResult> SeriesImages(Series series);
List<ImageFileResult> SeasonImages(Series series, Season season);
List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using FluentValidation.Results; using FluentValidation.Results;
@ -29,22 +29,19 @@ public ValidationResult Test()
return new ValidationResult(); return new ValidationResult();
} }
public virtual string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) public virtual string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile)
{ {
var existingFilename = Path.Combine(series.Path, metadataFile.RelativePath); var existingFilename = Path.Combine(movie.Path, metadataFile.RelativePath);
var extension = Path.GetExtension(existingFilename).TrimStart('.'); var extension = Path.GetExtension(existingFilename).TrimStart('.');
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension); var newFileName = Path.ChangeExtension(Path.Combine(movie.Path, movieFile.RelativePath), extension);
return newFileName; return newFileName;
} }
public abstract MetadataFile FindMetadataFile(Series series, string path); public abstract MetadataFile FindMetadataFile(Movie movie, string path);
public abstract MetadataFileResult SeriesMetadata(Series series); public abstract MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile);
public abstract MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile); public abstract List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile);
public abstract List<ImageFileResult> SeriesImages(Series series);
public abstract List<ImageFileResult> SeasonImages(Series series, Season season);
public abstract List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile);
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; } public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -19,23 +19,23 @@ public class MetadataService : ExtraFileManager<MetadataFile>
{ {
private readonly IMetadataFactory _metadataFactory; private readonly IMetadataFactory _metadataFactory;
private readonly ICleanMetadataService _cleanMetadataService; private readonly ICleanMetadataService _cleanMetadataService;
private readonly IDiskTransferService _diskTransferService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IDiskTransferService _diskTransferService;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IMediaFileAttributeService _mediaFileAttributeService;
private readonly IMetadataFileService _metadataFileService; private readonly IMetadataFileService _metadataFileService;
private readonly Logger _logger; private readonly Logger _logger;
public MetadataService(IConfigService configService, public MetadataService(IConfigService configService,
IDiskProvider diskProvider,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
IMetadataFactory metadataFactory, IMetadataFactory metadataFactory,
ICleanMetadataService cleanMetadataService, ICleanMetadataService cleanMetadataService,
IDiskProvider diskProvider,
IHttpClient httpClient, IHttpClient httpClient,
IMediaFileAttributeService mediaFileAttributeService, IMediaFileAttributeService mediaFileAttributeService,
IMetadataFileService metadataFileService, IMetadataFileService metadataFileService,
Logger logger) Logger logger)
: base(configService, diskTransferService, metadataFileService) : base(configService, diskProvider, diskTransferService, logger)
{ {
_metadataFactory = metadataFactory; _metadataFactory = metadataFactory;
_cleanMetadataService = cleanMetadataService; _cleanMetadataService = cleanMetadataService;
@ -49,14 +49,14 @@ public MetadataService(IConfigService configService,
public override int Order => 0; public override int Order => 0;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterMovieScan(Movie movie, List<MovieFile> movieFiles)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByMovie(movie.Id);
_cleanMetadataService.Clean(series); _cleanMetadataService.Clean(movie);
if (!_diskProvider.FolderExists(series.Path)) if (!_diskProvider.FolderExists(movie.Path))
{ {
_logger.Info("Series folder does not exist, skipping metadata creation"); _logger.Info("Movie folder does not exist, skipping metadata creation");
return Enumerable.Empty<MetadataFile>(); return Enumerable.Empty<MetadataFile>();
} }
@ -66,14 +66,10 @@ public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List
{ {
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles); var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles)); foreach (var episodeFile in movieFiles)
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
foreach (var episodeFile in episodeFiles)
{ {
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, consumerFiles)); files.AddIfNotNull(ProcessMovieMetadata(consumer, movie, episodeFile, consumerFiles));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, consumerFiles)); files.AddRange(ProcessMovieImages(consumer, movie, episodeFile, consumerFiles));
} }
} }
@ -82,15 +78,15 @@ public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List
return files; return files;
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile)
{ {
var files = new List<MetadataFile>(); var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled()) foreach (var consumer in _metadataFactory.Enabled())
{ {
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>())); files.AddIfNotNull(ProcessMovieMetadata(consumer, movie, movieFile, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>())); files.AddRange(ProcessMovieImages(consumer, movie, movieFile, new List<MetadataFile>()));
} }
_metadataFileService.Upsert(files); _metadataFileService.Upsert(files);
@ -98,41 +94,9 @@ public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, E
return files; return files;
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByMovie(movie.Id);
if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace())
{
return new List<MetadataFile>();
}
var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled())
{
var consumerFiles = GetMetadataFilesForConsumer(consumer, metadataFiles);
if (seriesFolder.IsNotNullOrWhiteSpace())
{
files.AddIfNotNull(ProcessSeriesMetadata(consumer, series, consumerFiles));
files.AddRange(ProcessSeriesImages(consumer, series, consumerFiles));
}
if (seasonFolder.IsNotNullOrWhiteSpace())
{
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
}
}
_metadataFileService.Upsert(files);
return files;
}
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<MetadataFile>(); var movedFiles = new List<MetadataFile>();
// TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it // TODO: Move EpisodeImage and EpisodeMetadata metadata files, instead of relying on consumers to do it
@ -140,26 +104,26 @@ public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<
foreach (var consumer in _metadataFactory.GetAvailableProviders()) foreach (var consumer in _metadataFactory.GetAvailableProviders())
{ {
foreach (var episodeFile in episodeFiles) foreach (var movieFile in movieFiles)
{ {
var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); var metadataFilesForConsumer = GetMetadataFilesForConsumer(consumer, metadataFiles).Where(m => m.MovieFileId == movieFile.Id).ToList();
foreach (var metadataFile in metadataFilesForConsumer) foreach (var metadataFile in metadataFilesForConsumer)
{ {
var newFileName = consumer.GetFilenameAfterMove(series, episodeFile, metadataFile); var newFileName = consumer.GetFilenameAfterMove(movie, movieFile, metadataFile);
var existingFileName = Path.Combine(series.Path, metadataFile.RelativePath); var existingFileName = Path.Combine(movie.Path, metadataFile.RelativePath);
if (newFileName.PathNotEquals(existingFileName)) if (newFileName.PathNotEquals(existingFileName))
{ {
try try
{ {
_diskProvider.MoveFile(existingFileName, newFileName); _diskProvider.MoveFile(existingFileName, newFileName);
metadataFile.RelativePath = series.Path.GetRelativePath(newFileName); metadataFile.RelativePath = movie.Path.GetRelativePath(newFileName);
movedFiles.Add(metadataFile); movedFiles.Add(metadataFile);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Warn(ex, "Unable to move metadata file: {0}", existingFileName); _logger.Warn(ex, "Unable to move metadata file after rename: {0}", existingFileName);
} }
} }
} }
@ -171,94 +135,50 @@ public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
{ {
return null; return null;
} }
private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> seriesMetadata) private List<MetadataFile> GetMetadataFilesForConsumer(IMetadata consumer, List<MetadataFile> movieMetadata)
{ {
return seriesMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList(); return movieMetadata.Where(c => c.Consumer == consumer.GetType().Name).ToList();
} }
private MetadataFile ProcessSeriesMetadata(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) private MetadataFile ProcessMovieMetadata(IMetadata consumer, Movie movie, MovieFile movieFile, List<MetadataFile> existingMetadataFiles)
{ {
var seriesMetadata = consumer.SeriesMetadata(series); var movieFileMetadata = consumer.MovieMetadata(movie, movieFile);
if (seriesMetadata == null) if (movieFileMetadata == null)
{ {
return null; return null;
} }
var hash = seriesMetadata.Contents.SHA256Hash(); var fullPath = Path.Combine(movie.Path, movieFileMetadata.RelativePath);
var metadata = GetMetadataFile(series, existingMetadataFiles, e => e.Type == MetadataType.SeriesMetadata) ?? var existingMetadata = GetMetadataFile(movie, existingMetadataFiles, c => c.Type == MetadataType.MovieMetadata &&
new MetadataFile c.MovieFileId == movieFile.Id);
{
SeriesId = series.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesMetadata
};
if (hash == metadata.Hash)
{
if (seriesMetadata.RelativePath != metadata.RelativePath)
{
metadata.RelativePath = seriesMetadata.RelativePath;
return metadata;
}
return null;
}
var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath);
_logger.Debug("Writing Series Metadata to: {0}", fullPath);
SaveMetadataFile(fullPath, seriesMetadata.Contents);
metadata.Hash = hash;
metadata.RelativePath = seriesMetadata.RelativePath;
metadata.Extension = Path.GetExtension(fullPath);
return metadata;
}
private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var episodeMetadata = consumer.EpisodeMetadata(series, episodeFile);
if (episodeMetadata == null)
{
return null;
}
var fullPath = Path.Combine(series.Path, episodeMetadata.RelativePath);
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeMetadata &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(movie.Path, existingMetadata.RelativePath);
if (fullPath.PathNotEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
existingMetadata.RelativePath = episodeMetadata.RelativePath; existingMetadata.RelativePath = movieFileMetadata.RelativePath;
} }
} }
var hash = episodeMetadata.Contents.SHA256Hash(); var hash = movieFileMetadata.Contents.SHA256Hash();
var metadata = existingMetadata ?? var metadata = existingMetadata ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
SeasonNumber = episodeFile.SeasonNumber, MovieFileId = movieFile.Id,
EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeMetadata, Type = MetadataType.MovieMetadata,
RelativePath = episodeMetadata.RelativePath, RelativePath = movieFileMetadata.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
@ -267,105 +187,34 @@ private MetadataFile ProcessEpisodeMetadata(IMetadata consumer, Series series, E
return null; return null;
} }
_logger.Debug("Writing Episode Metadata to: {0}", fullPath); _logger.Debug("Writing Movie File Metadata to: {0}", fullPath);
SaveMetadataFile(fullPath, episodeMetadata.Contents); SaveMetadataFile(fullPath, movieFileMetadata.Contents);
metadata.Hash = hash; metadata.Hash = hash;
return metadata; return metadata;
} }
private List<MetadataFile> ProcessSeriesImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles) private List<MetadataFile> ProcessMovieImages(IMetadata consumer, Movie movie, MovieFile movieFile, List<MetadataFile> existingMetadataFiles)
{ {
var result = new List<MetadataFile>(); var result = new List<MetadataFile>();
foreach (var image in consumer.SeriesImages(series)) foreach (var image in consumer.MovieImages(movie, movieFile))
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(movie.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath)) if (_diskProvider.FileExists(fullPath))
{ {
_logger.Debug("Series image already exists: {0}", fullPath); _logger.Debug("Movie image already exists: {0}", fullPath);
continue; continue;
} }
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage && var existingMetadata = GetMetadataFile(movie, existingMetadataFiles, c => c.Type == MetadataType.MovieImage &&
c.RelativePath == image.RelativePath) ?? c.RelativePath == image.RelativePath);
new MetadataFile
{
SeriesId = series.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesImage,
RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath)
};
DownloadImage(series, image);
result.Add(metadata);
}
return result;
}
private List<MetadataFile> ProcessSeasonImages(IMetadata consumer, Series series, List<MetadataFile> existingMetadataFiles)
{
var result = new List<MetadataFile>();
foreach (var season in series.Seasons)
{
foreach (var image in consumer.SeasonImages(series, season))
{
var fullPath = Path.Combine(series.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath))
{
_logger.Debug("Season image already exists: {0}", fullPath);
continue;
}
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeasonImage &&
c.SeasonNumber == season.SeasonNumber &&
c.RelativePath == image.RelativePath) ??
new MetadataFile
{
SeriesId = series.Id,
SeasonNumber = season.SeasonNumber,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeasonImage,
RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath)
};
DownloadImage(series, image);
result.Add(metadata);
}
}
return result;
}
private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series series, EpisodeFile episodeFile, List<MetadataFile> existingMetadataFiles)
{
var result = new List<MetadataFile>();
foreach (var image in consumer.EpisodeImages(series, episodeFile))
{
var fullPath = Path.Combine(series.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath))
{
_logger.Debug("Episode image already exists: {0}", fullPath);
continue;
}
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage &&
c.EpisodeFileId == episodeFile.Id);
if (existingMetadata != null) if (existingMetadata != null)
{ {
var existingFullPath = Path.Combine(series.Path, existingMetadata.RelativePath); var existingFullPath = Path.Combine(movie.Path, existingMetadata.RelativePath);
if (fullPath.PathNotEquals(existingFullPath)) if (fullPath.PathNotEquals(existingFullPath))
{ {
_diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move); _diskTransferService.TransferFile(existingFullPath, fullPath, TransferMode.Move);
@ -378,16 +227,15 @@ private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series serie
var metadata = existingMetadata ?? var metadata = existingMetadata ??
new MetadataFile new MetadataFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
SeasonNumber = episodeFile.SeasonNumber, MovieFileId = movieFile.Id,
EpisodeFileId = episodeFile.Id,
Consumer = consumer.GetType().Name, Consumer = consumer.GetType().Name,
Type = MetadataType.EpisodeImage, Type = MetadataType.MovieImage,
RelativePath = image.RelativePath, RelativePath = image.RelativePath,
Extension = Path.GetExtension(fullPath) Extension = Path.GetExtension(fullPath)
}; };
DownloadImage(series, image); DownloadImage(movie, image);
result.Add(metadata); result.Add(metadata);
} }
@ -395,9 +243,9 @@ private List<MetadataFile> ProcessEpisodeImages(IMetadata consumer, Series serie
return result; return result;
} }
private void DownloadImage(Series series, ImageFileResult image) private void DownloadImage(Movie movie, ImageFileResult image)
{ {
var fullPath = Path.Combine(series.Path, image.RelativePath); var fullPath = Path.Combine(movie.Path, image.RelativePath);
try try
{ {
@ -413,11 +261,11 @@ private void DownloadImage(Series series, ImageFileResult image)
} }
catch (WebException ex) catch (WebException ex)
{ {
_logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); _logger.Warn(ex, "Couldn't download image {0} for {1}. {2}", image.Url, movie, ex.Message);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, series, ex.Message); _logger.Error(ex, "Couldn't download image {0} for {1}. {2}", image.Url, movie, ex.Message);
} }
} }
@ -427,7 +275,7 @@ private void SaveMetadataFile(string path, string contents)
_mediaFileAttributeService.SetFilePermissions(path); _mediaFileAttributeService.SetFilePermissions(path);
} }
private MetadataFile GetMetadataFile(Series series, List<MetadataFile> existingMetadataFiles, Func<MetadataFile, bool> predicate) private MetadataFile GetMetadataFile(Movie movie, List<MetadataFile> existingMetadataFiles, Func<MetadataFile, bool> predicate)
{ {
var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList(); var matchingMetadataFiles = existingMetadataFiles.Where(predicate).ToList();
@ -439,7 +287,7 @@ private MetadataFile GetMetadataFile(Series series, List<MetadataFile> existingM
//Remove duplicate metadata files from DB and disk //Remove duplicate metadata files from DB and disk
foreach (var file in matchingMetadataFiles.Skip(1)) foreach (var file in matchingMetadataFiles.Skip(1))
{ {
var path = Path.Combine(series.Path, file.RelativePath); var path = Path.Combine(movie.Path, file.RelativePath);
_logger.Debug("Removing duplicate Metadata file: {0}", path); _logger.Debug("Removing duplicate Metadata file: {0}", path);

View File

@ -1,12 +1,9 @@
namespace NzbDrone.Core.Extras.Metadata namespace NzbDrone.Core.Extras.Metadata
{ {
public enum MetadataType public enum MetadataType
{ {
Unknown = 0, Unknown = 0,
SeriesMetadata = 1, MovieMetadata = 1,
EpisodeMetadata = 2, MovieImage = 2
SeriesImage = 3,
SeasonImage = 4,
EpisodeImage = 5
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -27,42 +27,43 @@ public ExistingOtherExtraImporter(IExtraFileService<OtherExtraFile> otherExtraFi
public override int Order => 2; public override int Order => 2;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Movie movie, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing extra files in {0}", series.Path); _logger.Debug("Looking for existing extra files in {0}", movie.Path);
var extraFiles = new List<OtherExtraFile>(); var extraFiles = new List<OtherExtraFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(movie, filesOnDisk, importedFiles);
foreach (var possibleExtraFile in filterResult.FilesOnDisk) foreach (var possibleExtraFile in filterResult.FilesOnDisk)
{ {
var localEpisode = _parsingService.GetLocalEpisode(possibleExtraFile, series); var extension = Path.GetExtension(possibleExtraFile);
if (localEpisode == null) if (extension.IsNullOrWhiteSpace())
{
_logger.Debug("No extension for file: {0}", possibleExtraFile);
continue;
}
var localMovie = _parsingService.GetLocalMovie(possibleExtraFile, movie);
if (localMovie == null)
{ {
_logger.Debug("Unable to parse extra file: {0}", possibleExtraFile); _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localMovie.Movie == null)
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile); _logger.Debug("Cannot find related movie for: {0}", possibleExtraFile);
continue;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile);
continue; continue;
} }
var extraFile = new OtherExtraFile var extraFile = new OtherExtraFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
SeasonNumber = localEpisode.SeasonNumber, MovieFileId = localMovie.Movie.MovieFileId,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, RelativePath = movie.Path.GetRelativePath(possibleExtraFile),
RelativePath = series.Path.GetRelativePath(possibleExtraFile), Extension = extension
Extension = Path.GetExtension(possibleExtraFile)
}; };
extraFiles.Add(extraFile); extraFiles.Add(extraFile);

View File

@ -1,4 +1,4 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -12,8 +12,8 @@ public interface IOtherExtraFileService : IExtraFileService<OtherExtraFile>
public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService public class OtherExtraFileService : ExtraFileService<OtherExtraFile>, IOtherExtraFileService
{ {
public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public OtherExtraFileService(IExtraFileRepository<OtherExtraFile> repository, IMovieService movieService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, movieService, diskProvider, recycleBinProvider, logger)
{ {
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -15,71 +15,41 @@ namespace NzbDrone.Core.Extras.Others
public class OtherExtraService : ExtraFileManager<OtherExtraFile> public class OtherExtraService : ExtraFileManager<OtherExtraFile>
{ {
private readonly IOtherExtraFileService _otherExtraFileService; private readonly IOtherExtraFileService _otherExtraFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
public OtherExtraService(IConfigService configService, public OtherExtraService(IConfigService configService,
IDiskProvider diskProvider,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
IOtherExtraFileService otherExtraFileService, IOtherExtraFileService otherExtraFileService,
IDiskProvider diskProvider,
Logger logger) Logger logger)
: base(configService, diskTransferService, otherExtraFileService) : base(configService, diskProvider, diskTransferService, logger)
{ {
_otherExtraFileService = otherExtraFileService; _otherExtraFileService = otherExtraFileService;
_diskProvider = diskProvider;
_logger = logger;
} }
public override int Order => 2; public override int Order => 2;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterMovieScan(Movie movie, List<MovieFile> movieFiles)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles)
{ {
return Enumerable.Empty<ExtraFile>(); var extraFiles = _otherExtraFileService.GetFilesByMovie(movie.Id);
}
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
// TODO: Remove
// We don't want to move files after rename yet.
return Enumerable.Empty<ExtraFile>();
var extraFiles = _otherExtraFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<OtherExtraFile>(); var movedFiles = new List<OtherExtraFile>();
foreach (var episodeFile in episodeFiles) foreach (var movieFile in movieFiles)
{ {
var extraFilesForEpisodeFile = extraFiles.Where(m => m.EpisodeFileId == episodeFile.Id).ToList(); var extraFilesForMovieFile = extraFiles.Where(m => m.MovieFileId == movieFile.Id).ToList();
foreach (var extraFile in extraFilesForEpisodeFile) foreach (var extraFile in extraFilesForMovieFile)
{ {
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); movedFiles.AddIfNotNull(MoveFile(movie, movieFile, extraFile));
var extension = Path.GetExtension(existingFileName).TrimStart('.');
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
movedFiles.Add(extraFile);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move extra file: {0}", existingFileName);
}
}
} }
} }
@ -88,15 +58,15 @@ public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
{ {
// If the extension is .nfo we need to change it to .nfo-orig // If the extension is .nfo we need to change it to .nfo-orig
if (Path.GetExtension(path).Equals(".nfo")) if (Path.GetExtension(path).Equals(".nfo", StringComparison.OrdinalIgnoreCase))
{ {
extension += "-orig"; extension += "-orig";
} }
var extraFile = ImportFile(series, episodeFile, path, extension, readOnly); var extraFile = ImportFile(movie, movieFile, path, readOnly, extension, null);
_otherExtraFileService.Upsert(extraFile); _otherExtraFileService.Upsert(extraFile);

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -27,12 +27,12 @@ public ExistingSubtitleImporter(IExtraFileService<SubtitleFile> subtitleFileServ
public override int Order => 1; public override int Order => 1;
public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string> filesOnDisk, List<string> importedFiles) public override IEnumerable<ExtraFile> ProcessFiles(Movie movie, List<string> filesOnDisk, List<string> importedFiles)
{ {
_logger.Debug("Looking for existing subtitle files in {0}", series.Path); _logger.Debug("Looking for existing subtitle files in {0}", movie.Path);
var subtitleFiles = new List<SubtitleFile>(); var subtitleFiles = new List<SubtitleFile>();
var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); var filterResult = FilterAndClean(movie, filesOnDisk, importedFiles);
foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) foreach (var possibleSubtitleFile in filterResult.FilesOnDisk)
{ {
@ -40,32 +40,25 @@ public override IEnumerable<ExtraFile> ProcessFiles(Series series, List<string>
if (SubtitleFileExtensions.Extensions.Contains(extension)) if (SubtitleFileExtensions.Extensions.Contains(extension))
{ {
var localEpisode = _parsingService.GetLocalEpisode(possibleSubtitleFile, series); var localMovie = _parsingService.GetLocalMovie(possibleSubtitleFile, movie);
if (localEpisode == null) if (localMovie == null)
{ {
_logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile); _logger.Debug("Unable to parse subtitle file: {0}", possibleSubtitleFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) if (localMovie.Movie == null)
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleSubtitleFile); _logger.Debug("Cannot find related movie for: {0}", possibleSubtitleFile);
continue;
}
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1)
{
_logger.Debug("Subtitle file: {0} does not match existing files.", possibleSubtitleFile);
continue; continue;
} }
var subtitleFile = new SubtitleFile var subtitleFile = new SubtitleFile
{ {
SeriesId = series.Id, MovieId = movie.Id,
SeasonNumber = localEpisode.SeasonNumber, MovieFileId = localMovie.Movie.MovieFileId,
EpisodeFileId = localEpisode.Episodes.First().EpisodeFileId, RelativePath = movie.Path.GetRelativePath(possibleSubtitleFile),
RelativePath = series.Path.GetRelativePath(possibleSubtitleFile),
Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile), Language = LanguageParser.ParseSubtitleLanguage(possibleSubtitleFile),
Extension = extension Extension = extension
}; };

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Extras.Subtitles namespace NzbDrone.Core.Extras.Subtitles
{ {
@ -8,7 +9,7 @@ public static class SubtitleFileExtensions
static SubtitleFileExtensions() static SubtitleFileExtensions()
{ {
_fileExtensions = new HashSet<string> _fileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
".aqt", ".aqt",
".ass", ".ass",

View File

@ -1,4 +1,4 @@
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras.Files; using NzbDrone.Core.Extras.Files;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -12,8 +12,8 @@ public interface ISubtitleFileService : IExtraFileService<SubtitleFile>
public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService public class SubtitleFileService : ExtraFileService<SubtitleFile>, ISubtitleFileService
{ {
public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, ISeriesService seriesService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger) public SubtitleFileService(IExtraFileRepository<SubtitleFile> repository, IMovieService movieService, IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, Logger logger)
: base(repository, seriesService, diskProvider, recycleBinProvider, logger) : base(repository, movieService, diskProvider, recycleBinProvider, logger)
{ {
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -17,83 +17,56 @@ namespace NzbDrone.Core.Extras.Subtitles
public class SubtitleService : ExtraFileManager<SubtitleFile> public class SubtitleService : ExtraFileManager<SubtitleFile>
{ {
private readonly ISubtitleFileService _subtitleFileService; private readonly ISubtitleFileService _subtitleFileService;
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger; private readonly Logger _logger;
public SubtitleService(IConfigService configService, public SubtitleService(IConfigService configService,
IDiskProvider diskProvider,
IDiskTransferService diskTransferService, IDiskTransferService diskTransferService,
ISubtitleFileService subtitleFileService, ISubtitleFileService subtitleFileService,
IDiskProvider diskProvider,
Logger logger) Logger logger)
: base(configService, diskTransferService, subtitleFileService) : base(configService, diskProvider, diskTransferService, logger)
{ {
_subtitleFileService = subtitleFileService; _subtitleFileService = subtitleFileService;
_diskProvider = diskProvider;
_logger = logger; _logger = logger;
} }
public override int Order => 1; public override int Order => 1;
public override IEnumerable<ExtraFile> CreateAfterSeriesScan(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> CreateAfterMovieScan(Movie movie, List<MovieFile> movieFiles)
{ {
return Enumerable.Empty<SubtitleFile>(); return Enumerable.Empty<SubtitleFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile) public override IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile)
{ {
return Enumerable.Empty<SubtitleFile>(); return Enumerable.Empty<SubtitleFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles)
{ {
return Enumerable.Empty<SubtitleFile>(); var subtitleFiles = _subtitleFileService.GetFilesByMovie(movie.Id);
}
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles)
{
// TODO: Remove
// We don't want to move files after rename yet.
return Enumerable.Empty<ExtraFile>();
var subtitleFiles = _subtitleFileService.GetFilesBySeries(series.Id);
var movedFiles = new List<SubtitleFile>(); var movedFiles = new List<SubtitleFile>();
foreach (var episodeFile in episodeFiles) foreach (var movieFile in movieFiles)
{ {
var groupedExtraFilesForEpisodeFile = subtitleFiles.Where(m => m.EpisodeFileId == episodeFile.Id) var groupedExtraFilesForMovieFile = subtitleFiles.Where(m => m.MovieFileId == movieFile.Id)
.GroupBy(s => s.Language + s.Extension).ToList(); .GroupBy(s => s.Language + s.Extension).ToList();
foreach (var group in groupedExtraFilesForEpisodeFile) foreach (var group in groupedExtraFilesForMovieFile)
{ {
var groupCount = group.Count(); var groupCount = group.Count();
var copy = 1; var copy = 1;
if (groupCount > 1) if (groupCount > 1)
{ {
_logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(series.Path, episodeFile.RelativePath)); _logger.Warn("Multiple subtitle files found with the same language and extension for {0}", Path.Combine(movie.Path, movieFile.RelativePath));
} }
foreach (var extraFile in group) foreach (var subtitleFile in group)
{ {
var existingFileName = Path.Combine(series.Path, extraFile.RelativePath); var suffix = GetSuffix(subtitleFile.Language, copy, groupCount > 1);
var extension = GetExtension(extraFile, existingFileName, copy, groupCount > 1); movedFiles.AddIfNotNull(MoveFile(movie, movieFile, subtitleFile, suffix));
var newFileName = Path.ChangeExtension(Path.Combine(series.Path, episodeFile.RelativePath), extension);
if (newFileName.PathNotEquals(existingFileName))
{
try
{
_diskProvider.MoveFile(existingFileName, newFileName);
extraFile.RelativePath = series.Path.GetRelativePath(newFileName);
movedFiles.Add(extraFile);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to move subtitle file: {0}", existingFileName);
}
}
copy++; copy++;
} }
@ -105,12 +78,14 @@ public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<
return movedFiles; return movedFiles;
} }
public override ExtraFile Import(Series series, EpisodeFile episodeFile, string path, string extension, bool readOnly) public override ExtraFile Import(Movie movie, MovieFile movieFile, string path, string extension, bool readOnly)
{ {
if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path))) if (SubtitleFileExtensions.Extensions.Contains(Path.GetExtension(path)))
{ {
var subtitleFile = ImportFile(series, episodeFile, path, extension, readOnly); var language = LanguageParser.ParseSubtitleLanguage(path);
subtitleFile.Language = LanguageParser.ParseSubtitleLanguage(path); var suffix = GetSuffix(language, 1, false);
var subtitleFile = ImportFile(movie, movieFile, path, readOnly, extension, suffix);
subtitleFile.Language = language;
_subtitleFileService.Upsert(subtitleFile); _subtitleFileService.Upsert(subtitleFile);
@ -120,26 +95,23 @@ public override ExtraFile Import(Series series, EpisodeFile episodeFile, string
return null; return null;
} }
private string GetExtension(SubtitleFile extraFile, string existingFileName, int copy, bool multipleCopies = false) private string GetSuffix(Language language, int copy, bool multipleCopies = false)
{ {
var fileExtension = Path.GetExtension(existingFileName); var suffixBuilder = new StringBuilder();
var extensionBuilder = new StringBuilder();
if (multipleCopies) if (multipleCopies)
{ {
extensionBuilder.Append(copy); suffixBuilder.Append(".");
extensionBuilder.Append("."); suffixBuilder.Append(copy);
} }
if (extraFile.Language != Language.Unknown) if (language != Language.Unknown)
{ {
extensionBuilder.Append(IsoLanguages.Get(extraFile.Language).TwoLetterCode); suffixBuilder.Append(".");
extensionBuilder.Append("."); suffixBuilder.Append(IsoLanguages.Get(language).TwoLetterCode);
} }
extensionBuilder.Append(fileExtension.TrimStart('.')); return suffixBuilder.ToString();
return extensionBuilder.ToString();
} }
} }
} }

View File

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
@ -13,12 +13,11 @@ public CleanupDuplicateMetadataFiles(IMainDatabase database)
public void Clean() public void Clean()
{ {
DeleteDuplicateSeriesMetadata(); DeleteDuplicateMovieMetadata();
DeleteDuplicateEpisodeMetadata(); DeleteDuplicateMovieFileMetadata();
DeleteDuplicateEpisodeImages();
} }
private void DeleteDuplicateSeriesMetadata() private void DeleteDuplicateMovieMetadata()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
@ -26,34 +25,21 @@ private void DeleteDuplicateSeriesMetadata()
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type = 1 WHERE Type = 1
GROUP BY SeriesId, Consumer GROUP BY MovieId, Consumer
HAVING COUNT(SeriesId) > 1 HAVING COUNT(MovieId) > 1
)"); )");
} }
private void DeleteDuplicateEpisodeMetadata() private void DeleteDuplicateMovieFileMetadata()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type = 2 WHERE Type = 1
GROUP BY EpisodeFileId, Consumer GROUP BY MovieFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1 HAVING COUNT(MovieFileId) > 1
)");
}
private void DeleteDuplicateEpisodeImages()
{
var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN (
SELECT Id FROM MetadataFiles
WHERE Type = 5
GROUP BY EpisodeFileId, Consumer
HAVING COUNT(EpisodeFileId) > 1
)"); )");
} }
} }

View File

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping.Housekeepers namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
@ -13,45 +13,45 @@ public CleanupOrphanedMetadataFiles(IMainDatabase database)
public void Clean() public void Clean()
{ {
DeleteOrphanedBySeries(); DeleteOrphanedByMovie();
DeleteOrphanedByEpisodeFile(); DeleteOrphanedByMovieFile();
DeleteWhereEpisodeFileIsZero(); DeleteWhereMovieFileIsZero();
} }
private void DeleteOrphanedBySeries() private void DeleteOrphanedByMovie()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN Series LEFT OUTER JOIN Movies
ON MetadataFiles.SeriesId = Series.Id ON MetadataFiles.MovieId = Movies.Id
WHERE Series.Id IS NULL)"); WHERE Movies.Id IS NULL)");
} }
private void DeleteOrphanedByEpisodeFile() private void DeleteOrphanedByMovieFile()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT MetadataFiles.Id FROM MetadataFiles SELECT MetadataFiles.Id FROM MetadataFiles
LEFT OUTER JOIN EpisodeFiles LEFT OUTER JOIN MovieFiles
ON MetadataFiles.EpisodeFileId = EpisodeFiles.Id ON MetadataFiles.MovieFileId = MovieFiles.Id
WHERE MetadataFiles.EpisodeFileId > 0 WHERE MetadataFiles.MovieFileId > 0
AND EpisodeFiles.Id IS NULL)"); AND MovieFiles.Id IS NULL)");
} }
private void DeleteWhereEpisodeFileIsZero() private void DeleteWhereMovieFileIsZero()
{ {
var mapper = _database.GetDataMapper(); var mapper = _database.GetDataMapper();
mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles mapper.ExecuteNonQuery(@"DELETE FROM MetadataFiles
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM MetadataFiles SELECT Id FROM MetadataFiles
WHERE Type IN (2, 5) WHERE Type IN (1, 2)
AND EpisodeFileId = 0)"); AND MovieFileId = 0)");
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -12,19 +12,19 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
public class DeleteBadMediaCovers : IHousekeepingTask public class DeleteBadMediaCovers : IHousekeepingTask
{ {
private readonly IMetadataFileService _metaFileService; private readonly IMetadataFileService _metaFileService;
private readonly ISeriesService _seriesService; private readonly IMovieService _movieService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly Logger _logger; private readonly Logger _logger;
public DeleteBadMediaCovers(IMetadataFileService metaFileService, public DeleteBadMediaCovers(IMetadataFileService metaFileService,
ISeriesService seriesService, IMovieService movieService,
IDiskProvider diskProvider, IDiskProvider diskProvider,
IConfigService configService, IConfigService configService,
Logger logger) Logger logger)
{ {
_metaFileService = metaFileService; _metaFileService = metaFileService;
_seriesService = seriesService; _movieService = movieService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_configService = configService; _configService = configService;
_logger = logger; _logger = logger;
@ -34,18 +34,18 @@ public void Clean()
{ {
if (!_configService.CleanupMetadataImages) return; if (!_configService.CleanupMetadataImages) return;
var series = _seriesService.GetAllSeries(); var movies = _movieService.GetAllMovies();
foreach (var show in series) foreach (var movie in movies)
{ {
var images = _metaFileService.GetFilesBySeries(show.Id) var images = _metaFileService.GetFilesByMovie(movie.Id)
.Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)); .Where(c => c.LastUpdated > new DateTime(2014, 12, 27) && c.RelativePath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase));
foreach (var image in images) foreach (var image in images)
{ {
try try
{ {
var path = Path.Combine(show.Path, image.RelativePath); var path = Path.Combine(movie.Path, image.RelativePath);
if (!IsValid(path)) if (!IsValid(path))
{ {
_logger.Debug("Deleting invalid image file " + path); _logger.Debug("Deleting invalid image file " + path);
@ -84,4 +84,4 @@ private bool IsValid(string path)
return !text.ToLowerInvariant().Contains("html"); return !text.ToLowerInvariant().Contains("html");
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -26,6 +26,7 @@ public interface IDiskScanService
string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetVideoFiles(string path, bool allDirectories = true);
string[] GetNonVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true);
List<string> FilterFiles(Series series, IEnumerable<string> files); List<string> FilterFiles(Series series, IEnumerable<string> files);
List<string> FilterFiles(Movie series, IEnumerable<string> files);
} }
public class DiskScanService : public class DiskScanService :
@ -213,7 +214,7 @@ public string[] GetVideoFiles(string path, bool allDirectories = true)
var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var filesOnDisk = _diskProvider.GetFiles(path, searchOption); var filesOnDisk = _diskProvider.GetFiles(path, searchOption);
var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower())) var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file)))
.ToList(); .ToList();
_logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path); _logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path);
@ -227,7 +228,7 @@ public string[] GetNonVideoFiles(string path, bool allDirectories = true)
var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var filesOnDisk = _diskProvider.GetFiles(path, searchOption); var filesOnDisk = _diskProvider.GetFiles(path, searchOption);
var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file).ToLower())) var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file)))
.ToList(); .ToList();
_logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path); _logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path);
@ -267,7 +268,7 @@ private void SetPermissions(string path)
_logger.Warn(ex, "Unable to apply permissions to: " + path); _logger.Warn(ex, "Unable to apply permissions to: " + path);
_logger.Debug(ex, ex.Message); _logger.Debug(ex, ex.Message);
} }
} }
public void Handle(SeriesUpdatedEvent message) public void Handle(SeriesUpdatedEvent message)
{ {

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
@ -106,7 +107,7 @@ public List<ImportResult> ProcessPath(string path, ImportMode importMode = Impor
public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie) public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie)
{ {
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f) == ".rar"); var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase));
foreach (var videoFile in videoFiles) foreach (var videoFile in videoFiles)
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -117,7 +117,7 @@ public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownloa
if (newDownload) if (newDownload)
{ {
_extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly); // _extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly);
} }
if (downloadClientItem != null) if (downloadClientItem != null)

View File

@ -120,7 +120,7 @@ public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownloa
if (newDownload) if (newDownload)
{ {
//_extraService.ImportExtraFiles(localMovie, episodeFile, copyOnly); TODO update for movie _extraService.ImportExtraFiles(localMovie, movieFile, copyOnly);
} }
if (downloadClientItem != null) if (downloadClientItem != null)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
@ -10,7 +11,7 @@ public static class MediaFileExtensions
static MediaFileExtensions() static MediaFileExtensions()
{ {
_fileExtensions = new Dictionary<string, Quality> _fileExtensions = new Dictionary<string, Quality>(StringComparer.OrdinalIgnoreCase)
{ {
//Unknown //Unknown
{ ".webm", Quality.Unknown }, { ".webm", Quality.Unknown },
@ -70,7 +71,7 @@ static MediaFileExtensions()
}; };
} }
public static HashSet<string> Extensions => new HashSet<string>(_fileExtensions.Keys); public static HashSet<string> Extensions => new HashSet<string>(_fileExtensions.Keys, StringComparer.OrdinalIgnoreCase);
public static Quality GetQualityForExtension(string extension) public static Quality GetQualityForExtension(string extension)
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.Linq; using System.Linq;
@ -128,6 +128,11 @@ private void RenameFiles(List<MovieFile> movieFiles, Movie movie, string oldMovi
{ {
_logger.Error(ex, "Failed to rename file: " + oldMovieFilePath); _logger.Error(ex, "Failed to rename file: " + oldMovieFilePath);
} }
if (renamed.Any())
{
_eventAggregator.PublishEvent(new MovieRenamedEvent(movie));
}
} }
} }

View File

@ -125,6 +125,7 @@
<Compile Include="Authentication\UserRepository.cs" /> <Compile Include="Authentication\UserRepository.cs" />
<Compile Include="Authentication\UserService.cs" /> <Compile Include="Authentication\UserService.cs" />
<Compile Include="Datastore\Migration\123_create_netimport_table.cs" /> <Compile Include="Datastore\Migration\123_create_netimport_table.cs" />
<Compile Include="Datastore\Migration\142_movie_extras.cs" />
<Compile Include="Datastore\Migration\140_add_alternative_titles_table.cs" /> <Compile Include="Datastore\Migration\140_add_alternative_titles_table.cs" />
<Compile Include="Datastore\Migration\141_fix_duplicate_alt_titles.cs" /> <Compile Include="Datastore\Migration\141_fix_duplicate_alt_titles.cs" />
<Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" /> <Compile Include="DecisionEngine\Specifications\RequiredIndexerFlagsSpecification.cs" />
@ -1378,11 +1379,7 @@
<Compile Include="Notifications\Telegram\TelegramError.cs" /> <Compile Include="Notifications\Telegram\TelegramError.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />
<ItemGroup> <ItemGroup />
<Folder Include="NetImport\ImportExclusions\" />
<Folder Include="NetImport\Radarr\" />
<Folder Include="MetadataSource\RadarrAPI\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup> <PropertyGroup>
<PostBuildEvent> <PostBuildEvent>

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -24,33 +24,30 @@ public class RefreshMovieService : IExecute<RefreshMovieCommand>
private readonly IProvideMovieInfo _movieInfo; private readonly IProvideMovieInfo _movieInfo;
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IAlternativeTitleService _titleService; private readonly IAlternativeTitleService _titleService;
private readonly IRefreshEpisodeService _refreshEpisodeService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager; private readonly IManageCommandQueue _commandQueueManager;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed; private readonly ICheckIfMovieShouldBeRefreshed _checkIfMovieShouldBeRefreshed;
private readonly IRadarrAPIClient _apiClient; private readonly IRadarrAPIClient _apiClient;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshMovieService(IProvideMovieInfo movieInfo, public RefreshMovieService(IProvideMovieInfo movieInfo,
IMovieService movieService, IMovieService movieService,
IAlternativeTitleService titleService, IAlternativeTitleService titleService,
IRefreshEpisodeService refreshEpisodeService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IDiskScanService diskScanService, IDiskScanService diskScanService,
IRadarrAPIClient apiClient, IRadarrAPIClient apiClient,
ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed, ICheckIfMovieShouldBeRefreshed checkIfMovieShouldBeRefreshed,
IManageCommandQueue commandQueue, IManageCommandQueue commandQueue,
Logger logger) Logger logger)
{ {
_movieInfo = movieInfo; _movieInfo = movieInfo;
_movieService = movieService; _movieService = movieService;
_titleService = titleService; _titleService = titleService;
_refreshEpisodeService = refreshEpisodeService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_apiClient = apiClient; _apiClient = apiClient;
_commandQueueManager = commandQueue; _commandQueueManager = commandQueue;
_diskScanService = diskScanService; _diskScanService = diskScanService;
_checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed; _checkIfMovieShouldBeRefreshed = checkIfMovieShouldBeRefreshed;
_logger = logger; _logger = logger;
@ -61,7 +58,7 @@ private void RefreshMovieInfo(Movie movie)
_logger.ProgressInfo("Updating Info for {0}", movie.Title); _logger.ProgressInfo("Updating Info for {0}", movie.Title);
Movie movieInfo; Movie movieInfo;
try try
{ {
movieInfo = _movieInfo.GetMovieInfo(movie.TmdbId, movie.Profile, movie.HasPreDBEntry); movieInfo = _movieInfo.GetMovieInfo(movie.TmdbId, movie.Profile, movie.HasPreDBEntry);
@ -99,7 +96,7 @@ private void RefreshMovieInfo(Movie movie)
movie.PhysicalRelease = movieInfo.PhysicalRelease; movie.PhysicalRelease = movieInfo.PhysicalRelease;
movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId; movie.YouTubeTrailerId = movieInfo.YouTubeTrailerId;
movie.Studio = movieInfo.Studio; movie.Studio = movieInfo.Studio;
movie.HasPreDBEntry = movieInfo.HasPreDBEntry; movie.HasPreDBEntry = movieInfo.HasPreDBEntry;
try try
{ {
@ -121,12 +118,12 @@ private void RefreshMovieInfo(Movie movie)
var mappingsTitles = mappings.Item1; var mappingsTitles = mappings.Item1;
movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie)); movie.AlternativeTitles.AddRange(_titleService.AddAltTitles(movieInfo.AlternativeTitles, movie));
_titleService.DeleteNotEnoughVotes(mappingsTitles); _titleService.DeleteNotEnoughVotes(mappingsTitles);
mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles, mappingsTitles = mappingsTitles.ExceptBy(t => t.CleanTitle, movie.AlternativeTitles,
t => t.CleanTitle, EqualityComparer<string>.Default).ToList(); t => t.CleanTitle, EqualityComparer<string>.Default).ToList();
mappingsTitles = mappingsTitles.Where(t => t.Votes > 3).ToList(); mappingsTitles = mappingsTitles.Where(t => t.Votes > 3).ToList();
@ -151,7 +148,7 @@ private void RefreshMovieInfo(Movie movie)
{ {
_logger.Info(ex, "Unable to communicate with Mappings Server."); _logger.Info(ex, "Unable to communicate with Mappings Server.");
} }
_movieService.UpdateMovie(movie); _movieService.UpdateMovie(movie);
@ -177,7 +174,7 @@ public void Execute(RefreshMovieCommand message)
if (message.MovieId.HasValue) if (message.MovieId.HasValue)
{ {
var movie = _movieService.GetMovie(message.MovieId.Value); var movie = _movieService.GetMovie(message.MovieId.Value);
RefreshMovieInfo(movie); RefreshMovieInfo(movie);
} }
else else
{ {
@ -202,7 +199,7 @@ public void Execute(RefreshMovieCommand message)
try try
{ {
_logger.Info("Skipping refresh of movie: {0}", movie.Title); _logger.Info("Skipping refresh of movie: {0}", movie.Title);
_commandQueueManager.Push(new RenameMovieFolderCommand(new List<int>{movie.Id})); _commandQueueManager.Push(new RenameMovieFolderCommand(new List<int> { movie.Id }));
_diskScanService.Scan(movie); _diskScanService.Scan(movie);
} }
catch (Exception e) catch (Exception e)

View File

@ -0,0 +1,14 @@
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'extra-extension-cell',
render : function() {
this.$el.empty();
var title = this.model.get('extension');
this.$el.html(title);
return this;
}
});

View File

@ -0,0 +1,19 @@
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'extra-type-cell',
render : function() {
this.$el.empty();
var title = this.model.get('type');
this.$el.html(this.toTitleCase(title));
return this;
},
toTitleCase : function(str)
{
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
});

View File

@ -11,7 +11,7 @@ var LoadingView = require('../../Shared/LoadingView');
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
var HistoryLayout = require('../History/MovieHistoryLayout'); var HistoryLayout = require('../History/MovieHistoryLayout');
var SearchLayout = require('../Search/MovieSearchLayout'); var SearchLayout = require('../Search/MovieSearchLayout');
var FilesLayout = require("../Files/FilesLayout"); var AllFilesLayout = require("../Files/AllFilesLayout");
var TitlesLayout = require("../Titles/TitlesLayout"); var TitlesLayout = require("../Titles/TitlesLayout");
require('backstrech'); require('backstrech');
require('../../Mixins/backbone.signalr.mixin'); require('../../Mixins/backbone.signalr.mixin');
@ -21,28 +21,27 @@ module.exports = Marionette.Layout.extend({
template : 'Movies/Details/MoviesDetailsTemplate', template : 'Movies/Details/MoviesDetailsTemplate',
regions : { regions : {
seasons : '#seasons', info : '#info',
info : '#info', search : '#movie-search',
search : '#movie-search', history : '#movie-history',
history : '#movie-history', filesTabs : '#movie-files-tabs',
files : "#movie-files", titles : "#movie-titles",
titles: "#movie-titles",
}, },
ui : { ui : {
header : '.x-header', header : '.x-header',
monitored : '.x-monitored', monitored : '.x-monitored',
edit : '.x-edit', edit : '.x-edit',
refresh : '.x-refresh', refresh : '.x-refresh',
rename : '.x-rename', rename : '.x-rename',
searchAuto : '.x-search', searchAuto : '.x-search',
poster : '.x-movie-poster', poster : '.x-movie-poster',
manualSearch : '.x-manual-search', manualSearch: '.x-manual-search',
history : '.x-movie-history', history : '.x-movie-history',
search : '.x-movie-search', search : '.x-movie-search',
files : ".x-movie-files", filesTabs : '.x-movie-files-tabs',
titles: ".x-movie-titles", titles : ".x-movie-titles",
}, },
events : { events : {
@ -53,10 +52,10 @@ module.exports = Marionette.Layout.extend({
'click .x-rename' : '_renameMovies', 'click .x-rename' : '_renameMovies',
'click .x-search' : '_moviesSearch', 'click .x-search' : '_moviesSearch',
'click .x-manual-search' : '_showSearch', 'click .x-manual-search' : '_showSearch',
'click .x-movie-history' : '_showHistory', 'click .x-movie-history' : '_showHistory',
'click .x-movie-search' : '_showSearch', 'click .x-movie-search' : '_showSearch',
"click .x-movie-files" : "_showFiles", 'click .x-movie-files-tabs' : '_showFileTabs',
"click .x-movie-titles" : "_showTitles", "click .x-movie-titles" : "_showTitles",
}, },
initialize : function() { initialize : function() {
@ -79,26 +78,20 @@ module.exports = Marionette.Layout.extend({
}, },
_refreshFiles : function() { _refreshFiles : function() {
this._showFiles(); this._showFileTabs();
}, },
onShow : function() { onShow : function() {
this.searchLayout = new SearchLayout({ model : this.model }); this.searchLayout = new SearchLayout({ model : this.model });
this.searchLayout.startManualSearch = true; this.searchLayout.startManualSearch = true;
this.allFilesLayout = new AllFilesLayout({ model : this.model });
this.filesLayout = new FilesLayout({ model : this.model });
this.titlesLayout = new TitlesLayout({ model : this.model }); this.titlesLayout = new TitlesLayout({ model : this.model });
this._showBackdrop(); this._showBackdrop();
this._showSeasons(); this._showSeasons();
this._setMonitoredState(); this._setMonitoredState();
this._showInfo(); this._showInfo();
if (this.model.get("movieFile")) { this._showHistory();
this._showFiles();
} else {
this._showHistory();
}
}, },
onRender : function() { onRender : function() {
@ -166,13 +159,13 @@ module.exports = Marionette.Layout.extend({
this.search.show(this.searchLayout); this.search.show(this.searchLayout);
}, },
_showFiles : function(e) { _showFileTabs : function(e) {
if (e) { if (e) {
e.preventDefault(); e.preventDefault();
} }
this.ui.files.tab('show'); this.ui.filesTabs.tab('show');
this.files.show(this.filesLayout); this.filesTabs.show(this.allFilesLayout);
}, },
_showTitles : function(e) { _showTitles : function(e) {
@ -254,9 +247,6 @@ module.exports = Marionette.Layout.extend({
}, },
_refresh : function() { _refresh : function() {
//this.seasonCollection.add(this.model.get('seasons'), { merge : true });
//this.episodeCollection.fetch();
//this.episodeFileCollection.fetch();
this._setMonitoredState(); this._setMonitoredState();
this._showInfo(); this._showInfo();
}, },

View File

@ -43,13 +43,14 @@
<ul class="nav nav-tabs" id="myTab"> <ul class="nav nav-tabs" id="myTab">
<li><a href="#movie-history" class="x-movie-history">History</a></li> <li><a href="#movie-history" class="x-movie-history">History</a></li>
<li><a href="#movie-search" class="x-movie-search">Search</a></li> <li><a href="#movie-search" class="x-movie-search">Search</a></li>
<li><a href="#movie-files" class="x-movie-files">Files</a></li> <li><a href="#movie-files-tabs" class="x-movie-files-tabs">Files</a></li>
<li><a href="#movie-titles" class="x-movie-titles">Titles</a></li> <li><a href="#movie-titles" class="x-movie-titles">Titles</a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane" id="movie-history"/> <div class="tab-pane" id="movie-history"/>
<div class="tab-pane" id="movie-search"/> <div class="tab-pane" id="movie-search"/>
<div class="tab-pane" id="movie-files"/> <div class="tab-pane" id="movie-files-tabs"/>
<div class="tab-pane" id="movie-titles"/> <div class="tab-pane" id="movie-titles"/>
</div> </div>
</div> </div>

View File

@ -0,0 +1,30 @@
var vent = require('vent');
var Marionette = require('marionette');
var FilesLayout = require('./Media/FilesLayout');
var ExtraFilesLayout = require('./Extras/ExtraFilesLayout');
module.exports = Marionette.Layout.extend({
template : 'Movies/Files/AllFilesLayoutTemplate',
regions : {
files : "#movie-files",
mediaFiles : "#movie-media-files",
extras : "#movie-extra-files"
},
onShow : function() {
this.filesLayout = new FilesLayout({ model : this.model });
this.extraFilesLayout = new ExtraFilesLayout({ model : this.model });
this._showFiles();
},
_showFiles : function(e) {
if (e) {
e.preventDefault();
}
this.mediaFiles.show(this.filesLayout);
this.extras.show(this.extraFilesLayout);
}
});

View File

@ -0,0 +1,5 @@
<div class="x-movie-files" id="movie-files">
<div id="movie-media-files" />
<legend>Extras</legend>
<div id="movie-extra-files" />
</div>

View File

@ -0,0 +1,37 @@
var PagableCollection = require('backbone.pageable');
var ExtraFileModel = require('./ExtraFileModel');
var AsSortedCollection = require('../../../Mixins/AsSortedCollection');
var Collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + "/extrafile",
model : ExtraFileModel,
state : {
pageSize : 2000,
sortKey : 'relativePath',
order : -1
},
mode : 'client',
sortMappings : {
'relativePath' : {
sortKey : "relativePath"
},
"type" : {
sortKey : "type"
},
"extension" : {
sortKey : "extension"
}
},
fetchMovieExtras : function(movieId) {
return this.fetch({ data : { movieId : movieId}});
}
});
Collection = AsSortedCollection.call(Collection);
module.exports = Collection;

View File

@ -0,0 +1,62 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var ExtraFilesCollection = require('./ExtraFilesCollection');
var LoadingView = require('../../../Shared/LoadingView');
var ExtraFileModel = require("./ExtraFileModel");
var FileTitleCell = require('../../../Cells/FileTitleCell');
var ExtraExtensionCell = require('../../../Cells/ExtraExtensionCell');
var ExtraTypeCell = require('../../../Cells/ExtraTypeCell');
var NoResultsView = require('../NoFilesView');
module.exports = Marionette.Layout.extend({
template : 'Movies/Files/Extras/ExtraFilesLayoutTemplate',
regions : {
extraFilesTable : '.extra-files-table'
},
columns : [
{
name : 'relativePath',
label : 'File',
cell : FileTitleCell
},
{
name : 'extension',
label : 'Extension',
cell : ExtraExtensionCell
},
{
name : 'type',
label : 'Type',
cell : ExtraTypeCell
}
],
initialize : function() {
this.collection = new ExtraFilesCollection();
this.listenTo(this.collection, 'sync', this._showTable);
},
onShow : function() {
this.extraFilesTable.show(new LoadingView());
this.collection.fetchMovieExtras(this.model.id);
},
_showTable : function() {
if (this.collection.any()) {
this.extraFilesTable.show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.collection,
className : 'table table-hover'
}));
} else {
this.extraFilesTable.show(new NoResultsView());
}
}
});

View File

@ -0,0 +1 @@
<div class="extra-files-table table-responsive"></div>

View File

@ -1,148 +0,0 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
//var ButtonsView = require('./ButtonsView');
//var ManualSearchLayout = require('./ManualLayout');
var FilesCollection = require('./FilesCollection');
var CommandController = require('../../Commands/CommandController');
var LoadingView = require('../../Shared/LoadingView');
var NoResultsView = require('./NoFilesView');
var FileModel = require("./FileModel");
var FileTitleCell = require('../../Cells/FileTitleCell');
var FileSizeCell = require('../../Cells/FileSizeCell');
var QualityCell = require('../../Cells/QualityCell');
var MediaInfoCell = require('../../Cells/MediaInfoCell');
var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell');
var DownloadReportCell = require('../../Release/DownloadReportCell');
var AgeCell = require('../../Release/AgeCell');
var ProtocolCell = require('../../Release/ProtocolCell');
var PeersCell = require('../../Release/PeersCell');
var EditionCell = require('../../Cells/EditionCell');
var DeleteFileCell = require("./DeleteFileCell");
var EditFileCell = require("./EditFileCell");
module.exports = Marionette.Layout.extend({
template : 'Movies/Files/FilesLayoutTemplate',
regions : {
main : '#movie-files-region',
grid : "#movie-files-grid"
},
events : {
'click .x-search-auto' : '_searchAuto',
'click .x-search-manual' : '_searchManual',
'click .x-search-back' : '_showButtons'
},
columns : [
{
name : 'title',
label : 'Title',
cell : FileTitleCell
},
{
name : "mediaInfo",
label : "Media Info",
cell : MediaInfoCell
},
{
name : 'edition',
label : 'Edition',
cell : EditionCell,
title : "Edition",
},
{
name : 'size',
label : 'Size',
cell : FileSizeCell
},
{
name : 'quality',
label : 'Quality',
cell : QualityCell,
},
{
name : "delete",
label : "",
cell : DeleteFileCell,
},
{
name : "edit",
label : "",
cell : EditFileCell,
}
],
initialize : function(movie) {
this.filesCollection = new FilesCollection();
var file = movie.model.get("movieFile");
this.movie = movie;
this.filesCollection.add(file);
//this.listenTo(this.releaseCollection, 'sync', this._showSearchResults);
this.listenTo(this.model, 'change', function(model, options) {
if (options && options.changeSource === 'signalr') {
this._refresh(model);
}
});
vent.on(vent.Commands.MovieFileEdited, this._showGrid, this);
},
_refresh : function(model) {
this.filesCollection = new FilesCollection();
if(model.get('hasFile')) {
var file = model.get("movieFile");
this.filesCollection.add(file);
}
this.onShow();
},
_refreshClose : function(options) {
this.filesCollection = new FilesCollection();
var file = this.movie.model.get("movieFile");
this.filesCollection.add(file);
this._showGrid();
},
onShow : function() {
this.grid.show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.filesCollection,
className : 'table table-hover'
}));
},
_showGrid : function() {
this.regionManager.get('grid').show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.filesCollection,
className : 'table table-hover'
}));
},
_showMainView : function() {
this.main.show(this.mainView);
},
_showButtons : function() {
this._showMainView();
},
_showSearchResults : function() {
if (this.releaseCollection.length === 0) {
this.mainView = new NoResultsView();
}
else {
//this.mainView = new ManualSearchLayout({ collection : this.releaseCollection });
}
this._showMainView();
}
});

View File

@ -1,3 +0,0 @@
<div id="movie-files-region">
<div id="movie-files-grid" class="table-responsive"></div>
</div>

View File

@ -1,14 +1,14 @@
var vent = require('vent'); var vent = require('vent');
var Marionette = require('marionette'); var Marionette = require('marionette');
var Qualities = require('../../../Quality/QualityDefinitionCollection'); var Qualities = require('../../../../Quality/QualityDefinitionCollection');
var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); var AsModelBoundView = require('../../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../Mixins/AsValidatedView'); var AsValidatedView = require('../../../../Mixins/AsValidatedView');
var AsEditModalView = require('../../../Mixins/AsEditModalView'); var AsEditModalView = require('../../../../Mixins/AsEditModalView');
require('../../../Mixins/TagInput'); require('../../../../Mixins/TagInput');
require('../../../Mixins/FileBrowser'); require('../../../../Mixins/FileBrowser');
var view = Marionette.ItemView.extend({ var view = Marionette.ItemView.extend({
template : 'Movies/Files/Edit/EditFileTemplate', template : 'Movies/Files/Media/Edit/EditFileTemplate',
ui : { ui : {
quality : '.x-quality', quality : '.x-quality',

View File

@ -0,0 +1,3 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({});

View File

@ -1,6 +1,6 @@
var PagableCollection = require('backbone.pageable'); var PagableCollection = require('backbone.pageable');
var FileModel = require('./FileModel'); var FileModel = require('./FileModel');
var AsSortedCollection = require('../../Mixins/AsSortedCollection'); var AsSortedCollection = require('../../../Mixins/AsSortedCollection');
var Collection = PagableCollection.extend({ var Collection = PagableCollection.extend({
url : window.NzbDrone.ApiRoot + "/moviefile", url : window.NzbDrone.ApiRoot + "/moviefile",

View File

@ -0,0 +1,120 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var FilesCollection = require('./FilesCollection');
var CommandController = require('../../../Commands/CommandController');
var LoadingView = require('../../../Shared/LoadingView');
var NoResultsView = require('../NoFilesView');
var FileModel = require("./FileModel");
var FileTitleCell = require('../../../Cells/FileTitleCell');
var FileSizeCell = require('../../../Cells/FileSizeCell');
var QualityCell = require('../../../Cells/QualityCell');
var MediaInfoCell = require('../../../Cells/MediaInfoCell');
var EditionCell = require('../../../Cells/EditionCell');
var DeleteFileCell = require("./DeleteFileCell");
var EditFileCell = require("./EditFileCell");
module.exports = Marionette.Layout.extend({
template : 'Movies/Files/Media/FilesLayoutTemplate',
regions : {
grid : "#movie-files-grid"
},
events : {
'click .x-search-auto' : '_searchAuto',
'click .x-search-manual' : '_searchManual',
'click .x-search-back' : '_showButtons'
},
columns : [
{
name : 'title',
label : 'Title',
cell : FileTitleCell
},
{
name : "mediaInfo",
label : "Media Info",
cell : MediaInfoCell
},
{
name : 'edition',
label : 'Edition',
cell : EditionCell,
title : "Edition",
},
{
name : 'size',
label : 'Size',
cell : FileSizeCell
},
{
name : 'quality',
label : 'Quality',
cell : QualityCell,
},
{
name : "delete",
label : "",
cell : DeleteFileCell,
},
{
name : "edit",
label : "",
cell : EditFileCell,
}
],
initialize : function(movie) {
this.filesCollection = new FilesCollection();
var file = movie.model.get("movieFile");
this.movie = movie;
this.filesCollection.add(file);
this.listenTo(this.model, 'change', function(model, options) {
if (options && options.changeSource === 'signalr') {
this._refresh(model);
}
});
vent.on(vent.Commands.MovieFileEdited, this._showGrid, this);
},
_refresh : function(model) {
this.filesCollection = new FilesCollection();
if(model.get('hasFile')) {
var file = model.get("movieFile");
this.filesCollection.add(file);
}
this.onShow();
},
_refreshClose : function(options) {
this.filesCollection = new FilesCollection();
var file = this.movie.model.get("movieFile");
this.filesCollection.add(file);
this._showGrid();
},
onShow : function() {
this._showGrid();
},
_showGrid : function() {
if (this.filesCollection.length === 0) {
this.grid.show(new NoResultsView());
}
else {
this.regionManager.get('grid').show(new Backgrid.Grid({
row : Backgrid.Row,
columns : this.columns,
collection : this.filesCollection,
className : 'table table-hover'
}));
}
}
});

View File

@ -0,0 +1,2 @@
<div id="movie-files-grid" class="table-responsive"/>

View File

@ -3,7 +3,34 @@ var AsModelBoundView = require('../../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../../Mixins/AsValidatedView'); var AsValidatedView = require('../../../Mixins/AsValidatedView');
var view = Marionette.ItemView.extend({ var view = Marionette.ItemView.extend({
template : 'Settings/MediaManagement/Sorting/SortingViewTemplate' template : 'Settings/MediaManagement/Sorting/SortingViewTemplate',
events : {
'change .x-import-extra-files' : '_setExtraFileExtensionVisibility'
},
ui : {
importExtraFiles : '.x-import-extra-files',
extraFileExtensions : '.x-extra-file-extensions'
},
onRender : function() {
if (!this.ui.importExtraFiles.prop('checked')) {
this.ui.extraFileExtensions.hide();
}
},
_setExtraFileExtensionVisibility : function() {
var showExtraFileExtensions = this.ui.importExtraFiles.prop('checked');
if (showExtraFileExtensions) {
this.ui.extraFileExtensions.slideDown();
}
else {
this.ui.extraFileExtensions.slideUp();
}
}
}); });
AsModelBoundView.call(view); AsModelBoundView.call(view);

View File

@ -71,11 +71,11 @@
</div> </div>
</fieldset> </fieldset>
<fieldset class="advanced-setting"> <fieldset>
<legend>Importing</legend> <legend>Importing</legend>
{{#if_mono}} {{#if_mono}}
<div class="form-group"> <div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Skip Free Space Check</label> <label class="col-sm-3 control-label">Skip Free Space Check</label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -99,7 +99,7 @@
</div> </div>
{{/if_mono}} {{/if_mono}}
<div class="form-group"> <div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Use Hardlinks instead of Copy</label> <label class="col-sm-3 control-label">Use Hardlinks instead of Copy</label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -122,4 +122,39 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-3 control-label">Import Extra Files</label>
<div class="col-sm-9">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="importExtraFiles" class="x-import-extra-files"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-sonarr-form-info" title="Import matching extra files (subtitles, nfo, etc) after importing an episode file"/>
</span>
</div>
</div>
</div>
<div class="form-group x-extra-file-extensions">
<label class="col-sm-3 control-label">Extra File Extensions</label>
<div class="col-sm-1 col-sm-push-5 help-inline">
<i class="icon-sonarr-form-info" title="Comma separated list of extra files to import, ie sub,nfo (.nfo will be imported as .nfo-orig)"/>
</div>
<div class="col-sm-5 col-sm-pull-1">
<input type="text" name="extraFileExtensions" class="form-control"/>
</div>
</div>
</fieldset> </fieldset>

View File

@ -15,7 +15,7 @@
{{#if value}} {{#if value}}
<span class="label label-success">{{label}}</span> <span class="label label-success">{{label}}</span>
{{else}} {{else}}
<span class="label">{{label}}</span> <span class="label label-default">{{label}}</span>
{{/if}} {{/if}}
{{/if_eq}} {{/if_eq}}
{{/each}} {{/each}}

View File

@ -11,7 +11,7 @@ var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout');
var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); var ManualImportLayout = require('../../ManualImport/ManualImportLayout');
var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout');
var MoviesDetailsLayout = require('../../Movies/Details/MoviesDetailsLayout'); var MoviesDetailsLayout = require('../../Movies/Details/MoviesDetailsLayout');
var EditFileView = require("../../Movies/Files/Edit/EditFileView"); var EditFileView = require("../../Movies/Files/Media/Edit/EditFileView");
module.exports = Marionette.AppRouter.extend({ module.exports = Marionette.AppRouter.extend({
initialize : function() { initialize : function() {