diff --git a/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs new file mode 100644 index 000000000..4734d90bd --- /dev/null +++ b/src/NzbDrone.Core.Test/Extras/Metadata/Consumers/Kometa/FindMetadataFileFixture.cs @@ -0,0 +1,76 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Extras.Metadata; +using NzbDrone.Core.Extras.Metadata.Consumers.Kometa; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Extras.Metadata.Consumers.Kometa +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Season00")] + [TestCase("Season01")] + [TestCase("Season02")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [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, "s01e01" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".jpg")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_not_return_metadata_if_image_file_is_a_thumb() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, "poster.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs new file mode 100644 index 000000000..4a0bd6c2f --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadata.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Extras.Metadata.Files; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa +{ + public class KometaMetadata : MetadataBase + { + private readonly Logger _logger; + private readonly IMapCoversToLocal _mediaCoverService; + + public KometaMetadata(IMapCoversToLocal mediaCoverService, + Logger logger) + { + _mediaCoverService = mediaCoverService; + _logger = logger; + } + + private static readonly Regex SeriesImagesRegex = new Regex(@"^(?poster)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^Season(?\d{2,})\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex EpisodeImageRegex = new Regex(@"^S(?\d{2,})E(?\d{2,})\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public override string Name => "Kometa"; + + public override string GetFilenameAfterMove(Series series, EpisodeFile episodeFile, MetadataFile metadataFile) + { + if (metadataFile.Type == MetadataType.EpisodeImage) + { + return GetEpisodeImageFilename(series, episodeFile); + } + + _logger.Debug("Unknown episode file metadata: {0}", metadataFile.RelativePath); + return Path.Combine(series.Path, metadataFile.RelativePath); + } + + public override MetadataFile FindMetadataFile(Series series, string path) + { + var filename = Path.GetFileName(path); + + if (filename == null) + { + return null; + } + + var metadata = new MetadataFile + { + SeriesId = series.Id, + Consumer = GetType().Name, + RelativePath = series.Path.GetRelativePath(path) + }; + + if (SeriesImagesRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.SeriesImage; + return metadata; + } + + var seasonMatch = SeasonImagesRegex.Match(filename); + + if (seasonMatch.Success) + { + metadata.Type = MetadataType.SeasonImage; + + var seasonNumberMatch = seasonMatch.Groups["season"].Value; + + if (int.TryParse(seasonNumberMatch, out var seasonNumber)) + { + metadata.SeasonNumber = seasonNumber; + } + else + { + return null; + } + + return metadata; + } + + if (EpisodeImageRegex.IsMatch(filename)) + { + metadata.Type = MetadataType.EpisodeImage; + return metadata; + } + + return null; + } + + public override MetadataFileResult SeriesMetadata(Series series) + { + return null; + } + + public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) + { + return null; + } + + public override List SeriesImages(Series series) + { + if (!Settings.SeriesImages) + { + return new List(); + } + + return ProcessSeriesImages(series).ToList(); + } + + public override List SeasonImages(Series series, Season season) + { + if (!Settings.SeasonImages) + { + return new List(); + } + + return ProcessSeasonImages(series, season).ToList(); + } + + public override List EpisodeImages(Series series, EpisodeFile episodeFile) + { + if (!Settings.EpisodeImages) + { + return new List(); + } + + 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(); + } + + return new List + { + new ImageFileResult(GetEpisodeImageFilename(series, episodeFile), screenshot.RemoteUrl) + }; + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to process episode image for file: {0}", Path.Combine(series.Path, episodeFile.RelativePath)); + + return new List(); + } + } + + private IEnumerable ProcessSeriesImages(Series series) + { + foreach (var image in series.Images) + { + if (image.CoverType == MediaCoverTypes.Poster) + { + var source = _mediaCoverService.GetCoverPath(series.Id, image.CoverType); + var destination = image.CoverType + Path.GetExtension(source); + + yield return new ImageFileResult(destination, source); + } + } + } + + private IEnumerable ProcessSeasonImages(Series series, Season season) + { + foreach (var image in season.Images) + { + if (image.CoverType == MediaCoverTypes.Poster) + { + var filename = string.Format("Season{0:00}.jpg", season.SeasonNumber); + + if (season.SeasonNumber == 0) + { + filename = "Season00.jpg"; + } + + yield return new ImageFileResult(filename, image.RemoteUrl); + } + } + } + + private string GetEpisodeImageFilename(Series series, EpisodeFile episodeFile) + { + var filename = string.Format("S{0:00}E{1:00}.jpg", episodeFile.SeasonNumber, episodeFile.Episodes.Value.FirstOrDefault()?.EpisodeNumber); + return Path.Combine(series.Path, filename); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs new file mode 100644 index 000000000..8b84954f6 --- /dev/null +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Kometa/KometaMetadataSettings.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Extras.Metadata.Consumers.Kometa +{ + public class KometaSettingsValidator : AbstractValidator + { + } + + public class KometaMetadataSettings : IProviderConfig + { + private static readonly KometaSettingsValidator Validator = new KometaSettingsValidator(); + + public KometaMetadataSettings() + { + SeriesImages = true; + SeasonImages = true; + EpisodeImages = true; + } + + [FieldDefinition(0, Label = "MetadataSettingsSeriesImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Poster.jpg")] + public bool SeriesImages { get; set; } + + [FieldDefinition(1, Label = "MetadataSettingsSeasonImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "Season##.jpg")] + public bool SeasonImages { get; set; } + + [FieldDefinition(2, Label = "MetadataSettingsEpisodeImages", Type = FieldType.Checkbox, Section = MetadataSectionType.Image, HelpText = "S##E##.jpg")] + public bool EpisodeImages { get; set; } + + public bool IsValid => true; + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}