diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs new file mode 100644 index 000000000..9010ba27d --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/203_release_typeFixture.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class release_typeFixture : MigrationTest + { + [Test] + public void should_convert_single_episode_without_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode); + } + + [Test] + public void should_convert_single_episode_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05.720p.HDTV.x265-Sonarr/S01E05.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SingleEpisode); + } + + [Test] + public void should_convert_multi_episode_without_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode); + } + + [Test] + public void should_convert_multi_episode_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01E05E06.720p.HDTV.x265-Sonarr/S01E05E06.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.MultiEpisode); + } + + [Test] + public void should_convert_season_pack_with_folder() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + OriginalFilePath = "Series.Title.S01.720p.HDTV.x265-Sonarr/S01E05.mkv", + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.SeasonPack); + } + + [Test] + public void should_not_convert_episode_without_original_file_path() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("EpisodeFiles").Row(new + { + SeriesId = 1, + SeasonNumber = 1, + RelativePath = "Season 01/S01E05.mkv", + Size = 125.Megabytes(), + DateAdded = DateTime.UtcNow.AddDays(-5), + ReleaseGroup = "Sonarr", + Quality = new QualityModel(Quality.HDTV720p).ToJson(), + Languages = "[1]" + }); + }); + + var items = db.Query("SELECT * FROM \"EpisodeFiles\""); + + items.Should().HaveCount(1); + + items.First().ReleaseType.Should().Be((int)ReleaseType.Unknown); + } + + public class EpisodeFile203 + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int SeasonNumber { get; set; } + public string RelativePath { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + public string OriginalFilePath { get; set; } + public string SceneName { get; set; } + public string ReleaseGroup { get; set; } + public QualityModel Quality { get; set; } + public long IndexerFlags { get; set; } + public MediaInfoModel MediaInfo { get; set; } + public List Languages { get; set; } + public long ReleaseType { get; set; } + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs index 9020601ff..179cf5b3f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/ImportApprovedEpisodesFixture.cs @@ -49,6 +49,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); _rejectedDecisions.Add(new ImportDecision(new LocalEpisode(), new Rejection("Rejected!"))); + _rejectedDecisions.ForEach(r => r.LocalEpisode.FileEpisodeInfo = new ParsedEpisodeInfo()); foreach (var episode in episodes) { @@ -59,7 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport Episodes = new List { episode }, Path = Path.Combine(series.Path, "30 Rock - S01E01 - Pilot.avi"), Quality = new QualityModel(Quality.Bluray720p), - ReleaseGroup = "DRONE" + ReleaseGroup = "DRONE", + FileEpisodeInfo = new ParsedEpisodeInfo() })); } diff --git a/src/NzbDrone.Core/Blocklisting/Blocklist.cs b/src/NzbDrone.Core/Blocklisting/Blocklist.cs index 4fdc4f24c..8941f42fd 100644 --- a/src/NzbDrone.Core/Blocklisting/Blocklist.cs +++ b/src/NzbDrone.Core/Blocklisting/Blocklist.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Blocklisting public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } public IndexerFlags IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } public List Languages { get; set; } diff --git a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs index 0015f7ea2..0ec53522c 100644 --- a/src/NzbDrone.Core/Blocklisting/BlocklistService.cs +++ b/src/NzbDrone.Core/Blocklisting/BlocklistService.cs @@ -194,6 +194,11 @@ namespace NzbDrone.Core.Blocklisting blocklist.IndexerFlags = flags; } + if (Enum.TryParse(message.Data.GetValueOrDefault("releaseType"), true, out ReleaseType releaseType)) + { + blocklist.ReleaseType = releaseType; + } + _blocklistRepository.Insert(blocklist); } diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index c07db977e..1f0cb296b 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -41,7 +41,8 @@ namespace NzbDrone.Core.CustomFormats Series = remoteEpisode.Series, Size = size, Languages = remoteEpisode.Languages, - IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0 + IndexerFlags = remoteEpisode.Release?.IndexerFlags ?? 0, + ReleaseType = remoteEpisode.ParsedEpisodeInfo.ReleaseType }; return ParseCustomFormat(input); @@ -76,7 +77,8 @@ namespace NzbDrone.Core.CustomFormats Series = series, Size = blocklist.Size ?? 0, Languages = blocklist.Languages, - IndexerFlags = blocklist.IndexerFlags + IndexerFlags = blocklist.IndexerFlags, + ReleaseType = blocklist.ReleaseType }; return ParseCustomFormat(input); @@ -88,6 +90,7 @@ namespace NzbDrone.Core.CustomFormats long.TryParse(history.Data.GetValueOrDefault("size"), out var size); Enum.TryParse(history.Data.GetValueOrDefault("indexerFlags"), true, out IndexerFlags indexerFlags); + Enum.TryParse(history.Data.GetValueOrDefault("releaseType"), out ReleaseType releaseType); var episodeInfo = new ParsedEpisodeInfo { @@ -104,7 +107,8 @@ namespace NzbDrone.Core.CustomFormats Series = series, Size = size, Languages = history.Languages, - IndexerFlags = indexerFlags + IndexerFlags = indexerFlags, + ReleaseType = releaseType }; return ParseCustomFormat(input); @@ -128,6 +132,7 @@ namespace NzbDrone.Core.CustomFormats Size = localEpisode.Size, Languages = localEpisode.Languages, IndexerFlags = localEpisode.IndexerFlags, + ReleaseType = localEpisode.ReleaseType, Filename = Path.GetFileName(localEpisode.Path) }; @@ -188,7 +193,7 @@ namespace NzbDrone.Core.CustomFormats ReleaseTitle = releaseTitle, Quality = episodeFile.Quality, Languages = episodeFile.Languages, - ReleaseGroup = episodeFile.ReleaseGroup + ReleaseGroup = episodeFile.ReleaseGroup, }; var input = new CustomFormatInput @@ -198,7 +203,8 @@ namespace NzbDrone.Core.CustomFormats Size = episodeFile.Size, Languages = episodeFile.Languages, IndexerFlags = episodeFile.IndexerFlags, - Filename = Path.GetFileName(episodeFile.RelativePath) + ReleaseType = episodeFile.ReleaseType, + Filename = Path.GetFileName(episodeFile.RelativePath), }; return ParseCustomFormat(input, allCustomFormats); diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs index e202ffccf..465fbfef5 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.CustomFormats public IndexerFlags IndexerFlags { get; set; } public List Languages { get; set; } public string Filename { get; set; } + public ReleaseType ReleaseType { get; set; } public CustomFormatInput() { diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs index 56f73f8b9..3eaeeb5f6 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/IndexerFlagSpecification.cs @@ -11,11 +11,11 @@ namespace NzbDrone.Core.CustomFormats public IndexerFlagSpecificationValidator() { RuleFor(c => c.Value).NotEmpty(); - RuleFor(c => c.Value).Custom((qualityValue, context) => + RuleFor(c => c.Value).Custom((flag, context) => { - if (!Enum.IsDefined(typeof(IndexerFlags), qualityValue)) + if (!Enum.IsDefined(typeof(IndexerFlags), flag)) { - context.AddFailure($"Invalid indexer flag condition value: {qualityValue}"); + context.AddFailure($"Invalid indexer flag condition value: {flag}"); } }); } diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs new file mode 100644 index 000000000..acc6d9c4d --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs @@ -0,0 +1,43 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class SeasonPackSpecificationValidator : AbstractValidator + { + public SeasonPackSpecificationValidator() + { + RuleFor(c => c.Value).Custom((releaseType, context) => + { + if (!Enum.IsDefined(typeof(ReleaseType), releaseType)) + { + context.AddFailure($"Invalid release type condition value: {releaseType}"); + } + }); + } + } + + public class SeasonPackSpecification : CustomFormatSpecificationBase + { + private static readonly SeasonPackSpecificationValidator Validator = new (); + + public override int Order => 10; + public override string ImplementationName => "Release Type"; + + [FieldDefinition(1, Label = "ReleaseType", Type = FieldType.Select, SelectOptions = typeof(ReleaseType))] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + return input.ReleaseType == (ReleaseType)Value; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs b/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs new file mode 100644 index 000000000..cca7a7fa5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/203_release_type.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Data; +using System.IO; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(203)] + public class release_type : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blocklist").AddColumn("ReleaseType").AsInt32().WithDefaultValue(0); + Alter.Table("EpisodeFiles").AddColumn("ReleaseType").AsInt32().WithDefaultValue(0); + + Execute.WithConnection(UpdateEpisodeFiles); + } + + private void UpdateEpisodeFiles(IDbConnection conn, IDbTransaction tran) + { + var updates = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"OriginalFilePath\" FROM \"EpisodeFiles\" WHERE \"OriginalFilePath\" IS NOT NULL"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var id = reader.GetInt32(0); + var originalFilePath = reader.GetString(1); + + var folderName = Path.GetDirectoryName(originalFilePath); + var fileName = Path.GetFileNameWithoutExtension(originalFilePath); + var title = folderName.IsNullOrWhiteSpace() ? fileName : folderName; + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + + if (parsedEpisodeInfo != null && parsedEpisodeInfo.ReleaseType != ReleaseType.Unknown) + { + updates.Add(new + { + Id = id, + ReleaseType = (int)parsedEpisodeInfo.ReleaseType + }); + } + } + } + + var updateEpisodeFilesSql = "UPDATE \"EpisodeFiles\" SET \"ReleaseType\" = @ReleaseType WHERE \"Id\" = @Id"; + conn.Execute(updateEpisodeFilesSql, updates, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index df2788762..d7e76d94f 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -170,6 +170,7 @@ namespace NzbDrone.Core.History history.Data.Add("SeriesMatchType", message.Episode.SeriesMatchType.ToString()); history.Data.Add("ReleaseSource", message.Episode.ReleaseSource.ToString()); history.Data.Add("IndexerFlags", message.Episode.Release.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.Episode.ParsedEpisodeInfo.ReleaseType.ToString()); if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) { @@ -222,6 +223,7 @@ namespace NzbDrone.Core.History history.Data.Add("CustomFormatScore", message.EpisodeInfo.CustomFormatScore.ToString()); history.Data.Add("Size", message.EpisodeInfo.Size.ToString()); history.Data.Add("IndexerFlags", message.ImportedEpisode.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.ImportedEpisode.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -283,6 +285,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.EpisodeFile.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -315,6 +318,7 @@ namespace NzbDrone.Core.History history.Data.Add("ReleaseGroup", message.EpisodeFile.ReleaseGroup); history.Data.Add("Size", message.EpisodeFile.Size.ToString()); history.Data.Add("IndexerFlags", message.EpisodeFile.IndexerFlags.ToString()); + history.Data.Add("ReleaseType", message.EpisodeFile.ReleaseType.ToString()); _historyRepository.Insert(history); } @@ -343,6 +347,7 @@ namespace NzbDrone.Core.History history.Data.Add("Message", message.Message); history.Data.Add("ReleaseGroup", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseGroup); history.Data.Add("Size", message.TrackedDownload?.DownloadItem.TotalSize.ToString()); + history.Data.Add("ReleaseType", message.TrackedDownload?.RemoteEpisode?.ParsedEpisodeInfo?.ReleaseType.ToString()); historyToAdd.Add(history); } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 90de21e06..40cc2f581 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1597,6 +1597,7 @@ "ReleaseSceneIndicatorUnknownMessage": "Numbering varies for this episode and release does not match any known mappings.", "ReleaseSceneIndicatorUnknownSeries": "Unknown episode or series.", "ReleaseTitle": "Release Title", + "ReleaseType": "Release Type", "Reload": "Reload", "RemotePath": "Remote Path", "RemotePathMappingBadDockerPathHealthCheckMessage": "You are using docker; download client {downloadClientName} places downloads in {path} but this is not a valid {osName} path. Review your remote path mappings and download client settings.", diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index a02fdd44a..8dee12c2b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -27,6 +27,7 @@ namespace NzbDrone.Core.MediaFiles public LazyLoaded> Episodes { get; set; } public LazyLoaded Series { get; set; } public List Languages { get; set; } + public ReleaseType ReleaseType { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index e4650746d..74e2a71e6 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -97,6 +97,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; episodeFile.Languages = localEpisode.Languages; + // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. + episodeFile.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo.ReleaseType; + if (downloadClientItem?.DownloadId.IsNotNullOrWhiteSpace() == true) { var grabHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId) @@ -107,12 +112,27 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { episodeFile.IndexerFlags = flags; } + + // Prefer the release type from the grabbed history + if (Enum.TryParse(grabHistory?.Data.GetValueOrDefault("releaseType"), true, out ReleaseType releaseType)) + { + episodeFile.ReleaseType = releaseType; + } } else { episodeFile.IndexerFlags = localEpisode.IndexerFlags; } + // Fall back to parsed information if history is unavailable or missing + if (episodeFile.ReleaseType == ReleaseType.Unknown) + { + // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. + episodeFile.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo.ReleaseType; + } + bool copyOnly; switch (importMode) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index d5164f1b1..c3f07a0a5 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -119,6 +119,10 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport localEpisode.FileEpisodeInfo = fileEpisodeInfo; localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); + localEpisode.ReleaseType = localEpisode.DownloadClientEpisodeInfo?.ReleaseType ?? + localEpisode.FolderEpisodeInfo?.ReleaseType ?? + localEpisode.FileEpisodeInfo?.ReleaseType ?? + ReleaseType.Unknown; try { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs index f4daed6e8..365d0ae31 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportFile.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual @@ -17,6 +18,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List Languages { get; set; } public string ReleaseGroup { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public string DownloadId { get; set; } public bool Equals(ManualImportFile other) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs index 703428535..9f690474b 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportItem.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -25,6 +26,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public IEnumerable Rejections { get; set; } public ManualImportItem() diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index c327a1418..58af3323d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -425,6 +425,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Size = _diskProvider.GetFileSize(decision.LocalEpisode.Path); item.Rejections = decision.Rejections; item.IndexerFlags = (int)decision.LocalEpisode.IndexerFlags; + item.ReleaseType = decision.LocalEpisode.ReleaseType; return item; } @@ -444,6 +445,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual item.Quality = episodeFile.Quality; item.Languages = episodeFile.Languages; item.IndexerFlags = (int)episodeFile.IndexerFlags; + item.ReleaseType = episodeFile.ReleaseType; item.Size = _diskProvider.GetFileSize(item.Path); item.Rejections = Enumerable.Empty(); item.EpisodeFileId = episodeFile.Id; @@ -481,6 +483,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual Quality = file.Quality, Languages = file.Languages, IndexerFlags = (IndexerFlags)file.IndexerFlags, + ReleaseType = file.ReleaseType, Series = series, Size = 0 }; @@ -510,6 +513,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.Quality = file.Quality; localEpisode.Languages = file.Languages; localEpisode.IndexerFlags = (IndexerFlags)file.IndexerFlags; + localEpisode.ReleaseType = file.ReleaseType; // TODO: Cleanup non-tracked downloads diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 2ca924779..af7c7347c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Parser.Model public QualityModel Quality { get; set; } public List Languages { get; set; } public IndexerFlags IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } public bool SceneSource { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 3833199bb..3f4eeee25 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -90,6 +90,29 @@ namespace NzbDrone.Core.Parser.Model } } + public ReleaseType ReleaseType + { + get + { + if (EpisodeNumbers.Length > 1 || AbsoluteEpisodeNumbers.Length > 1) + { + return Model.ReleaseType.MultiEpisode; + } + + if (EpisodeNumbers.Length == 1 || AbsoluteEpisodeNumbers.Length == 1) + { + return Model.ReleaseType.SingleEpisode; + } + + if (FullSeason) + { + return Model.ReleaseType.SeasonPack; + } + + return Model.ReleaseType.Unknown; + } + } + public override string ToString() { var episodeString = "[Unknown Episode]"; diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseType.cs b/src/NzbDrone.Core/Parser/Model/ReleaseType.cs new file mode 100644 index 000000000..75d44c424 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/ReleaseType.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Parser.Model +{ + public enum ReleaseType + { + Unknown = 0, + + [FieldOption(label: "Single Episode")] + SingleEpisode = 1, + + [FieldOption(label: "Multi-Episode")] + MultiEpisode = 2, + + [FieldOption(label: "Season Pack")] + SeasonPack = 3 + } +} diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs index a2bdbbe41..b1e9dd1fb 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileController.cs @@ -209,6 +209,11 @@ namespace Sonarr.Api.V3.EpisodeFiles { episodeFile.IndexerFlags = (IndexerFlags)resourceEpisodeFile.IndexerFlags; } + + if (resourceEpisodeFile.ReleaseType != null) + { + episodeFile.ReleaseType = (ReleaseType)resourceEpisodeFile.ReleaseType; + } } _mediaFileService.Update(episodeFiles); diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index 0b6adcdc1..d77338ea3 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -26,6 +26,7 @@ namespace Sonarr.Api.V3.EpisodeFiles public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int? IndexerFlags { get; set; } + public int? ReleaseType { get; set; } public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -33,34 +34,6 @@ namespace Sonarr.Api.V3.EpisodeFiles public static class EpisodeFileResourceMapper { - private static EpisodeFileResource ToResource(this EpisodeFile model) - { - if (model == null) - { - return null; - } - - return new EpisodeFileResource - { - Id = model.Id, - - SeriesId = model.SeriesId, - SeasonNumber = model.SeasonNumber, - RelativePath = model.RelativePath, - - // Path - Size = model.Size, - DateAdded = model.DateAdded, - SceneName = model.SceneName, - ReleaseGroup = model.ReleaseGroup, - Languages = model.Languages, - Quality = model.Quality, - MediaInfo = model.MediaInfo.ToResource(model.SceneName) - - // QualityCutoffNotMet - }; - } - public static EpisodeFileResource ToResource(this EpisodeFile model, NzbDrone.Core.Tv.Series series, IUpgradableSpecification upgradableSpecification, ICustomFormatCalculationService formatCalculationService) { if (model == null) @@ -90,7 +63,8 @@ namespace Sonarr.Api.V3.EpisodeFiles QualityCutoffNotMet = upgradableSpecification.QualityCutoffNotMet(series.QualityProfile.Value, model.Quality), CustomFormats = customFormats.ToResource(false), CustomFormatScore = customFormatScore, - IndexerFlags = (int)model.IndexerFlags + IndexerFlags = (int)model.IndexerFlags, + ReleaseType = (int)model.ReleaseType, }; } } diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs index 0e47dcd60..a65e6bdf3 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -4,6 +4,7 @@ using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.Episodes; @@ -31,6 +32,7 @@ namespace Sonarr.Api.V3.ManualImport public List CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public IEnumerable Rejections { get; set; } } @@ -67,6 +69,7 @@ namespace Sonarr.Api.V3.ManualImport // QualityWeight DownloadId = model.DownloadId, IndexerFlags = model.IndexerFlags, + ReleaseType = model.ReleaseType, Rejections = model.Rejections }; }