From 4dee096c835309b89fe796b63d794a7ce0aceb4c Mon Sep 17 00:00:00 2001 From: Ahmed Al-Taiar Date: Sat, 17 Jan 2026 12:47:13 -0500 Subject: [PATCH] Add audio custom format specs and show scores in import UI Added custom format specs for audio (sample rate, bit depth, bitrate, codec, channels) with validation and UI fields. MediaInfo is now passed to custom format evaluation. ManualImportResource and UI updated to expose and display custom format matches and scores. Improved API validation and upgrade logic to use custom format scores. SizeSpecification validator now allows Max = Min. --- .../Interactive/InteractiveImportRow.css | 6 ++ .../Interactive/InteractiveImportRow.css.d.ts | 1 + .../Interactive/InteractiveImportRow.js | 31 +++++----- .../CustomFormats/CustomFormatController.cs | 40 +++++------- .../ManualImport/ManualImportResource.cs | 7 +++ .../CustomFormatCalculationService.cs | 4 +- .../CustomFormats/CustomFormatInput.cs | 1 + .../AudioBitDepthSpecification.cs | 45 ++++++++++++++ .../AudioBitrateSpecification.cs | 45 ++++++++++++++ .../AudioChannelsSpecification.cs | 45 ++++++++++++++ .../Specifications/AudioCodecSpecification.cs | 61 +++++++++++++++++++ .../AudioSampleRateSpecification.cs | 45 ++++++++++++++ .../Specifications/SizeSpecification.cs | 2 +- .../TrackImport/Manual/ManualImportService.cs | 5 ++ .../Specifications/UpgradeSpecification.cs | 33 ++++++++-- 15 files changed, 324 insertions(+), 47 deletions(-) create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/AudioBitDepthSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/AudioBitrateSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/AudioChannelsSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/AudioCodecSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/AudioSampleRateSpecification.cs diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css index 4c3b27de4..bcbfd1664 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -10,6 +10,12 @@ text-align: center; } +.customFormatScore { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + text-align: center; +} + .label { composes: label from '~Components/Label.css'; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css.d.ts b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css.d.ts index 21de4616c..c5e520d0b 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css.d.ts +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css.d.ts @@ -2,6 +2,7 @@ // Please do not change this file! interface CssExports { 'additionalFile': string; + 'customFormatScore': string; 'customFormatTooltip': string; 'label': string; 'path': string; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index f5395ccbc..b40afed7e 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -19,6 +19,7 @@ import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal'; import formatBytes from 'Utilities/Number/formatBytes'; +import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import translate from 'Utilities/String/translate'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; @@ -185,6 +186,7 @@ class InteractiveImportRow extends Component { releaseGroup, size, customFormats, + customFormatScore, indexerFlags, rejections, columns, @@ -323,23 +325,17 @@ class InteractiveImportRow extends Component { {formatBytes(size)} - - { - customFormats?.length ? - - } - title={translate('Formats')} - body={ -
- -
- } - position={tooltipPositions.LEFT} - /> : - null - } + + } + position={tooltipPositions.LEFT} + /> {isIndexerFlagsColumnVisible ? ( @@ -462,6 +458,7 @@ InteractiveImportRow.propTypes = { quality: PropTypes.object, size: PropTypes.number.isRequired, customFormats: PropTypes.arrayOf(PropTypes.object), + customFormatScore: PropTypes.number.isRequired, indexerFlags: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs b/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs index 153952c37..4f0bf98c0 100644 --- a/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs +++ b/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs @@ -1,14 +1,12 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; -using FluentValidation.Results; using Lidarr.Http; using Lidarr.Http.REST; using Lidarr.Http.REST.Attributes; using Microsoft.AspNetCore.Mvc; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; -using NzbDrone.Core.Validation; namespace Lidarr.Api.V1.CustomFormats { @@ -39,6 +37,22 @@ public CustomFormatController(ICustomFormatService formatService, { context.AddFailure("Condition name(s) cannot be empty or consist of only spaces"); } + + // Validate each specification's internal rules + var model = customFormat.ToModel(_specifications); + for (var i = 0; i < model.Specifications.Count; i++) + { + var spec = model.Specifications[i]; + var specValidationResult = spec.Validate(); + + if (!specValidationResult.IsValid) + { + foreach (var error in specValidationResult.Errors) + { + context.AddFailure($"Specifications[{i}].{error.PropertyName}", error.ErrorMessage); + } + } + } }); } @@ -60,8 +74,6 @@ public ActionResult Create([FromBody] CustomFormatResource { var model = customFormatResource.ToModel(_specifications); - Validate(model); - return Created(_formatService.Insert(model).Id); } @@ -71,8 +83,6 @@ public ActionResult Update([FromBody] CustomFormatResource { var model = resource.ToModel(_specifications); - Validate(model); - _formatService.Update(model); return Accepted(model.Id); @@ -130,24 +140,6 @@ public object GetTemplates() return schema; } - private void Validate(CustomFormat definition) - { - foreach (var validationResult in definition.Specifications.Select(spec => spec.Validate())) - { - VerifyValidationResult(validationResult); - } - } - - private void VerifyValidationResult(ValidationResult validationResult) - { - var result = new NzbDroneValidationResult(validationResult.Errors); - - if (!result.IsValid) - { - throw new ValidationException(result.Errors); - } - } - private IEnumerable GetPresets() { yield return new ReleaseTitleSpecification diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs index b2f70eb3f..662b637ea 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs @@ -2,6 +2,7 @@ using System.Linq; using Lidarr.Api.V1.Albums; using Lidarr.Api.V1.Artist; +using Lidarr.Api.V1.CustomFormats; using Lidarr.Api.V1.Tracks; using Lidarr.Http.REST; using NzbDrone.Core.DecisionEngine; @@ -24,6 +25,8 @@ public class ManualImportResource : RestResource public string ReleaseGroup { get; set; } public int QualityWeight { get; set; } public string DownloadId { get; set; } + public List CustomFormats { get; set; } + public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } public IEnumerable Rejections { get; set; } public ParsedTrackInfo AudioTags { get; set; } @@ -41,6 +44,8 @@ public static ManualImportResource ToResource(this ManualImportItem model) return null; } + var customFormatScore = model.Artist?.QualityProfile?.Value?.CalculateCustomFormatScore(model.CustomFormats) ?? 0; + return new ManualImportResource { Id = model.Id, @@ -56,6 +61,8 @@ public static ManualImportResource ToResource(this ManualImportItem model) // QualityWeight DownloadId = model.DownloadId, + CustomFormats = model.CustomFormats.ToResource(false), + CustomFormatScore = customFormatScore, IndexerFlags = model.IndexerFlags, Rejections = model.Rejections, diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs index 0eb14fef0..87ed194c0 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs @@ -122,6 +122,7 @@ public List ParseCustomFormat(LocalTrack localTrack) Size = localTrack.Size, Filename = Path.GetFileName(localTrack.Path), IndexerFlags = localTrack.IndexerFlags, + MediaInfo = localTrack.FileTrackInfo?.MediaInfo }; return ParseCustomFormat(input); @@ -189,7 +190,8 @@ private List ParseCustomFormat(TrackFile trackFile, Artist artist, Artist = artist, Size = trackFile.Size, IndexerFlags = trackFile.IndexerFlags, - Filename = Path.GetFileName(trackFile.Path) + Filename = Path.GetFileName(trackFile.Path), + MediaInfo = trackFile.MediaInfo }; return ParseCustomFormat(input, allCustomFormats); diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs index f43003b47..0cf975cb5 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs @@ -10,6 +10,7 @@ public class CustomFormatInput public long Size { get; set; } public IndexerFlags IndexerFlags { get; set; } public string Filename { get; set; } + public MediaInfoModel MediaInfo { get; set; } // public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series) // { diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitDepthSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitDepthSpecification.cs new file mode 100644 index 000000000..9b58eac16 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitDepthSpecification.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class AudioBitDepthSpecificationValidator : AbstractValidator + { + public AudioBitDepthSpecificationValidator() + { + RuleFor(c => c.Min).GreaterThanOrEqualTo(0); + RuleFor(c => c.Max).GreaterThanOrEqualTo(c => c.Min); + } + } + + public class AudioBitDepthSpecification : CustomFormatSpecificationBase + { + private static readonly AudioBitDepthSpecificationValidator Validator = new (); + + public override int Order => 10; + public override string ImplementationName => "Audio Bit Depth"; + + [FieldDefinition(1, Label = "Minimum Bit Depth", HelpText = "Minimum bit depth (e.g., 16 for CD quality, 24 for Hi-Res)", Type = FieldType.Number)] + public int Min { get; set; } + + [FieldDefinition(2, Label = "Maximum Bit Depth", HelpText = "Maximum bit depth", Type = FieldType.Number)] + public int Max { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + if (input.MediaInfo == null) + { + return false; + } + + var bitDepth = input.MediaInfo.AudioBits; + return bitDepth >= Min && bitDepth <= Max; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitrateSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitrateSpecification.cs new file mode 100644 index 000000000..9c4f74097 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/AudioBitrateSpecification.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class AudioBitrateSpecificationValidator : AbstractValidator + { + public AudioBitrateSpecificationValidator() + { + RuleFor(c => c.Min).GreaterThanOrEqualTo(0); + RuleFor(c => c.Max).GreaterThanOrEqualTo(c => c.Min); + } + } + + public class AudioBitrateSpecification : CustomFormatSpecificationBase + { + private static readonly AudioBitrateSpecificationValidator Validator = new (); + + public override int Order => 11; + public override string ImplementationName => "Audio Bitrate"; + + [FieldDefinition(1, Label = "Minimum Bitrate", HelpText = "Minimum bitrate in kbps (e.g., 320 for MP3)", Type = FieldType.Number)] + public int Min { get; set; } + + [FieldDefinition(2, Label = "Maximum Bitrate", HelpText = "Maximum bitrate in kbps", Type = FieldType.Number)] + public int Max { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + if (input.MediaInfo == null) + { + return false; + } + + var bitrate = input.MediaInfo.AudioBitrate; + return bitrate >= Min && bitrate <= Max; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/AudioChannelsSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/AudioChannelsSpecification.cs new file mode 100644 index 000000000..e91c726d2 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/AudioChannelsSpecification.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class AudioChannelsSpecificationValidator : AbstractValidator + { + public AudioChannelsSpecificationValidator() + { + RuleFor(c => c.Min).GreaterThanOrEqualTo(0); + RuleFor(c => c.Max).GreaterThanOrEqualTo(c => c.Min); + } + } + + public class AudioChannelsSpecification : CustomFormatSpecificationBase + { + private static readonly AudioChannelsSpecificationValidator Validator = new (); + + public override int Order => 13; + public override string ImplementationName => "Audio Channels"; + + [FieldDefinition(1, Label = "Minimum Channels", HelpText = "Minimum number of audio channels (e.g., 2 for stereo, 6 for 5.1)", Type = FieldType.Number)] + public int Min { get; set; } + + [FieldDefinition(2, Label = "Maximum Channels", HelpText = "Maximum number of audio channels", Type = FieldType.Number)] + public int Max { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + if (input.MediaInfo == null) + { + return false; + } + + var channels = input.MediaInfo.AudioChannels; + return channels >= Min && channels <= Max; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/AudioCodecSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/AudioCodecSpecification.cs new file mode 100644 index 000000000..b3527d448 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/AudioCodecSpecification.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class AudioCodecSpecificationValidator : AbstractValidator + { + public AudioCodecSpecificationValidator() + { + RuleFor(c => c.Value).NotEmpty().WithMessage("Audio Codec must not be empty"); + } + } + + public class AudioCodecSpecification : CustomFormatSpecificationBase + { + private static readonly AudioCodecSpecificationValidator Validator = new (); + + protected Regex _regex; + protected string _raw; + + public override int Order => 12; + public override string ImplementationName => "Audio Codec"; + + [FieldDefinition(1, Label = "Audio Codec", HelpText = "Codec name or regex pattern (e.g., FLAC, MP3, AAC, ALAC, OPUS, WavPack, APE)", Type = FieldType.Textbox)] + public string Value + { + get => _raw; + set + { + _raw = value; + + if (!string.IsNullOrWhiteSpace(value)) + { + _regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + } + } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + if (input.MediaInfo == null || string.IsNullOrWhiteSpace(input.MediaInfo.AudioFormat)) + { + return false; + } + + if (_regex == null) + { + return false; + } + + return _regex.IsMatch(input.MediaInfo.AudioFormat); + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/AudioSampleRateSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/AudioSampleRateSpecification.cs new file mode 100644 index 000000000..af18c07a9 --- /dev/null +++ b/src/NzbDrone.Core/CustomFormats/Specifications/AudioSampleRateSpecification.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.CustomFormats +{ + public class AudioSampleRateSpecificationValidator : AbstractValidator + { + public AudioSampleRateSpecificationValidator() + { + RuleFor(c => c.Min).GreaterThanOrEqualTo(0); + RuleFor(c => c.Max).GreaterThanOrEqualTo(c => c.Min); + } + } + + public class AudioSampleRateSpecification : CustomFormatSpecificationBase + { + private static readonly AudioSampleRateSpecificationValidator Validator = new (); + + public override int Order => 9; + public override string ImplementationName => "Audio Sample Rate"; + + [FieldDefinition(1, Label = "Minimum Sample Rate", HelpText = "Minimum sample rate in Hz (e.g., 44100 for CD quality, 192000 for Hi-Res)", Type = FieldType.Number)] + public int Min { get; set; } + + [FieldDefinition(2, Label = "Maximum Sample Rate", HelpText = "Maximum sample rate in Hz", Type = FieldType.Number)] + public int Max { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input) + { + if (input.MediaInfo == null) + { + return false; + } + + var sampleRate = input.MediaInfo.AudioSampleRate; + return sampleRate >= Min && sampleRate <= Max; + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs index 9e2fe766e..45fe3bdbf 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs @@ -10,7 +10,7 @@ public class SizeSpecificationValidator : AbstractValidator public SizeSpecificationValidator() { RuleFor(c => c.Min).GreaterThanOrEqualTo(0); - RuleFor(c => c.Max).GreaterThan(c => c.Min); + RuleFor(c => c.Max).GreaterThanOrEqualTo(c => c.Min); RuleFor(c => c.Max).LessThanOrEqualTo(double.MaxValue); } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index fb35fa4a8..3adc61435 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -304,6 +304,11 @@ public List UpdateItems(List items) item.Rejections = decision.Rejections; item.Size = decision.Item.Size; + if (decision.Item.Artist != null) + { + item.CustomFormats = _formatCalculator.ParseCustomFormat(decision.Item); + } + result.Add(item); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs index 268f627a9..641aad5ba 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs @@ -34,6 +34,11 @@ public Decision IsSatisfiedBy(LocalTrack localTrack, DownloadClientItem download var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; var qualityComparer = new QualityModelComparer(localTrack.Artist.QualityProfile); + var qualityProfile = localTrack.Artist.QualityProfile.Value; + + // Calculate custom formats for the new track + var newCustomFormats = _customFormatCalculationService.ParseCustomFormat(localTrack); + var newFormatScore = qualityProfile.CalculateCustomFormatScore(newCustomFormats); foreach (var track in localTrack.Tracks.Where(e => e.TrackFileId > 0)) { @@ -53,11 +58,31 @@ public Decision IsSatisfiedBy(LocalTrack localTrack, DownloadClientItem download return Decision.Reject("Not an upgrade for existing track file(s). New Quality is {0}", localTrack.Quality.Quality); } - if (qualityCompare == 0 && downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && - localTrack.Quality.Revision.CompareTo(trackFile.Quality.Revision) < 0) + // When quality is equal, compare custom format scores + if (qualityCompare == 0) { - _logger.Debug("This file isn't a quality upgrade for all tracks. Skipping {0}", localTrack.Path); - return Decision.Reject("Not an upgrade for existing track file(s)"); + var existingCustomFormats = _customFormatCalculationService.ParseCustomFormat(trackFile, localTrack.Artist); + var existingFormatScore = qualityProfile.CalculateCustomFormatScore(existingCustomFormats); + + if (newFormatScore < existingFormatScore) + { + _logger.Debug("This file isn't a custom format upgrade. Existing: {0} [{1}], New: {2} [{3}]. Skipping {4}", + existingFormatScore, + string.Join(", ", existingCustomFormats.Select(x => x.Name)), + newFormatScore, + string.Join(", ", newCustomFormats.Select(x => x.Name)), + localTrack.Path); + return Decision.Reject("Not a custom format upgrade for existing track file(s). Existing score: {0}, New score: {1}", + existingFormatScore, + newFormatScore); + } + + if (downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && + localTrack.Quality.Revision.CompareTo(trackFile.Quality.Revision) < 0) + { + _logger.Debug("This file isn't a quality upgrade for all tracks. Skipping {0}", localTrack.Path); + return Decision.Reject("Not an upgrade for existing track file(s)"); + } } }