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

Fixes #121, Fixes #167, Fixes #2262 and Fixes #1104
pull/2425/merge
Qstick 6 years ago committed by Leonardo Galli
parent b40423f3a3
commit e7e9e2b154

@ -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 @@ namespace NzbDrone.Api.Config
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 @@ namespace NzbDrone.Api.Config
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
}; };

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

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

@ -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" />

@ -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 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[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 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
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 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
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 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
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))

@ -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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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();

@ -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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
} }
[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);

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

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

@ -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 @@ namespace NzbDrone.Core.Configuration
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); }
} }

@ -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 @@ namespace NzbDrone.Core.Configuration
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; }

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

@ -36,45 +36,7 @@ using NzbDrone.Core.Extras.Subtitles;
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
{ {

@ -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 @@ using NzbDrone.Core.Messaging.Events;
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 @@ namespace NzbDrone.Core.Extras
_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);

@ -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 @@ namespace NzbDrone.Core.Extras
.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 @@ namespace NzbDrone.Core.Extras
{ {
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 @@ namespace NzbDrone.Core.Extras
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;
} }
} }
} }

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

@ -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 @@ namespace NzbDrone.Core.Extras.Files
{ {
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 @@ namespace NzbDrone.Core.Extras.Files
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;
}
} }
} }

@ -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 @@ namespace NzbDrone.Core.Extras.Files
{ {
} }
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)

@ -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 @@ namespace NzbDrone.Core.Extras.Files
} }
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 @@ namespace NzbDrone.Core.Extras.Files
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 @@ namespace NzbDrone.Core.Extras.Files
_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 @@ namespace NzbDrone.Core.Extras.Files
} }
} }
_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);
} }
} }
} }

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

@ -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 @@ namespace NzbDrone.Core.Extras
} }
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 @@ namespace NzbDrone.Core.Extras
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);

@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
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));
tvShow.Add(new XElement("Status", series.Status));
tvShow.Add(new XElement("Network", series.Network));
tvShow.Add(new XElement("Airs_Time", series.AirTime));
if (series.FirstAired.HasValue)
{
tvShow.Add(new XElement("FirstAired", series.FirstAired.Value.ToString("yyyy-MM-dd")));
}
tvShow.Add(new XElement("ContentRating", series.Certification));
tvShow.Add(new XElement("Added", series.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
tvShow.Add(new XElement("LockData", "false"));
tvShow.Add(new XElement("Overview", series.Overview));
tvShow.Add(new XElement("LocalTitle", series.Title));
if (series.FirstAired.HasValue)
{
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);
_logger.Debug("Saving series.xml for {0}", series.Title); movieElement.Add(new XElement("id", movie.ImdbId));
movieElement.Add(new XElement("Status", movie.Status));
return new MetadataFileResult("series.xml", doc.ToString()); movieElement.Add(new XElement("Added", movie.Added.ToString("MM/dd/yyyy HH:mm:ss tt")));
} movieElement.Add(new XElement("LockData", "false"));
} movieElement.Add(new XElement("Overview", movie.Overview));
movieElement.Add(new XElement("LocalTitle", movie.Title));
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) movieElement.Add(new XElement("Rating", movie.Ratings.Value));
{ movieElement.Add(new XElement("ProductionYear", movie.Year));
return null; movieElement.Add(new XElement("RunningTime", movie.Runtime));
} movieElement.Add(new XElement("IMDB", movie.ImdbId));
movieElement.Add(new XElement("Genres", movie.Genres.Select(genre => new XElement("Genre", genre))));
public override List<ImageFileResult> SeriesImages(Series series) var doc = new XDocument(movieElement);
{ doc.Save(xw);
return new List<ImageFileResult>();
}
public override List<ImageFileResult> SeasonImages(Series series, Season season) _logger.Debug("Saving movie.xml for {0}", movie.Title);
{
return new List<ImageFileResult>();
}
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) return new MetadataFileResult("movie.xml", doc.ToString());
{ }
return new List<ImageFileResult>();
} }
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) private IEnumerable<ImageFileResult> ProcessMovieImages(Movie movie)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
private string GetEpisodeNfoFilename(string episodeFilePath)
{
return null;
}
private string GetEpisodeImageFilename(string episodeFilePath)
{
return null;
}
} }
} }

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser
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;

@ -31,30 +31,30 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
_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,55 +63,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
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)
{
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; if (parseResult != null)
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;
} }
} }
@ -120,24 +93,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
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() &&
ValidCertification.Contains(series.Certification.ToUpperInvariant()))
{
details.Add(new XElement("mpaa", series.Certification.ToUpperInvariant()));
}
else details.Add(new XElement("genre", string.Join(" / ", movie.Genres)));
{ details.Add(new XElement("description", movie.Overview));
details.Add(new XElement("mpaa", "UNRATED")); details.Add(new XElement("length", movie.Runtime));
}
doc.Add(details); doc.Add(details);
doc.Save(xw); doc.Save(xw);
@ -173,111 +126,39 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
xmlResult += doc.ToString(); xmlResult += doc.ToString();
xmlResult += Environment.NewLine; xmlResult += Environment.NewLine;
} }
}
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
}
public override List<ImageFileResult> SeriesImages(Series series)
{
var image = series.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? series.Images.FirstOrDefault();
if (image == null)
{
_logger.Trace("Failed to find suitable Series image for series {0}.", series.Title);
return null;
}
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = Path.GetFileName(series.Path) + Path.GetExtension(source);
return new List<ImageFileResult>{ new ImageFileResult(destination, source) }; return new MetadataFileResult(GetMovieFileMetadataFilename(movieFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{ {
var seasonFolders = GetSeasonFolders(series); if (!Settings.MovieImages)
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>(); 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 = movie.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? movie.Images.FirstOrDefault();
var image = season.Images.SingleOrDefault(c => c.CoverType == MediaCoverTypes.Poster) ?? season.Images.FirstOrDefault();
if (image == null) if (image == null)
{ {
_logger.Trace("Failed to find suitable season image for series {0}, season {1}.", series.Title, season.SeasonNumber); _logger.Trace("Failed to find suitable Movie image for movie {0}.", movie.Title);
return new List<ImageFileResult>(); return null;
} }
var filename = Path.GetFileName(seasonFolder) + ".jpg"; var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
var path = series.Path.GetRelativePath(Path.Combine(series.Path, seasonFolder, filename)); var destination = Path.GetFileName(movie.Path) + Path.GetExtension(source);
return new List<ImageFileResult> { new ImageFileResult(path, image.Url) };
}
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile)
{
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)}; return new List<ImageFileResult> { new ImageFileResult(destination, source) };
} }
private string GetEpisodeMetadataFilename(string episodeFilePath) private string GetMovieFileMetadataFilename(string movieFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "xml"); return Path.ChangeExtension(movieFilePath, "xml");
} }
private string GetEpisodeImageFilename(string episodeFilePath) private string GetMovieFileImageFilename(string movieFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "jpg"); return Path.ChangeExtension(movieFilePath, "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;
} }
} }
} }

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox
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;

@ -31,30 +31,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
_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,49 +60,28 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
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;
} }
@ -113,24 +90,17 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
}; };
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) private string GetMovieFileMetadataFilename(string movieFilePath)
{
if (!Settings.SeasonImages)
{
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)
{ {
if (!Settings.EpisodeImages) return Path.ChangeExtension(movieFilePath, "xml");
{
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) private string GetMovieFileImageFilename(string movieFilePath)
{ {
return Path.ChangeExtension(episodeFilePath, "xml"); return Path.ChangeExtension(movieFilePath, "metathumb");
}
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;
} }
} }
} }

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Wdtv
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;

@ -27,357 +27,227 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
_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,
return metadata; Consumer = GetType().Name,
} RelativePath = movie.Path.GetRelativePath(path)
};
var seasonMatch = SeasonImagesRegex.Match(filename); if (MovieImagesRegex.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 (MovieFileImageRegex.IsMatch(filename))
{ {
metadata.Type = MetadataType.EpisodeImage; metadata.Type = MetadataType.MovieImage;
return metadata; return metadata;
} }
if (filename.Equals("tvshow.nfo", StringComparison.InvariantCultureIgnoreCase)) if (filename.Equals("movie.nfo", StringComparison.OrdinalIgnoreCase))
{ {
metadata.Type = MetadataType.SeriesMetadata; metadata.Type = MetadataType.MovieMetadata;
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 && 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())
{
xmlActor.Add(new XElement("thumb", actor.Images.First().Url));
}
tvShow.Add(xmlActor);
} }
var doc = new XDocument(tvShow); else
doc.Save(xw);
_logger.Debug("Saving tvshow.nfo for {0}", series.Title);
return new MetadataFileResult("tvshow.nfo", doc.ToString());
}
}
public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile)
{
if (!Settings.EpisodeMetadata)
{
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(); details.Add(new XElement("thumb", image.Url));
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("watched", "false"));
details.Add(new XElement("displayseason"));
details.Add(new XElement("displayepisode"));
if (image == null) 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)
{ {
details.Add(new XElement("thumb")); video.Add(new XElement("duration", movieFile.MediaInfo.RunTime.TotalMinutes));
video.Add(new XElement("durationinseconds", movieFile.MediaInfo.RunTime.TotalSeconds));
} }
else streamDetails.Add(video);
{
details.Add(new XElement("thumb", image.Url));
}
details.Add(new XElement("watched", "false")); 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 (episode.Ratings != null && episode.Ratings.Votes > 0) if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Length > 0)
{ {
details.Add(new XElement("rating", episode.Ratings.Value)); var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", movieFile.MediaInfo.Subtitles));
streamDetails.Add(subtitle);
} }
if (episodeFile.MediaInfo != null) fileInfo.Add(streamDetails);
{ details.Add(fileInfo);
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 doc.Add(details);
//details.Add(new XElement("credits", tvdbEpisode.Writer.FirstOrDefault())); doc.Save(xw);
//details.Add(new XElement("director", tvdbEpisode.Directors.FirstOrDefault()));
doc.Add(details); xmlResult += doc.ToString();
doc.Save(xw); xmlResult += Environment.NewLine;
xmlResult += doc.ToString();
xmlResult += Environment.NewLine;
}
} }
return new MetadataFileResult(GetEpisodeMetadataFilename(episodeFile.RelativePath), xmlResult.Trim(Environment.NewLine.ToCharArray())); var metadataFileName = GetMovieMetadataFilename(movieFile.RelativePath);
}
public override List<ImageFileResult> SeriesImages(Series series) if (Settings.UseMovieNfo)
{
if (!Settings.SeriesImages)
{ {
return new List<ImageFileResult>(); metadataFileName = "movie.nfo";
} }
return ProcessSeriesImages(series).ToList(); return new MetadataFileResult(metadataFileName, xmlResult.Trim(Environment.NewLine.ToCharArray()));
} }
public override List<ImageFileResult> SeasonImages(Series series, Season season) public override List<ImageFileResult> MovieImages(Movie movie, MovieFile movieFile)
{ {
if (!Settings.SeasonImages) if (!Settings.MovieImages)
{ {
return new List<ImageFileResult>(); return new List<ImageFileResult>();
} }
return ProcessSeasonImages(series, season).ToList(); return ProcessMovieImages(movie).ToList();
} }
public override List<ImageFileResult> EpisodeImages(Series series, EpisodeFile episodeFile) private IEnumerable<ImageFileResult> ProcessMovieImages(Movie movie)
{ {
if (!Settings.EpisodeImages) foreach (var image in movie.Images)
{
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)); var source = _mediaCoverService.GetCoverPath(movie.Id, image.CoverType);
var destination = Path.ChangeExtension(movie.MovieFile.RelativePath,"").TrimEnd(".") + "-" + image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
return new List<ImageFileResult>(); yield return new ImageFileResult(destination, source);
} }
} }
private IEnumerable<ImageFileResult> ProcessSeriesImages(Series series) private string GetMovieMetadataFilename(string movieFilePath)
{ {
foreach (var image in series.Images) return Path.ChangeExtension(movieFilePath, "nfo");
{
var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType);
var destination = image.CoverType.ToString().ToLowerInvariant() + Path.GetExtension(source);
yield return new ImageFileResult(destination, source);
}
} }
private IEnumerable<ImageFileResult> ProcessSeasonImages(Series series, Season season) private string GetMovieImageFilename(string movieFilePath, string existingImageName)
{ {
foreach (var image in season.Images) var fileExtention = Path.GetExtension(existingImageName);
{ var match = MovieFileImageRegex.Matches(existingImageName);
var filename = string.Format("season{0:00}-{1}.jpg", season.SeasonNumber, image.CoverType.ToString().ToLower());
if (season.SeasonNumber == 0) if (match.Count > 0)
{ {
filename = string.Format("season-specials-{0}.jpg", image.CoverType.ToString().ToLower()); var imageType = match[0].Groups["type"].Value;
} return Parser.Parser.RemoveFileExtension(movieFilePath) + imageType + fileExtention;
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)

@ -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,27 +18,19 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
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;

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata
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 @@ namespace NzbDrone.Core.Extras.Metadata
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);

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata.Files
_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);

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata.Files
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)
{ {
} }

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

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata
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; }

@ -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 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
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 @@ namespace NzbDrone.Core.Extras.Metadata
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 @@ namespace NzbDrone.Core.Extras.Metadata
{ {
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,47 +78,15 @@ namespace NzbDrone.Core.Extras.Metadata
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>();
foreach (var consumer in _metadataFactory.Enabled())
{
files.AddIfNotNull(ProcessEpisodeMetadata(consumer, series, episodeFile, new List<MetadataFile>()));
files.AddRange(ProcessEpisodeImages(consumer, series, episodeFile, new List<MetadataFile>()));
}
_metadataFileService.Upsert(files);
return files;
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id);
if (seriesFolder.IsNullOrWhiteSpace() && seasonFolder.IsNullOrWhiteSpace())
{
return new List<MetadataFile>();
}
var files = new List<MetadataFile>(); var files = new List<MetadataFile>();
foreach (var consumer in _metadataFactory.Enabled()) 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.AddIfNotNull(ProcessMovieMetadata(consumer, movie, movieFile, new List<MetadataFile>()));
{ files.AddRange(ProcessMovieImages(consumer, movie, movieFile, new List<MetadataFile>()));
files.AddRange(ProcessSeasonImages(consumer, series, consumerFiles));
}
} }
_metadataFileService.Upsert(files); _metadataFileService.Upsert(files);
@ -130,9 +94,9 @@ namespace NzbDrone.Core.Extras.Metadata
return files; return files;
} }
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles)
{ {
var metadataFiles = _metadataFileService.GetFilesBySeries(series.Id); var metadataFiles = _metadataFileService.GetFilesByMovie(movie.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 @@ namespace NzbDrone.Core.Extras.Metadata
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 @@ namespace NzbDrone.Core.Extras.Metadata
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)
{
return null;
}
var hash = seriesMetadata.Contents.SHA256Hash();
var metadata = GetMetadataFile(series, existingMetadataFiles, e => e.Type == MetadataType.SeriesMetadata) ??
new MetadataFile
{
SeriesId = series.Id,
Consumer = consumer.GetType().Name,
Type = MetadataType.SeriesMetadata
};
if (hash == metadata.Hash) if (movieFileMetadata == null)
{ {
if (seriesMetadata.RelativePath != metadata.RelativePath)
{
metadata.RelativePath = seriesMetadata.RelativePath;
return metadata;
}
return null; return null;
} }
var fullPath = Path.Combine(series.Path, seriesMetadata.RelativePath); var fullPath = Path.Combine(movie.Path, movieFileMetadata.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; var existingMetadata = GetMetadataFile(movie, existingMetadataFiles, c => c.Type == MetadataType.MovieMetadata &&
} c.MovieFileId == movieFile.Id);
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 @@ namespace NzbDrone.Core.Extras.Metadata
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>();
foreach (var image in consumer.SeriesImages(series))
{
var fullPath = Path.Combine(series.Path, image.RelativePath);
if (_diskProvider.FileExists(fullPath))
{
_logger.Debug("Series image already exists: {0}", fullPath);
continue;
}
var metadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.SeriesImage &&
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>(); var result = new List<MetadataFile>();
foreach (var image in consumer.EpisodeImages(series, episodeFile)) 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("Episode image already exists: {0}", fullPath); _logger.Debug("Movie image already exists: {0}", fullPath);
continue; continue;
} }
var existingMetadata = GetMetadataFile(series, existingMetadataFiles, c => c.Type == MetadataType.EpisodeImage && var existingMetadata = GetMetadataFile(movie, existingMetadataFiles, c => c.Type == MetadataType.MovieImage &&
c.EpisodeFileId == episodeFile.Id); c.RelativePath == image.RelativePath);
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 @@ namespace NzbDrone.Core.Extras.Metadata
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 @@ namespace NzbDrone.Core.Extras.Metadata
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 @@ namespace NzbDrone.Core.Extras.Metadata
} }
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 @@ namespace NzbDrone.Core.Extras.Metadata
_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 @@ namespace NzbDrone.Core.Extras.Metadata
//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);

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

@ -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 @@ namespace NzbDrone.Core.Extras.Others
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("Unable to parse extra file: {0}", possibleExtraFile); _logger.Debug("No extension for file: {0}", possibleExtraFile);
continue; continue;
} }
if (localEpisode.Episodes.Empty()) var localMovie = _parsingService.GetLocalMovie(possibleExtraFile, movie);
if (localMovie == null)
{ {
_logger.Debug("Cannot find related episodes for: {0}", possibleExtraFile); _logger.Debug("Unable to parse extra file: {0}", possibleExtraFile);
continue; continue;
} }
if (localEpisode.Episodes.DistinctBy(e => e.EpisodeFileId).Count() > 1) if (localMovie.Movie == null)
{ {
_logger.Debug("Extra file: {0} does not match existing files.", possibleExtraFile); _logger.Debug("Cannot find related movie for: {0}", 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);

@ -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 @@ namespace NzbDrone.Core.Extras.Others
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)
{ {
} }
} }

@ -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>();
}
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, EpisodeFile episodeFile)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> CreateAfterEpisodeImport(Series series, string seriesFolder, string seasonFolder) public override IEnumerable<ExtraFile> CreateAfterMovieImport(Movie movie, MovieFile movieFile)
{ {
return Enumerable.Empty<ExtraFile>(); return Enumerable.Empty<ExtraFile>();
} }
public override IEnumerable<ExtraFile> MoveFilesAfterRename(Series series, List<EpisodeFile> episodeFiles) public override IEnumerable<ExtraFile> MoveFilesAfterRename(Movie movie, List<MovieFile> movieFiles)
{ {
// TODO: Remove var extraFiles = _otherExtraFileService.GetFilesByMovie(movie.Id);
// 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 @@ namespace NzbDrone.Core.Extras.Others
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);

@ -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 @@ namespace NzbDrone.Core.Extras.Subtitles
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 @@ namespace NzbDrone.Core.Extras.Subtitles
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
}; };

@ -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 @@ namespace NzbDrone.Core.Extras.Subtitles
static SubtitleFileExtensions() static SubtitleFileExtensions()
{ {
_fileExtensions = new HashSet<string> _fileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
".aqt", ".aqt",
".ass", ".ass",

@ -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 @@ namespace NzbDrone.Core.Extras.Subtitles
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)
{ {
} }
} }

@ -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 @@ namespace NzbDrone.Core.Extras.Subtitles
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 @@ namespace NzbDrone.Core.Extras.Subtitles
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();
} }
} }
} }

@ -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 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
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 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
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
)"); )");
} }
} }

@ -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 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
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)");
} }
} }
} }

@ -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 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
{ {
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);

@ -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 @@ namespace NzbDrone.Core.MediaFiles
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 @@ namespace NzbDrone.Core.MediaFiles
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 @@ namespace NzbDrone.Core.MediaFiles
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);

@ -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 @@ namespace NzbDrone.Core.MediaFiles
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)
{ {

@ -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 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (newDownload) if (newDownload)
{ {
_extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly); // _extraService.ImportExtraFiles(localEpisode, episodeFile, copyOnly);
} }
if (downloadClientItem != null) if (downloadClientItem != null)

@ -120,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (newDownload) if (newDownload)
{ {
//_extraService.ImportExtraFiles(localMovie, episodeFile, copyOnly); TODO update for movie _extraService.ImportExtraFiles(localMovie, movieFile, copyOnly);
} }
if (downloadClientItem != null) if (downloadClientItem != null)

@ -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 @@ namespace NzbDrone.Core.MediaFiles
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 @@ namespace NzbDrone.Core.MediaFiles
}; };
} }
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)
{ {

@ -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 @@ namespace NzbDrone.Core.MediaFiles
{ {
_logger.Error(ex, "Failed to rename file: " + oldMovieFilePath); _logger.Error(ex, "Failed to rename file: " + oldMovieFilePath);
} }
if (renamed.Any())
{
_eventAggregator.PublishEvent(new MovieRenamedEvent(movie));
}
} }
} }

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

@ -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,9 +24,8 @@ namespace NzbDrone.Core.Tv
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;
@ -36,21 +35,19 @@ namespace NzbDrone.Core.Tv
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;
@ -99,7 +96,7 @@ namespace NzbDrone.Core.Tv
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
{ {
@ -177,7 +174,7 @@ namespace NzbDrone.Core.Tv
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 @@ namespace NzbDrone.Core.Tv
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)

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

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

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

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

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

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

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

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

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

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

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

@ -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',

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

@ -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",

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

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

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

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

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

@ -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() {

Loading…
Cancel
Save