mirror of
https://github.com/fergalmoran/Readarr.git
synced 2026-03-11 07:57:47 +00:00
New: Custom Formats
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.CustomFormats
|
||||
{
|
||||
public class CustomFormatsTestHelpers : CoreTest
|
||||
{
|
||||
private static List<CustomFormat> _customFormats { get; set; }
|
||||
|
||||
public static void GivenCustomFormats(params CustomFormat[] formats)
|
||||
{
|
||||
_customFormats = formats.ToList();
|
||||
}
|
||||
|
||||
public static List<ProfileFormatItem> GetSampleFormatItems(params string[] allowed)
|
||||
{
|
||||
var allowedItems = _customFormats.Where(x => allowed.Contains(x.Name)).Select((f, index) => new ProfileFormatItem { Format = f, Score = (int)Math.Pow(2, index) }).ToList();
|
||||
var disallowedItems = _customFormats.Where(x => !allowed.Contains(x.Name)).Select(f => new ProfileFormatItem { Format = f, Score = -1 * (int)Math.Pow(2, allowedItems.Count) });
|
||||
|
||||
return disallowedItems.Concat(allowedItems).ToList();
|
||||
}
|
||||
|
||||
public static List<ProfileFormatItem> GetDefaultFormatItems()
|
||||
{
|
||||
return new List<ProfileFormatItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
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.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||
{
|
||||
[TestFixture]
|
||||
public class add_custom_formatsFixture : MigrationTest<add_custom_formats>
|
||||
{
|
||||
[Test]
|
||||
public void should_add_cf_from_named_release_profile()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().Name.Should().Be("Unnamed_1");
|
||||
customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
|
||||
customFormats.First().Specifications.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_migrate_if_bad_regex_in_release_profile()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "[somestring[",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = true,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_cf_naming_token_if_set_in_release_profile()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = true,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().Name.Should().Be("Unnamed_1");
|
||||
customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeTrue();
|
||||
customFormats.First().Specifications.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_remove_release_profile_if_ignored_or_required()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "some",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = true,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var releaseProfiles = db.Query<ReleaseProfile026>("SELECT \"Id\" FROM \"ReleaseProfiles\"");
|
||||
|
||||
releaseProfiles.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_remove_release_profile_if_no_ignored_or_required()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = true,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var releaseProfiles = db.Query<ReleaseProfile026>("SELECT \"Id\" FROM \"ReleaseProfiles\"");
|
||||
|
||||
releaseProfiles.Should().HaveCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_cf_from_unnamed_release_profile()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().Name.Should().Be("Unnamed_1");
|
||||
customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
|
||||
customFormats.First().Specifications.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_cfs_from_multiple_unnamed_release_profile()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x265",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(2);
|
||||
customFormats.First().Name.Should().Be("Unnamed_1");
|
||||
customFormats.Last().Name.Should().Be("Unnamed_2");
|
||||
customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
|
||||
customFormats.First().Specifications.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_two_cfs_if_release_profile_has_multiple_terms()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
Key = "x265",
|
||||
Value = 5
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<CustomFormat026>("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
|
||||
|
||||
customFormats.Should().HaveCount(2);
|
||||
customFormats.First().Name.Should().Be("Unnamed_1_0");
|
||||
customFormats.Last().Name.Should().Be("Unnamed_1_1");
|
||||
customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
|
||||
customFormats.First().Specifications.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_scores_for_enabled_release_profiles()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = true,
|
||||
IndexerId = 0
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("QualityProfiles").Row(new
|
||||
{
|
||||
Name = "SDTV",
|
||||
Cutoff = 1,
|
||||
Items = "[ { \"quality\": 1, \"allowed\": true } ]"
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<QualityProfile026>("SELECT \"Id\", \"Name\", \"FormatItems\" FROM \"QualityProfiles\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().FormatItems.Should().HaveCount(1);
|
||||
customFormats.First().FormatItems.First().Score.Should().Be(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_set_zero_scores_for_disabled_release_profiles()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("ReleaseProfiles").Row(new
|
||||
{
|
||||
Preferred = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
Key = "x264",
|
||||
Value = 2
|
||||
}
|
||||
}.ToJson(),
|
||||
Required = "",
|
||||
Ignored = "",
|
||||
Tags = "[]",
|
||||
IncludePreferredWhenRenaming = false,
|
||||
Enabled = false,
|
||||
IndexerId = 0
|
||||
});
|
||||
|
||||
c.Insert.IntoTable("QualityProfiles").Row(new
|
||||
{
|
||||
Name = "SDTV",
|
||||
Cutoff = 1,
|
||||
Items = "[ { \"quality\": 1, \"allowed\": true } ]"
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<QualityProfile026>("SELECT \"Id\", \"Name\", \"FormatItems\" FROM \"QualityProfiles\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().FormatItems.Should().HaveCount(1);
|
||||
customFormats.First().FormatItems.First().Score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_migrate_naming_configs()
|
||||
{
|
||||
var db = WithMigrationTestDb(c =>
|
||||
{
|
||||
c.Insert.IntoTable("NamingConfig").Row(new
|
||||
{
|
||||
ReplaceIllegalCharacters = false,
|
||||
StandardBookFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Preferred Words } {Quality Full}",
|
||||
});
|
||||
});
|
||||
|
||||
var customFormats = db.Query<NamingConfig026>("SELECT \"StandardBookFormat\" FROM \"NamingConfig\"");
|
||||
|
||||
customFormats.Should().HaveCount(1);
|
||||
customFormats.First().StandardBookFormat.Should().Be("{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Custom Formats } {Quality Full}");
|
||||
}
|
||||
|
||||
private class NamingConfig026
|
||||
{
|
||||
public string StandardBookFormat { get; set; }
|
||||
}
|
||||
|
||||
private class ReleaseProfile026
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
private class QualityProfile026
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public List<FormatItem026> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class FormatItem026
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormat026
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool IncludeCustomFormatWhenRenaming { get; set; }
|
||||
public List<CustomFormatSpec026> Specifications { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormatSpec026
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public CustomFormatReleaseTitleSpec026 Body { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormatReleaseTitleSpec026
|
||||
{
|
||||
public int Order { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Negate { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class CustomFormatAllowedByProfileSpecificationFixture : CoreTest<CustomFormatAllowedbyProfileSpecification>
|
||||
{
|
||||
private RemoteBook _remoteAlbum;
|
||||
|
||||
private CustomFormat _format1;
|
||||
private CustomFormat _format2;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_format1 = new CustomFormat("Awesome Format");
|
||||
_format1.Id = 1;
|
||||
|
||||
_format2 = new CustomFormat("Cool Format");
|
||||
_format2.Id = 2;
|
||||
|
||||
var fakeArtist = Builder<Author>.CreateNew()
|
||||
.With(c => c.QualityProfile = new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
MinFormatScore = 1
|
||||
})
|
||||
.Build();
|
||||
|
||||
_remoteAlbum = new RemoteBook
|
||||
{
|
||||
Author = fakeArtist,
|
||||
ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
};
|
||||
|
||||
CustomFormatsTestHelpers.GivenCustomFormats(_format1, _format2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_allow_if_format_score_greater_than_min()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { _format1 };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_format_score_not_greater_than_min()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { _format2 };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Console.WriteLine(_remoteAlbum.CustomFormatScore);
|
||||
Console.WriteLine(_remoteAlbum.Author.QualityProfile.Value.MinFormatScore);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_format_score_not_greater_than_min_2()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { _format2, _format1 };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_allow_if_all_format_is_defined_in_profile()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { _format2, _format1 };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_no_format_was_parsed_and_min_score_positive()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_allow_if_no_format_was_parsed_min_score_is_zero()
|
||||
{
|
||||
_remoteAlbum.CustomFormats = new List<CustomFormat> { };
|
||||
_remoteAlbum.Author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteAlbum.Author.QualityProfile.Value.MinFormatScore = 0;
|
||||
_remoteAlbum.CustomFormatScore = _remoteAlbum.Author.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -11,8 +12,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestFixture]
|
||||
public class CutoffSpecificationFixture : CoreTest<UpgradableSpecification>
|
||||
{
|
||||
private static readonly int NoPreferredWordScore = 0;
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_current_book_is_less_than_cutoff()
|
||||
{
|
||||
@@ -20,10 +19,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
new List<QualityModel> { new QualityModel(Quality.Unknown, new Revision(version: 2)) },
|
||||
NoPreferredWordScore).Should().BeTrue();
|
||||
new List<CustomFormat>()).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -33,10 +33,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
new List<QualityModel> { new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
NoPreferredWordScore).Should().BeFalse();
|
||||
new List<CustomFormat>()).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -46,10 +47,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.AZW3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
new List<QualityModel> { new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
NoPreferredWordScore).Should().BeFalse();
|
||||
new List<CustomFormat>()).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -59,10 +61,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
new List<QualityModel> { new QualityModel(Quality.MP3, new Revision(version: 1)) },
|
||||
NoPreferredWordScore,
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3, new Revision(version: 2))).Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -73,30 +76,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
new List<QualityModel> { new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
NoPreferredWordScore,
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_cutoffs_are_met_and_score_is_higher()
|
||||
{
|
||||
QualityProfile profile = new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
};
|
||||
|
||||
Subject.CutoffNotMet(
|
||||
profile,
|
||||
new List<QualityModel> { new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
NoPreferredWordScore,
|
||||
new QualityModel(Quality.FLAC, new Revision(version: 2)),
|
||||
10).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade()
|
||||
{
|
||||
@@ -104,14 +91,31 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
};
|
||||
|
||||
Subject.CutoffNotMet(
|
||||
profile,
|
||||
new List<QualityModel> { new QualityModel(Quality.FLAC, new Revision(version: 1)) },
|
||||
NoPreferredWordScore,
|
||||
new QualityModel(Quality.FLAC, new Revision(version: 2)),
|
||||
NoPreferredWordScore).Should().BeTrue();
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_cutoff_is_set_to_highest_quality()
|
||||
{
|
||||
QualityProfile profile = new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = false
|
||||
};
|
||||
|
||||
Subject.CutoffNotMet(
|
||||
profile,
|
||||
new List<QualityModel> { new QualityModel(Quality.Unknown, new Revision(version: 1)) },
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3, new Revision(version: 2))).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,15 +416,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBook1 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC));
|
||||
var remoteBook2 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC));
|
||||
|
||||
remoteBook1.PreferredWordScore = 10;
|
||||
remoteBook2.PreferredWordScore = 0;
|
||||
remoteBook1.CustomFormatScore = 10;
|
||||
remoteBook2.CustomFormatScore = 0;
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook1));
|
||||
decisions.Add(new DownloadDecision(remoteBook2));
|
||||
|
||||
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||
qualifiedReports.First().RemoteBook.PreferredWordScore.Should().Be(10);
|
||||
qualifiedReports.First().RemoteBook.CustomFormatScore.Should().Be(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -437,8 +437,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBook1 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
|
||||
var remoteBook2 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
|
||||
|
||||
remoteBook1.PreferredWordScore = 10;
|
||||
remoteBook2.PreferredWordScore = 0;
|
||||
remoteBook1.CustomFormatScore = 10;
|
||||
remoteBook2.CustomFormatScore = 0;
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook1));
|
||||
@@ -458,8 +458,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBook1 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
|
||||
var remoteBook2 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
|
||||
|
||||
remoteBook1.PreferredWordScore = 10;
|
||||
remoteBook2.PreferredWordScore = 0;
|
||||
remoteBook1.CustomFormatScore = 10;
|
||||
remoteBook2.CustomFormatScore = 0;
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook1));
|
||||
@@ -479,8 +479,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBook1 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
|
||||
var remoteBook2 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
|
||||
|
||||
remoteBook1.PreferredWordScore = 10;
|
||||
remoteBook2.PreferredWordScore = 0;
|
||||
remoteBook1.CustomFormatScore = 10;
|
||||
remoteBook2.CustomFormatScore = 0;
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook1));
|
||||
@@ -489,7 +489,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||
qualifiedReports.First().RemoteBook.ParsedBookInfo.Quality.Quality.Should().Be(Quality.FLAC);
|
||||
qualifiedReports.First().RemoteBook.ParsedBookInfo.Quality.Revision.Version.Should().Be(1);
|
||||
qualifiedReports.First().RemoteBook.PreferredWordScore.Should().Be(10);
|
||||
qualifiedReports.First().RemoteBook.CustomFormatScore.Should().Be(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -536,8 +536,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBook1 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(1, 0)));
|
||||
var remoteBook2 = GivenRemoteBook(new List<Book> { GivenBook(1) }, new QualityModel(Quality.FLAC, new Revision(1, 1)));
|
||||
|
||||
remoteBook1.PreferredWordScore = 10;
|
||||
remoteBook2.PreferredWordScore = 0;
|
||||
remoteBook1.CustomFormatScore = 10;
|
||||
remoteBook2.CustomFormatScore = 0;
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook1));
|
||||
@@ -548,7 +548,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
qualifiedReports.First().RemoteBook.ParsedBookInfo.Quality.Quality.Should().Be(Quality.FLAC);
|
||||
qualifiedReports.First().RemoteBook.ParsedBookInfo.Quality.Revision.Version.Should().Be(1);
|
||||
qualifiedReports.First().RemoteBook.ParsedBookInfo.Quality.Revision.Real.Should().Be(0);
|
||||
qualifiedReports.First().RemoteBook.PreferredWordScore.Should().Be(10);
|
||||
qualifiedReports.First().RemoteBook.CustomFormatScore.Should().Be(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Queue;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
@@ -31,11 +34,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
|
||||
CustomFormatsTestHelpers.GivenCustomFormats();
|
||||
|
||||
_author = Builder<Author>.CreateNew()
|
||||
.With(e => e.QualityProfile = new QualityProfile
|
||||
{
|
||||
UpgradeAllowed = true,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(),
|
||||
MinFormatScore = 0
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -59,8 +66,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.With(r => r.Author = _author)
|
||||
.With(r => r.Books = new List<Book> { _book })
|
||||
.With(r => r.ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3) })
|
||||
.With(r => r.PreferredWordScore = 0)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
.Setup(x => x.ParseCustomFormat(It.IsAny<RemoteBook>(), It.IsAny<long>()))
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void GivenEmptyQueue()
|
||||
@@ -70,6 +81,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.Returns(new List<Queue.Queue>());
|
||||
}
|
||||
|
||||
private void GivenQueueFormats(List<CustomFormat> formats)
|
||||
{
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
.Setup(x => x.ParseCustomFormat(It.IsAny<RemoteBook>(), It.IsAny<long>()))
|
||||
.Returns(formats);
|
||||
}
|
||||
|
||||
private void GivenQueue(IEnumerable<RemoteBook> remoteBooks, TrackedDownloadState trackedDownloadState = TrackedDownloadState.Downloading)
|
||||
{
|
||||
var queue = remoteBooks.Select(remoteBook => new Queue.Queue
|
||||
@@ -97,6 +115,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.With(r => r.Author = _otherAuthor)
|
||||
.With(r => r.Books = new List<Book> { _book })
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -115,6 +134,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.Build();
|
||||
|
||||
@@ -136,6 +156,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.AZW3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -153,6 +174,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -160,9 +182,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_qualities_are_the_same_with_higher_preferred_word_score()
|
||||
public void should_return_true_when_qualities_are_the_same_with_higher_custom_format_score()
|
||||
{
|
||||
_remoteBook.PreferredWordScore = 1;
|
||||
_remoteBook.CustomFormats = new List<CustomFormat> { new CustomFormat("My Format", new ReleaseTitleSpecification { Value = "MP3" }) { Id = 1 } };
|
||||
|
||||
var lowFormat = new List<CustomFormat> { new CustomFormat("Bad Format", new ReleaseTitleSpecification { Value = "MP3" }) { Id = 2 } };
|
||||
|
||||
CustomFormatsTestHelpers.GivenCustomFormats(_remoteBook.CustomFormats.First(), lowFormat.First());
|
||||
|
||||
_author.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format");
|
||||
|
||||
GivenQueueFormats(lowFormat);
|
||||
|
||||
var remoteBook = Builder<RemoteBook>.CreateNew()
|
||||
.With(r => r.Author = _author)
|
||||
@@ -172,6 +202,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = lowFormat)
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -189,6 +220,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -208,6 +240,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -225,6 +258,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -242,6 +276,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
_remoteBook.Books.Add(_otherBook);
|
||||
@@ -261,6 +296,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
_remoteBook.Books.Add(_otherBook);
|
||||
@@ -275,6 +311,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
var remoteBooks = Builder<RemoteBook>.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(r => r.Author = _author)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.With(r => r.ParsedBookInfo = new ParsedBookInfo
|
||||
{
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
@@ -305,6 +342,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.FLAC)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook });
|
||||
@@ -324,6 +362,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Quality = new QualityModel(Quality.MP3)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.With(r => r.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteBook> { remoteBook }, TrackedDownloadState.DownloadFailedPending);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Mocker.SetConstant<ITermMatcherService>(Mocker.Resolve<TermMatcherService>());
|
||||
}
|
||||
|
||||
private void GivenRestictions(string required, string ignored)
|
||||
private void GivenRestictions(List<string> required, List<string> ignored)
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_be_true_when_title_contains_one_required_term()
|
||||
{
|
||||
GivenRestictions("WEBRip", null);
|
||||
GivenRestictions(new List<string> { "WEBRip" }, new List<string>());
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
@@ -68,7 +69,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_be_false_when_title_does_not_contain_any_required_terms()
|
||||
{
|
||||
GivenRestictions("doesnt,exist", null);
|
||||
GivenRestictions(new List<string> { "doesnt", "exist" }, new List<string>());
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
@@ -76,7 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "ignored");
|
||||
GivenRestictions(new List<string>(), new List<string> { "ignored" });
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
@@ -84,7 +85,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "edited");
|
||||
GivenRestictions(new List<string>(), new List<string> { "edited" });
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
@@ -95,7 +96,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestCase("X264,NOTTHERE")]
|
||||
public void should_ignore_case_when_matching_required(string required)
|
||||
{
|
||||
GivenRestictions(required, null);
|
||||
GivenRestictions(required.Split(',').ToList(), new List<string>());
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
@@ -106,7 +107,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestCase("X264,NOTTHERE")]
|
||||
public void should_ignore_case_when_matching_ignored(string ignored)
|
||||
{
|
||||
GivenRestictions(null, ignored);
|
||||
GivenRestictions(new List<string>(), ignored.Split(',').ToList());
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new ReleaseProfile { Required = "320", Ignored = "www.Speed.cd" }
|
||||
new ReleaseProfile { Required = new List<string> { "320" }, Ignored = new List<string> { "www.Speed.cd" } }
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
@@ -132,7 +133,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestCase(@"/\.WEB/", true)]
|
||||
public void should_match_perl_regex(string pattern, bool expected)
|
||||
{
|
||||
GivenRestictions(pattern, null);
|
||||
GivenRestictions(pattern.Split(',').ToList(), new List<string>());
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().Be(expected);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
@@ -87,7 +88,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
private void GivenUpgradeForExistingFile()
|
||||
{
|
||||
Mocker.GetMock<IUpgradableSpecification>()
|
||||
.Setup(s => s.IsUpgradable(It.IsAny<QualityProfile>(), It.IsAny<QualityModel>(), It.IsAny<int>(), It.IsAny<QualityModel>(), It.IsAny<int>()))
|
||||
.Setup(s => s.IsUpgradable(It.IsAny<QualityProfile>(), It.IsAny<QualityModel>(), It.IsAny<List<CustomFormat>>(), It.IsAny<QualityModel>(), It.IsAny<List<CustomFormat>>()))
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
@@ -117,8 +118,23 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_quality_is_last_allowed_in_profile()
|
||||
public void should_be_false_when_quality_is_last_allowed_in_profile_and_bypass_disabled()
|
||||
{
|
||||
_remoteBook.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteBook.ParsedBookInfo.Quality = new QualityModel(Quality.MP3);
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_quality_is_last_allowed_in_profile_and_bypass_enabled()
|
||||
{
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.BypassIfHighestQuality = true;
|
||||
|
||||
_remoteBook.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteBook.ParsedBookInfo.Quality = new QualityModel(Quality.MP3);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||
@@ -194,5 +210,43 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_when_custom_format_score_is_above_minimum_but_bypass_disabled()
|
||||
{
|
||||
_remoteBook.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteBook.CustomFormatScore = 100;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_when_custom_format_score_is_above_minimum_and_bypass_enabled_but_under_minimum()
|
||||
{
|
||||
_remoteBook.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteBook.CustomFormatScore = 5;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.BypassIfAboveCustomFormatScore = true;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_custom_format_score_is_above_minimum_and_bypass_enabled()
|
||||
{
|
||||
_remoteBook.Release.PublishDate = DateTime.UtcNow;
|
||||
_remoteBook.CustomFormatScore = 100;
|
||||
|
||||
_delayProfile.UsenetDelay = 720;
|
||||
_delayProfile.BypassIfAboveCustomFormatScore = true;
|
||||
_delayProfile.MinimumCustomFormatScore = 50;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteBook, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||
using NzbDrone.Core.History;
|
||||
@@ -13,9 +14,10 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||
{
|
||||
[TestFixture]
|
||||
public class HistorySpecificationFixture : CoreTest<HistorySpecification>
|
||||
@@ -37,6 +39,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
_upgradeHistory = Mocker.Resolve<HistorySpecification>();
|
||||
|
||||
CustomFormatsTestHelpers.GivenCustomFormats();
|
||||
|
||||
var singleBookList = new List<Book> { new Book { Id = FIRST_ALBUM_ID } };
|
||||
var doubleBookList = new List<Book>
|
||||
{
|
||||
@@ -50,6 +54,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
UpgradeAllowed = true,
|
||||
Cutoff = Quality.MP3.Id,
|
||||
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"),
|
||||
MinFormatScore = 0,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
})
|
||||
.Build();
|
||||
@@ -58,14 +64,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Author = _fakeAuthor,
|
||||
ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
Books = doubleBookList
|
||||
Books = doubleBookList,
|
||||
CustomFormats = new List<CustomFormat>()
|
||||
};
|
||||
|
||||
_parseResultSingle = new RemoteBook
|
||||
{
|
||||
Author = _fakeAuthor,
|
||||
ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
Books = singleBookList
|
||||
Books = singleBookList,
|
||||
CustomFormats = new List<CustomFormat>()
|
||||
};
|
||||
|
||||
_upgradableQuality = new QualityModel(Quality.MP3, new Revision(version: 1));
|
||||
@@ -74,6 +82,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Mocker.GetMock<IConfigService>()
|
||||
.SetupGet(s => s.EnableCompletedDownloadHandling)
|
||||
.Returns(true);
|
||||
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
.Setup(x => x.ParseCustomFormat(It.IsAny<EntityHistory>(), It.IsAny<Author>()))
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void GivenMostRecentForBook(int bookId, string downloadId, QualityModel quality, DateTime date, EntityHistoryEventType eventType)
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
@@ -11,93 +13,197 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestFixture]
|
||||
public class UpgradeAllowedSpecificationFixture : CoreTest<UpgradableSpecification>
|
||||
{
|
||||
[Test]
|
||||
public void should_return_false_when_quality_is_better_and_upgrade_allowed_is_false_for_quality_profile()
|
||||
private CustomFormat _customFormatOne;
|
||||
private CustomFormat _customFormatTwo;
|
||||
private QualityProfile _qualityProfile;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
_customFormatOne = new CustomFormat
|
||||
{
|
||||
Id = 1,
|
||||
Name = "One"
|
||||
};
|
||||
_customFormatTwo = new CustomFormat
|
||||
{
|
||||
Id = 2,
|
||||
Name = "Two"
|
||||
};
|
||||
|
||||
_qualityProfile = new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = false,
|
||||
CutoffFormatScore = 100,
|
||||
FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = false
|
||||
},
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Format = _customFormatOne,
|
||||
Score = 50
|
||||
},
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Format = _customFormatTwo,
|
||||
Score = 100
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_when_quality_is_better_custom_formats_are_the_same_and_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.FLAC))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.FLAC),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_when_quality_is_same_and_custom_format_is_upgrade_and_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne },
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatTwo })
|
||||
.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_custom_format_upgrade_when_upgrading_is_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = true;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne },
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatTwo })
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_same_custom_format_score_when_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne },
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne })
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_lower_custom_format_score_when_upgrading_is_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = true;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatTwo },
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne })
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_lower_language_when_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatTwo },
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat> { _customFormatOne })
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_quality_upgrade_when_upgrading_is_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = true;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.FLAC))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.FLAC),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_same_quality_when_upgrading_is_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = true;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.MP3))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_same_quality_when_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = false
|
||||
},
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.MP3))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_lower_quality_when_upgrading_is_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = true;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = true
|
||||
},
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.MP3))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.PDF),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_for_lower_quality_when_upgrading_is_not_allowed()
|
||||
{
|
||||
_qualityProfile.UpgradeAllowed = false;
|
||||
|
||||
Subject.IsUpgradeAllowed(
|
||||
new QualityProfile
|
||||
{
|
||||
Cutoff = Quality.FLAC.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
UpgradeAllowed = false
|
||||
},
|
||||
_qualityProfile,
|
||||
new QualityModel(Quality.MP3),
|
||||
new QualityModel(Quality.MP3))
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.PDF),
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
@@ -29,6 +32,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
|
||||
CustomFormatsTestHelpers.GivenCustomFormats();
|
||||
|
||||
_firstFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now };
|
||||
_secondFile = new BookFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now };
|
||||
|
||||
@@ -40,7 +45,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
UpgradeAllowed = true,
|
||||
Cutoff = Quality.MP3.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"),
|
||||
MinFormatScore = 0,
|
||||
})
|
||||
.Build();
|
||||
|
||||
@@ -52,15 +59,21 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
Author = fakeAuthor,
|
||||
ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
Books = doubleBookList
|
||||
Books = doubleBookList,
|
||||
CustomFormats = new List<CustomFormat>()
|
||||
};
|
||||
|
||||
_parseResultSingle = new RemoteBook
|
||||
{
|
||||
Author = fakeAuthor,
|
||||
ParsedBookInfo = new ParsedBookInfo { Quality = new QualityModel(Quality.MP3, new Revision(version: 2)) },
|
||||
Books = singleBookList
|
||||
Books = singleBookList,
|
||||
CustomFormats = new List<CustomFormat>()
|
||||
};
|
||||
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
.Setup(x => x.ParseCustomFormat(It.IsAny<BookFile>()))
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void WithFirstFileUpgradable()
|
||||
@@ -136,6 +149,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[Test]
|
||||
public void should_be_false_if_some_tracks_are_upgradable_and_some_are_downgrades()
|
||||
{
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
.Setup(s => s.ParseCustomFormat(It.IsAny<BookFile>()))
|
||||
.Returns(new List<CustomFormat>());
|
||||
|
||||
WithFirstFileUpgradable();
|
||||
_parseResultSingle.ParsedBookInfo.Quality = new QualityModel(Quality.MP3);
|
||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -23,8 +24,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new object[] { Quality.MP3, 1, Quality.MP3, 1, Quality.MP3, false }
|
||||
};
|
||||
|
||||
private static readonly int NoPreferredWordScore = 0;
|
||||
|
||||
private void GivenAutoDownloadPropers(ProperDownloadTypes type)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>()
|
||||
@@ -47,9 +46,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Subject.IsUpgradable(
|
||||
profile,
|
||||
new QualityModel(current, new Revision(version: currentVersion)),
|
||||
NoPreferredWordScore,
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(newQuality, new Revision(version: newVersion)),
|
||||
NoPreferredWordScore)
|
||||
new List<CustomFormat>())
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
@@ -66,9 +65,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Subject.IsUpgradable(
|
||||
profile,
|
||||
new QualityModel(Quality.MP3, new Revision(version: 1)),
|
||||
NoPreferredWordScore,
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3, new Revision(version: 2)),
|
||||
NoPreferredWordScore)
|
||||
new List<CustomFormat>())
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
@@ -85,9 +84,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
Subject.IsUpgradable(
|
||||
profile,
|
||||
new QualityModel(Quality.MP3, new Revision(version: 1)),
|
||||
NoPreferredWordScore,
|
||||
new List<CustomFormat>(),
|
||||
new QualityModel(Quality.MP3, new Revision(version: 2)),
|
||||
NoPreferredWordScore)
|
||||
new List<CustomFormat>())
|
||||
.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||
.With(h => h.Title = title)
|
||||
.With(h => h.Release = release)
|
||||
.With(h => h.Reason = reason)
|
||||
.With(h => h.ParsedBookInfo = _parsedBookInfo)
|
||||
.Build();
|
||||
|
||||
_heldReleases.AddRange(heldReleases);
|
||||
|
||||
@@ -52,7 +52,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||
_pending.Add(new PendingRelease
|
||||
{
|
||||
Id = id,
|
||||
ParsedBookInfo = new ParsedBookInfo { BookTitle = book }
|
||||
Title = "Author.Title-Book.Title.abc-Readarr",
|
||||
ParsedBookInfo = new ParsedBookInfo { BookTitle = book },
|
||||
Release = Builder<ReleaseInfo>.CreateNew().Build()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||
{
|
||||
[TestFixture]
|
||||
public class CleanupQualityProfileFormatItemsFixture : DbTest<CleanupQualityProfileFormatItems, QualityProfile>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.SetConstant<IQualityProfileFormatItemsCleanupRepository>(
|
||||
new QualityProfileFormatItemsCleanupRepository(Mocker.Resolve<IMainDatabase>(), Mocker.Resolve<IEventAggregator>()));
|
||||
|
||||
Mocker.SetConstant<ICustomFormatRepository>(
|
||||
new CustomFormatRepository(Mocker.Resolve<IMainDatabase>(), Mocker.Resolve<IEventAggregator>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_remove_orphaned_custom_formats()
|
||||
{
|
||||
var qualityProfile = Builder<QualityProfile>.CreateNew()
|
||||
.With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities())
|
||||
.With(h => h.MinFormatScore = 50)
|
||||
.With(h => h.CutoffFormatScore = 100)
|
||||
.With(h => h.FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
Builder<ProfileFormatItem>.CreateNew()
|
||||
.With(c => c.Format = new CustomFormat("My Custom Format") { Id = 0 })
|
||||
.Build()
|
||||
})
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(qualityProfile);
|
||||
Subject.Clean();
|
||||
|
||||
var result = AllStoredModels;
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result.First().FormatItems.Should().BeEmpty();
|
||||
result.First().MinFormatScore.Should().Be(0);
|
||||
result.First().CutoffFormatScore.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_remove_unorphaned_custom_formats()
|
||||
{
|
||||
var minFormatScore = 50;
|
||||
var cutoffFormatScore = 100;
|
||||
|
||||
var customFormat = Builder<CustomFormat>.CreateNew()
|
||||
.With(h => h.Specifications = new List<ICustomFormatSpecification>())
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(customFormat);
|
||||
|
||||
var qualityProfile = Builder<QualityProfile>.CreateNew()
|
||||
.With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities())
|
||||
.With(h => h.MinFormatScore = minFormatScore)
|
||||
.With(h => h.CutoffFormatScore = cutoffFormatScore)
|
||||
.With(h => h.FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
Builder<ProfileFormatItem>.CreateNew()
|
||||
.With(c => c.Format = customFormat)
|
||||
.Build()
|
||||
})
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(qualityProfile);
|
||||
|
||||
Subject.Clean();
|
||||
var result = AllStoredModels;
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result.First().FormatItems.Should().HaveCount(1);
|
||||
result.First().MinFormatScore.Should().Be(minFormatScore);
|
||||
result.First().CutoffFormatScore.Should().Be(cutoffFormatScore);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_add_missing_custom_formats()
|
||||
{
|
||||
var minFormatScore = 50;
|
||||
var cutoffFormatScore = 100;
|
||||
|
||||
var customFormat1 = Builder<CustomFormat>.CreateNew()
|
||||
.With(h => h.Id = 1)
|
||||
.With(h => h.Name = "Custom Format 1")
|
||||
.With(h => h.Specifications = new List<ICustomFormatSpecification>())
|
||||
.BuildNew();
|
||||
|
||||
var customFormat2 = Builder<CustomFormat>.CreateNew()
|
||||
.With(h => h.Id = 2)
|
||||
.With(h => h.Name = "Custom Format 2")
|
||||
.With(h => h.Specifications = new List<ICustomFormatSpecification>())
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(customFormat1);
|
||||
Db.Insert(customFormat2);
|
||||
|
||||
var qualityProfile = Builder<QualityProfile>.CreateNew()
|
||||
.With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities())
|
||||
.With(h => h.MinFormatScore = minFormatScore)
|
||||
.With(h => h.CutoffFormatScore = cutoffFormatScore)
|
||||
.With(h => h.FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
Builder<ProfileFormatItem>.CreateNew()
|
||||
.With(c => c.Format = customFormat1)
|
||||
.Build()
|
||||
})
|
||||
.BuildNew();
|
||||
|
||||
Db.Insert(qualityProfile);
|
||||
|
||||
Subject.Clean();
|
||||
var result = AllStoredModels;
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result.First().FormatItems.Should().HaveCount(2);
|
||||
result.First().MinFormatScore.Should().Be(minFormatScore);
|
||||
result.First().CutoffFormatScore.Should().Be(cutoffFormatScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.IO;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.MediaFiles.BookImport;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.BookImport
|
||||
{
|
||||
[TestFixture]
|
||||
public class GetSceneNameFixture : CoreTest
|
||||
{
|
||||
private LocalBook _localEpisode;
|
||||
private string _seasonName = "artist.title-album.title.FLAC-ingot";
|
||||
private string _episodeName = "artist.title-album.title.FLAC-ingot";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var series = Builder<Author>.CreateNew()
|
||||
.With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.With(s => s.Path = @"C:\Test\Music\Artist Title".AsOsAgnostic())
|
||||
.Build();
|
||||
|
||||
var episode = Builder<Book>.CreateNew()
|
||||
.Build();
|
||||
|
||||
_localEpisode = new LocalBook
|
||||
{
|
||||
Author = series,
|
||||
Book = episode,
|
||||
Path = Path.Combine(series.Path, "01 Some Body Loves.mkv"),
|
||||
Quality = new QualityModel(Quality.FLAC),
|
||||
ReleaseGroup = "DRONE"
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_use_download_client_item_title_as_scene_name()
|
||||
{
|
||||
_localEpisode.DownloadClientBookInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = _episodeName
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.Be(_episodeName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_download_client_item_title_as_scene_name_if_full_season()
|
||||
{
|
||||
_localEpisode.DownloadClientBookInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = _seasonName,
|
||||
Discography = true
|
||||
};
|
||||
|
||||
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _seasonName, _episodeName)
|
||||
.AsOsAgnostic();
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_file_name_as_scenename_if_it_doesnt_look_like_scenename()
|
||||
{
|
||||
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_it_doesnt_look_like_scenename()
|
||||
{
|
||||
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
_localEpisode.FolderTrackInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = "aaaaa"
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_it_is_for_a_full_season()
|
||||
{
|
||||
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
_localEpisode.FolderTrackInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = _seasonName,
|
||||
Discography = true
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_use_folder_name_as_scenename_if_there_are_other_video_files()
|
||||
{
|
||||
_localEpisode.Path = Path.Combine(@"C:\Test\Unsorted TV", _episodeName, "aaaaa.mkv")
|
||||
.AsOsAgnostic();
|
||||
|
||||
_localEpisode.FolderTrackInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = _seasonName,
|
||||
Discography = false
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.BeNull();
|
||||
}
|
||||
|
||||
[TestCase(".flac")]
|
||||
[TestCase(".par2")]
|
||||
[TestCase(".nzb")]
|
||||
public void should_remove_extension_from_nzb_title_for_scene_name(string extension)
|
||||
{
|
||||
_localEpisode.DownloadClientBookInfo = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = _episodeName + extension
|
||||
};
|
||||
|
||||
SceneNameCalculator.GetSceneName(_localEpisode).Should()
|
||||
.Be(_episodeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.BookImport.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.MediaFiles.BookImport.Specifications
|
||||
{
|
||||
[TestFixture]
|
||||
public class SameFileSpecificationFixture : CoreTest<SameFileSpecification>
|
||||
{
|
||||
private LocalBook _localTrack;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_localTrack = Builder<LocalBook>.CreateNew()
|
||||
.With(l => l.Size = 150.Megabytes())
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_accepted_if_no_existing_file()
|
||||
{
|
||||
_localTrack.Book = Builder<Book>.CreateNew()
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
/*
|
||||
[Test]
|
||||
public void should_be_accepted_if_multiple_existing_files()
|
||||
{
|
||||
_localTrack.Tracks = Builder<Track>.CreateListOfSize(2)
|
||||
.TheFirst(1)
|
||||
.With(e => e.TrackFileId = 1)
|
||||
.With(e => e.TrackFile = new LazyLoaded<TrackFile>(
|
||||
new TrackFile
|
||||
{
|
||||
Size = _localTrack.Size
|
||||
}))
|
||||
.TheNext(1)
|
||||
.With(e => e.TrackFileId = 2)
|
||||
.With(e => e.TrackFile = new LazyLoaded<TrackFile>(
|
||||
new TrackFile
|
||||
{
|
||||
Size = _localTrack.Size
|
||||
}))
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue();
|
||||
}*/
|
||||
|
||||
[Test]
|
||||
public void should_be_accepted_if_file_size_is_different()
|
||||
{
|
||||
_localTrack.Book = Builder<Book>.CreateNew()
|
||||
.With(e => e.BookFiles = new LazyLoaded<List<BookFile>>(
|
||||
new List<BookFile>
|
||||
{
|
||||
new BookFile
|
||||
{
|
||||
Size = _localTrack.Size + 100.Megabytes()
|
||||
}
|
||||
}))
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_reject_if_file_size_is_the_same()
|
||||
{
|
||||
_localTrack.Book = Builder<Book>.CreateNew()
|
||||
.With(e => e.BookFiles = new LazyLoaded<List<BookFile>>(
|
||||
new List<BookFile>
|
||||
{
|
||||
new BookFile
|
||||
{
|
||||
Size = _localTrack.Size
|
||||
}
|
||||
}))
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_accepted_if_file_cannot_be_fetched()
|
||||
{
|
||||
_localTrack.Book = Builder<Book>.CreateNew()
|
||||
.With(e => e.BookFiles = new LazyLoaded<List<BookFile>>((List<BookFile>)null))
|
||||
.Build();
|
||||
|
||||
Subject.IsSatisfiedBy(_localTrack, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -64,6 +65,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
[TestCase("Florence + the Machine", "Florence + the Machine")]
|
||||
|
||||
@@ -5,6 +5,7 @@ using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -84,6 +85,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
private void GivenProper()
|
||||
|
||||
@@ -4,6 +4,7 @@ using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -64,6 +65,10 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
Mocker.GetMock<IQualityDefinitionService>()
|
||||
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
|
||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(v => v.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
}
|
||||
|
||||
[TestCase("The Mist", "Mist, The")]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
@@ -13,11 +15,15 @@ namespace NzbDrone.Core.Test.Profiles
|
||||
{
|
||||
[TestFixture]
|
||||
|
||||
public class ProfileServiceFixture : CoreTest<QualityProfileService>
|
||||
public class QualityProfileServiceFixture : CoreTest<QualityProfileService>
|
||||
{
|
||||
[Test]
|
||||
public void init_should_add_default_profiles()
|
||||
{
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(new List<CustomFormat>());
|
||||
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IProfileRepository>()
|
||||
@@ -1,94 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
||||
{
|
||||
[TestFixture]
|
||||
public class CalculateFixture : CoreTest<Core.Profiles.Releases.PreferredWordService>
|
||||
{
|
||||
private Author _author = null;
|
||||
private List<ReleaseProfile> _releaseProfiles = null;
|
||||
private string _title = "Author.Name-Book.Title.2018.FLAC.24bit-Readarr";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_author = Builder<Author>.CreateNew()
|
||||
.With(s => s.Tags = new HashSet<int>(new[] { 1, 2 }))
|
||||
.Build();
|
||||
|
||||
_releaseProfiles = new List<ReleaseProfile>();
|
||||
|
||||
_releaseProfiles.Add(new ReleaseProfile
|
||||
{
|
||||
Preferred = new List<KeyValuePair<string, int>>
|
||||
{
|
||||
new KeyValuePair<string, int>("24bit", 5),
|
||||
new KeyValuePair<string, int>("16bit", -10)
|
||||
}
|
||||
});
|
||||
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(_releaseProfiles);
|
||||
}
|
||||
|
||||
private void GivenMatchingTerms(params string[] terms)
|
||||
{
|
||||
Mocker.GetMock<ITermMatcherService>()
|
||||
.Setup(s => s.IsMatch(It.IsAny<string>(), _title))
|
||||
.Returns<string, string>((term, title) => terms.Contains(term));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_0_when_there_are_no_release_profiles()
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>());
|
||||
|
||||
Subject.Calculate(_author, _title, 0).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_0_when_there_are_no_matching_preferred_words()
|
||||
{
|
||||
GivenMatchingTerms();
|
||||
|
||||
Subject.Calculate(_author, _title, 0).Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_calculate_positive_score()
|
||||
{
|
||||
GivenMatchingTerms("24bit");
|
||||
|
||||
Subject.Calculate(_author, _title, 0).Should().Be(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_calculate_negative_score()
|
||||
{
|
||||
GivenMatchingTerms("16bit");
|
||||
|
||||
Subject.Calculate(_author, _title, 0).Should().Be(-10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_calculate_using_multiple_profiles()
|
||||
{
|
||||
_releaseProfiles.Add(_releaseProfiles.First());
|
||||
|
||||
GivenMatchingTerms("24bit");
|
||||
|
||||
Subject.Calculate(_author, _title, 0).Should().Be(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService
|
||||
{
|
||||
[TestFixture]
|
||||
public class GetMatchingPreferredWordsFixture : CoreTest<Core.Profiles.Releases.PreferredWordService>
|
||||
{
|
||||
private Author _author = null;
|
||||
private List<ReleaseProfile> _releaseProfiles = null;
|
||||
private string _title = "Author.Name-Book.Name-2018-Flac-Vinyl-Readarr";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_author = Builder<Author>.CreateNew()
|
||||
.With(s => s.Tags = new HashSet<int>(new[] { 1, 2 }))
|
||||
.Build();
|
||||
|
||||
_releaseProfiles = new List<ReleaseProfile>();
|
||||
|
||||
_releaseProfiles.Add(new ReleaseProfile
|
||||
{
|
||||
Preferred = new List<KeyValuePair<string, int>>
|
||||
{
|
||||
new KeyValuePair<string, int>("Vinyl", 5),
|
||||
new KeyValuePair<string, int>("CD", -10)
|
||||
}
|
||||
});
|
||||
|
||||
Mocker.GetMock<ITermMatcherService>()
|
||||
.Setup(s => s.MatchingTerm(It.IsAny<string>(), _title))
|
||||
.Returns<string, string>((term, title) => title.Contains(term) ? term : null);
|
||||
}
|
||||
|
||||
private void GivenReleaseProfile()
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(_releaseProfiles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_empty_list_when_there_are_no_release_profiles()
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>());
|
||||
|
||||
Subject.GetMatchingPreferredWords(_author, _title).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_empty_list_when_there_are_no_matching_preferred_words()
|
||||
{
|
||||
_releaseProfiles.First().Preferred.RemoveAt(0);
|
||||
GivenReleaseProfile();
|
||||
|
||||
Subject.GetMatchingPreferredWords(_author, _title).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_list_of_matching_terms()
|
||||
{
|
||||
GivenReleaseProfile();
|
||||
|
||||
Subject.GetMatchingPreferredWords(_author, _title).Should().Contain(new[] { "Vinyl" });
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/NzbDrone.Core/Annotations/SelectOption.cs
Normal file
10
src/NzbDrone.Core/Annotations/SelectOption.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace NzbDrone.Core.Annotations
|
||||
{
|
||||
public class SelectOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Order { get; set; }
|
||||
public string Hint { get; set; }
|
||||
}
|
||||
}
|
||||
9
src/NzbDrone.Core/Annotations/SelectOptionsConverter.cs
Normal file
9
src/NzbDrone.Core/Annotations/SelectOptionsConverter.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Annotations
|
||||
{
|
||||
public interface ISelectOptionsConverter
|
||||
{
|
||||
List<SelectOption> GetSelectOptions();
|
||||
}
|
||||
}
|
||||
71
src/NzbDrone.Core/CustomFormats/CustomFormat.cs
Normal file
71
src/NzbDrone.Core/CustomFormats/CustomFormat.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class CustomFormat : ModelBase, IEquatable<CustomFormat>
|
||||
{
|
||||
public CustomFormat()
|
||||
{
|
||||
}
|
||||
|
||||
public CustomFormat(string name, params ICustomFormatSpecification[] specs)
|
||||
{
|
||||
Name = name;
|
||||
Specifications = specs.ToList();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IncludeCustomFormatWhenRenaming { get; set; }
|
||||
|
||||
public List<ICustomFormatSpecification> Specifications { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public bool Equals(CustomFormat other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return int.Equals(Id, other.Id);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((CustomFormat)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public interface ICustomFormatCalculationService
|
||||
{
|
||||
List<CustomFormat> ParseCustomFormat(RemoteBook remoteBook, long size);
|
||||
List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author artist);
|
||||
List<CustomFormat> ParseCustomFormat(BookFile bookFile);
|
||||
List<CustomFormat> ParseCustomFormat(Blocklist blocklist, Author artist);
|
||||
List<CustomFormat> ParseCustomFormat(EntityHistory history, Author artist);
|
||||
List<CustomFormat> ParseCustomFormat(LocalBook localBook);
|
||||
}
|
||||
|
||||
public class CustomFormatCalculationService : ICustomFormatCalculationService
|
||||
{
|
||||
private readonly ICustomFormatService _formatService;
|
||||
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService)
|
||||
{
|
||||
_formatService = formatService;
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(RemoteBook remoteBook, long size)
|
||||
{
|
||||
var input = new CustomFormatInput
|
||||
{
|
||||
BookInfo = remoteBook.ParsedBookInfo,
|
||||
Author = remoteBook.Author,
|
||||
Size = size
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author)
|
||||
{
|
||||
return ParseCustomFormat(bookFile, author, _formatService.All());
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(BookFile bookFile)
|
||||
{
|
||||
return ParseCustomFormat(bookFile, bookFile.Author.Value, _formatService.All());
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(Blocklist blocklist, Author author)
|
||||
{
|
||||
var parsed = Parser.Parser.ParseBookTitle(blocklist.SourceTitle);
|
||||
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = author.Name,
|
||||
ReleaseTitle = parsed?.ReleaseTitle ?? blocklist.SourceTitle,
|
||||
Quality = blocklist.Quality,
|
||||
ReleaseGroup = parsed?.ReleaseGroup
|
||||
};
|
||||
|
||||
var input = new CustomFormatInput
|
||||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = blocklist.Size ?? 0
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(EntityHistory history, Author author)
|
||||
{
|
||||
var parsed = Parser.Parser.ParseBookTitle(history.SourceTitle);
|
||||
|
||||
long.TryParse(history.Data.GetValueOrDefault("size"), out var size);
|
||||
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = author.Name,
|
||||
ReleaseTitle = parsed?.ReleaseTitle ?? history.SourceTitle,
|
||||
Quality = history.Quality,
|
||||
ReleaseGroup = parsed?.ReleaseGroup,
|
||||
};
|
||||
|
||||
var input = new CustomFormatInput
|
||||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = size
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(LocalBook localBook)
|
||||
{
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = localBook.Author.Name,
|
||||
ReleaseTitle = localBook.SceneName,
|
||||
Quality = localBook.Quality,
|
||||
ReleaseGroup = localBook.ReleaseGroup
|
||||
};
|
||||
|
||||
var input = new CustomFormatInput
|
||||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = localBook.Author,
|
||||
Size = localBook.Size
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input);
|
||||
}
|
||||
|
||||
private List<CustomFormat> ParseCustomFormat(CustomFormatInput input)
|
||||
{
|
||||
return ParseCustomFormat(input, _formatService.All());
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(CustomFormatInput input, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var matches = new List<CustomFormat>();
|
||||
|
||||
foreach (var customFormat in allCustomFormats)
|
||||
{
|
||||
var specificationMatches = customFormat.Specifications
|
||||
.GroupBy(t => t.GetType())
|
||||
.Select(g => new SpecificationMatchesGroup
|
||||
{
|
||||
Matches = g.ToDictionary(t => t, t => t.IsSatisfiedBy(input))
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (specificationMatches.All(x => x.DidMatch))
|
||||
{
|
||||
matches.Add(customFormat);
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var sceneName = string.Empty;
|
||||
if (bookFile.SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.SceneName;
|
||||
}
|
||||
else if (bookFile.OriginalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.OriginalFilePath;
|
||||
}
|
||||
else if (bookFile.Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = Path.GetFileName(bookFile.Path);
|
||||
}
|
||||
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = author.Name,
|
||||
ReleaseTitle = sceneName,
|
||||
Quality = bookFile.Quality,
|
||||
ReleaseGroup = bookFile.ReleaseGroup
|
||||
};
|
||||
|
||||
var input = new CustomFormatInput
|
||||
{
|
||||
BookInfo = bookInfo,
|
||||
Author = author,
|
||||
Size = bookFile.Size,
|
||||
Filename = Path.GetFileName(bookFile.Path)
|
||||
};
|
||||
|
||||
return ParseCustomFormat(input, allCustomFormats);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs
Normal file
36
src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class CustomFormatInput
|
||||
{
|
||||
public ParsedBookInfo BookInfo { get; set; }
|
||||
public Author Author { get; set; }
|
||||
public long Size { get; set; }
|
||||
public string Filename { get; set; }
|
||||
|
||||
// public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series)
|
||||
// {
|
||||
// EpisodeInfo = episodeInfo;
|
||||
// Series = series;
|
||||
// }
|
||||
//
|
||||
// public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series, long size, List<Language> languages)
|
||||
// {
|
||||
// EpisodeInfo = episodeInfo;
|
||||
// Series = series;
|
||||
// Size = size;
|
||||
// Languages = languages;
|
||||
// }
|
||||
//
|
||||
// public CustomFormatInput(ParsedEpisodeInfo episodeInfo, Series series, long size, List<Language> languages, string filename)
|
||||
// {
|
||||
// EpisodeInfo = episodeInfo;
|
||||
// Series = series;
|
||||
// Size = size;
|
||||
// Languages = languages;
|
||||
// Filename = filename;
|
||||
// }
|
||||
}
|
||||
}
|
||||
17
src/NzbDrone.Core/CustomFormats/CustomFormatRepository.cs
Normal file
17
src/NzbDrone.Core/CustomFormats/CustomFormatRepository.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public interface ICustomFormatRepository : IBasicRepository<CustomFormat>
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomFormatRepository : BasicRepository<CustomFormat>, ICustomFormatRepository
|
||||
{
|
||||
public CustomFormatRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/NzbDrone.Core/CustomFormats/CustomFormatService.cs
Normal file
77
src/NzbDrone.Core/CustomFormats/CustomFormatService.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.CustomFormats.Events;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public interface ICustomFormatService
|
||||
{
|
||||
void Update(CustomFormat customFormat);
|
||||
CustomFormat Insert(CustomFormat customFormat);
|
||||
List<CustomFormat> All();
|
||||
CustomFormat GetById(int id);
|
||||
void Delete(int id);
|
||||
}
|
||||
|
||||
public class CustomFormatService : ICustomFormatService
|
||||
{
|
||||
private readonly ICustomFormatRepository _formatRepository;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ICached<Dictionary<int, CustomFormat>> _cache;
|
||||
|
||||
public CustomFormatService(ICustomFormatRepository formatRepository,
|
||||
ICacheManager cacheManager,
|
||||
IEventAggregator eventAggregator)
|
||||
{
|
||||
_formatRepository = formatRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_cache = cacheManager.GetCache<Dictionary<int, CustomFormat>>(typeof(CustomFormat), "formats");
|
||||
}
|
||||
|
||||
private Dictionary<int, CustomFormat> AllDictionary()
|
||||
{
|
||||
return _cache.Get("all", () => _formatRepository.All().ToDictionary(m => m.Id));
|
||||
}
|
||||
|
||||
public List<CustomFormat> All()
|
||||
{
|
||||
return AllDictionary().Values.ToList();
|
||||
}
|
||||
|
||||
public CustomFormat GetById(int id)
|
||||
{
|
||||
return AllDictionary()[id];
|
||||
}
|
||||
|
||||
public void Update(CustomFormat customFormat)
|
||||
{
|
||||
_formatRepository.Update(customFormat);
|
||||
_cache.Clear();
|
||||
}
|
||||
|
||||
public CustomFormat Insert(CustomFormat customFormat)
|
||||
{
|
||||
// Add to DB then insert into profiles
|
||||
var result = _formatRepository.Insert(customFormat);
|
||||
_cache.Clear();
|
||||
|
||||
_eventAggregator.PublishEvent(new CustomFormatAddedEvent(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
var format = _formatRepository.Get(id);
|
||||
|
||||
// Remove from profiles before removing from DB
|
||||
_eventAggregator.PublishEvent(new CustomFormatDeletedEvent(format));
|
||||
|
||||
_formatRepository.Delete(id);
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats.Events
|
||||
{
|
||||
public class CustomFormatAddedEvent : IEvent
|
||||
{
|
||||
public CustomFormatAddedEvent(CustomFormat format)
|
||||
{
|
||||
CustomFormat = format;
|
||||
}
|
||||
|
||||
public CustomFormat CustomFormat { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats.Events
|
||||
{
|
||||
public class CustomFormatDeletedEvent : IEvent
|
||||
{
|
||||
public CustomFormatDeletedEvent(CustomFormat format)
|
||||
{
|
||||
CustomFormat = format;
|
||||
}
|
||||
|
||||
public CustomFormat CustomFormat { get; private set; }
|
||||
}
|
||||
}
|
||||
13
src/NzbDrone.Core/CustomFormats/SpecificationMatchesGroup.cs
Normal file
13
src/NzbDrone.Core/CustomFormats/SpecificationMatchesGroup.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class SpecificationMatchesGroup
|
||||
{
|
||||
public Dictionary<ICustomFormatSpecification, bool> Matches { get; set; }
|
||||
|
||||
public bool DidMatch => !(Matches.Any(m => m.Key.Required && m.Value == false) ||
|
||||
Matches.All(m => m.Value == false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public abstract class CustomFormatSpecificationBase : ICustomFormatSpecification
|
||||
{
|
||||
public abstract int Order { get; }
|
||||
public abstract string ImplementationName { get; }
|
||||
|
||||
public virtual string InfoLink => "https://wiki.servarr.com/readarr/settings#custom-formats-2";
|
||||
|
||||
public string Name { get; set; }
|
||||
public bool Negate { get; set; }
|
||||
public bool Required { get; set; }
|
||||
|
||||
public ICustomFormatSpecification Clone()
|
||||
{
|
||||
return (ICustomFormatSpecification)MemberwiseClone();
|
||||
}
|
||||
|
||||
public abstract NzbDroneValidationResult Validate();
|
||||
|
||||
public bool IsSatisfiedBy(CustomFormatInput input)
|
||||
{
|
||||
var match = IsSatisfiedByWithoutNegate(input);
|
||||
if (Negate)
|
||||
{
|
||||
match = !match;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
protected abstract bool IsSatisfiedByWithoutNegate(CustomFormatInput input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public interface ICustomFormatSpecification
|
||||
{
|
||||
int Order { get; }
|
||||
string InfoLink { get; }
|
||||
string ImplementationName { get; }
|
||||
string Name { get; set; }
|
||||
bool Negate { get; set; }
|
||||
bool Required { get; set; }
|
||||
|
||||
NzbDroneValidationResult Validate();
|
||||
|
||||
ICustomFormatSpecification Clone();
|
||||
bool IsSatisfiedBy(CustomFormatInput input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class RegexSpecificationBaseValidator : AbstractValidator<RegexSpecificationBase>
|
||||
{
|
||||
public RegexSpecificationBaseValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).NotEmpty().WithMessage("Regex Pattern must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class RegexSpecificationBase : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly RegexSpecificationBaseValidator Validator = new RegexSpecificationBaseValidator();
|
||||
|
||||
protected Regex _regex;
|
||||
protected string _raw;
|
||||
|
||||
[FieldDefinition(1, Label = "Regular Expression", HelpText = "Custom Format RegEx is Case Insensitive")]
|
||||
public string Value
|
||||
{
|
||||
get => _raw;
|
||||
set
|
||||
{
|
||||
_raw = value;
|
||||
|
||||
if (value.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_regex = new Regex(value, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool MatchString(string compared)
|
||||
{
|
||||
if (compared == null || _regex == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _regex.IsMatch(compared);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class ReleaseGroupSpecification : RegexSpecificationBase
|
||||
{
|
||||
public override int Order => 9;
|
||||
public override string ImplementationName => "Release Group";
|
||||
public override string InfoLink => "https://wiki.servarr.com/readarr/settings#custom-formats-2";
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
return MatchString(input.BookInfo?.ReleaseGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class ReleaseTitleSpecification : RegexSpecificationBase
|
||||
{
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Release Title";
|
||||
public override string InfoLink => "https://wiki.servarr.com/readarr/settings#custom-formats-2";
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
return MatchString(input.BookInfo?.ReleaseTitle) || MatchString(input.Filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class SizeSpecificationValidator : AbstractValidator<SizeSpecification>
|
||||
{
|
||||
public SizeSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Min).GreaterThanOrEqualTo(0);
|
||||
RuleFor(c => c.Max).GreaterThan(c => c.Min);
|
||||
}
|
||||
}
|
||||
|
||||
public class SizeSpecification : CustomFormatSpecificationBase
|
||||
{
|
||||
private static readonly SizeSpecificationValidator Validator = new SizeSpecificationValidator();
|
||||
|
||||
public override int Order => 8;
|
||||
public override string ImplementationName => "Size";
|
||||
|
||||
[FieldDefinition(1, Label = "Minimum Size", HelpText = "Release must be greater than this size", Unit = "GB", Type = FieldType.Number)]
|
||||
public double Min { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Maximum Size", HelpText = "Release must be less than or equal to this size", Unit = "GB", Type = FieldType.Number)]
|
||||
public double Max { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(CustomFormatInput input)
|
||||
{
|
||||
var size = input.Size;
|
||||
|
||||
return size > Min.Gigabytes() && size <= Max.Gigabytes();
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class CustomFormatIntConverter : JsonConverter<CustomFormat>
|
||||
{
|
||||
public override CustomFormat Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return new CustomFormat { Id = reader.GetInt32() };
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, CustomFormat value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class CustomFormatSpecificationListConverter : JsonConverter<List<ICustomFormatSpecification>>
|
||||
{
|
||||
public override void Write(Utf8JsonWriter writer, List<ICustomFormatSpecification> value, JsonSerializerOptions options)
|
||||
{
|
||||
var wrapped = value.Select(x => new SpecificationWrapper
|
||||
{
|
||||
Type = x.GetType().Name,
|
||||
Body = x
|
||||
});
|
||||
|
||||
JsonSerializer.Serialize(writer, wrapped, options);
|
||||
}
|
||||
|
||||
public override List<ICustomFormatSpecification> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
ValidateToken(reader, JsonTokenType.StartArray);
|
||||
|
||||
var results = new List<ICustomFormatSpecification>();
|
||||
|
||||
reader.Read(); // Advance to the first object after the StartArray token. This should be either a StartObject token, or the EndArray token. Anything else is invalid.
|
||||
|
||||
while (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
reader.Read(); // Move to type property name
|
||||
ValidateToken(reader, JsonTokenType.PropertyName);
|
||||
|
||||
reader.Read(); // Move to type property value
|
||||
ValidateToken(reader, JsonTokenType.String);
|
||||
var typename = reader.GetString();
|
||||
|
||||
reader.Read(); // Move to body property name
|
||||
ValidateToken(reader, JsonTokenType.PropertyName);
|
||||
|
||||
reader.Read(); // Move to start of object (stored in this property)
|
||||
ValidateToken(reader, JsonTokenType.StartObject); // Start of formattag
|
||||
|
||||
var type = Type.GetType($"NzbDrone.Core.CustomFormats.{typename}, Readarr.Core", true);
|
||||
var item = (ICustomFormatSpecification)JsonSerializer.Deserialize(ref reader, type, options);
|
||||
results.Add(item);
|
||||
|
||||
reader.Read(); // Move past end of body object
|
||||
reader.Read(); // Move past end of 'wrapper' object
|
||||
}
|
||||
|
||||
ValidateToken(reader, JsonTokenType.EndArray);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Helper function for validating where you are in the JSON
|
||||
private void ValidateToken(Utf8JsonReader reader, JsonTokenType tokenType)
|
||||
{
|
||||
if (reader.TokenType != tokenType)
|
||||
{
|
||||
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
|
||||
}
|
||||
}
|
||||
|
||||
private class SpecificationWrapper
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public object Body { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
304
src/NzbDrone.Core/Datastore/Migration/026_add_custom_formats.cs
Normal file
304
src/NzbDrone.Core/Datastore/Migration/026_add_custom_formats.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(026)]
|
||||
public class add_custom_formats : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("DelayProfiles").AddColumn("BypassIfHighestQuality").AsBoolean().WithDefaultValue(false);
|
||||
|
||||
// Set to true for existing Delay Profiles to keep behavior the same.
|
||||
Update.Table("DelayProfiles").Set(new { BypassIfHighestQuality = true }).AllRows();
|
||||
|
||||
Alter.Table("BookFiles").AddColumn("OriginalFilePath").AsString().Nullable();
|
||||
|
||||
Execute.WithConnection(ChangeRequiredIgnoredTypes);
|
||||
|
||||
// Add Custom Format Columns
|
||||
Create.TableForModel("CustomFormats")
|
||||
.WithColumn("Name").AsString().Unique()
|
||||
.WithColumn("Specifications").AsString().WithDefaultValue("[]")
|
||||
.WithColumn("IncludeCustomFormatWhenRenaming").AsBoolean().WithDefaultValue(false);
|
||||
|
||||
// Add Custom Format Columns to Quality Profiles
|
||||
Alter.Table("QualityProfiles").AddColumn("FormatItems").AsString().WithDefaultValue("[]");
|
||||
Alter.Table("QualityProfiles").AddColumn("MinFormatScore").AsInt32().WithDefaultValue(0);
|
||||
Alter.Table("QualityProfiles").AddColumn("CutoffFormatScore").AsInt32().WithDefaultValue(0);
|
||||
|
||||
// Migrate Preferred Words to Custom Formats
|
||||
Execute.WithConnection(MigratePreferredTerms);
|
||||
Execute.WithConnection(MigrateNamingConfigs);
|
||||
|
||||
// Remove Preferred Word Columns from ReleaseProfiles
|
||||
Delete.Column("Preferred").FromTable("ReleaseProfiles");
|
||||
Delete.Column("IncludePreferredWhenRenaming").FromTable("ReleaseProfiles");
|
||||
|
||||
// Remove Profiles that will no longer validate
|
||||
Execute.Sql("DELETE FROM \"ReleaseProfiles\" WHERE \"Required\" = '[]' AND \"Ignored\" = '[]'");
|
||||
|
||||
Alter.Table("DelayProfiles").AddColumn("BypassIfAboveCustomFormatScore").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("DelayProfiles").AddColumn("MinimumCustomFormatScore").AsInt32().Nullable();
|
||||
}
|
||||
|
||||
private void ChangeRequiredIgnoredTypes(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updatedProfiles = new List<object>();
|
||||
|
||||
using (var getEmailCmd = conn.CreateCommand())
|
||||
{
|
||||
getEmailCmd.Transaction = tran;
|
||||
getEmailCmd.CommandText = "SELECT \"Id\", \"Required\", \"Ignored\" FROM \"ReleaseProfiles\"";
|
||||
|
||||
using (var reader = getEmailCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var requiredObj = reader.GetValue(1);
|
||||
var ignoredObj = reader.GetValue(2);
|
||||
|
||||
var required = requiredObj == DBNull.Value
|
||||
? Enumerable.Empty<string>()
|
||||
: requiredObj.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var ignored = ignoredObj == DBNull.Value
|
||||
? Enumerable.Empty<string>()
|
||||
: ignoredObj.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
updatedProfiles.Add(new
|
||||
{
|
||||
Id = id,
|
||||
Required = required.ToJson(),
|
||||
Ignored = ignored.ToJson()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateProfileSql = "UPDATE \"ReleaseProfiles\" SET \"Required\" = @Required, \"Ignored\" = @Ignored WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateProfileSql, updatedProfiles, transaction: tran);
|
||||
}
|
||||
|
||||
private void MigratePreferredTerms(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updatedCollections = new List<CustomFormat026>();
|
||||
|
||||
// Pull list of quality Profiles
|
||||
var qualityProfiles = new List<QualityProfile026>();
|
||||
using (var getProfiles = conn.CreateCommand())
|
||||
{
|
||||
getProfiles.Transaction = tran;
|
||||
getProfiles.CommandText = @"SELECT ""Id"" FROM ""QualityProfiles""";
|
||||
|
||||
using (var definitionsReader = getProfiles.ExecuteReader())
|
||||
{
|
||||
while (definitionsReader.Read())
|
||||
{
|
||||
var id = definitionsReader.GetInt32(0);
|
||||
qualityProfiles.Add(new QualityProfile026
|
||||
{
|
||||
Id = id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate List of Custom Formats from Preferred Words
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT \"Preferred\", \"IncludePreferredWhenRenaming\", \"Enabled\", \"Id\" FROM \"ReleaseProfiles\" WHERE \"Preferred\" IS NOT NULL";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var preferred = reader.GetString(0);
|
||||
var includeName = reader.GetBoolean(1);
|
||||
var enabled = reader.GetBoolean(2);
|
||||
var releaseProfileId = reader.GetInt32(3);
|
||||
|
||||
string name = null;
|
||||
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
name = $"Unnamed_{releaseProfileId}";
|
||||
}
|
||||
else
|
||||
{
|
||||
name = $"{name}_{releaseProfileId}";
|
||||
}
|
||||
|
||||
var data = STJson.Deserialize<List<PreferredWord026>>(preferred);
|
||||
|
||||
var specs = new List<CustomFormatSpec026>();
|
||||
|
||||
var nameIdentifier = 0;
|
||||
|
||||
foreach (var term in data)
|
||||
{
|
||||
var regexTerm = term.Key.TrimStart('/').TrimEnd("/i");
|
||||
|
||||
// Validate Regex before creating a CF
|
||||
try
|
||||
{
|
||||
Regex.Match("", regexTerm);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
updatedCollections.Add(new CustomFormat026
|
||||
{
|
||||
Name = data.Count > 1 ? $"{name}_{nameIdentifier++}" : name,
|
||||
PreferredName = name,
|
||||
IncludeCustomFormatWhenRenaming = includeName,
|
||||
Score = term.Value,
|
||||
Enabled = enabled,
|
||||
Specifications = new List<CustomFormatSpec026>
|
||||
{
|
||||
new CustomFormatSpec026
|
||||
{
|
||||
Type = "ReleaseTitleSpecification",
|
||||
Body = new CustomFormatReleaseTitleSpec026
|
||||
{
|
||||
Order = 1,
|
||||
ImplementationName = "Release Title",
|
||||
Name = regexTerm,
|
||||
Value = regexTerm
|
||||
}
|
||||
}
|
||||
}.ToJson()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert Custom Formats
|
||||
var updateSql = "INSERT INTO \"CustomFormats\" (\"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\") VALUES (@Name, @IncludeCustomFormatWhenRenaming, @Specifications)";
|
||||
conn.Execute(updateSql, updatedCollections, transaction: tran);
|
||||
|
||||
// Pull List of Custom Formats with new Ids
|
||||
var formats = new List<CustomFormat026>();
|
||||
using (var getProfiles = conn.CreateCommand())
|
||||
{
|
||||
getProfiles.Transaction = tran;
|
||||
getProfiles.CommandText = @"SELECT ""Id"", ""Name"" FROM ""CustomFormats""";
|
||||
|
||||
using (var definitionsReader = getProfiles.ExecuteReader())
|
||||
{
|
||||
while (definitionsReader.Read())
|
||||
{
|
||||
var id = definitionsReader.GetInt32(0);
|
||||
var name = definitionsReader.GetString(1);
|
||||
formats.Add(new CustomFormat026
|
||||
{
|
||||
Id = id,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update each profile with original scores
|
||||
foreach (var profile in qualityProfiles)
|
||||
{
|
||||
profile.FormatItems = formats.Select(x => new { Format = x.Id, Score = updatedCollections.First(f => f.Name == x.Name).Enabled ? updatedCollections.First(f => f.Name == x.Name).Score : 0 }).ToJson();
|
||||
}
|
||||
|
||||
// Push profile updates to DB
|
||||
var updateProfilesSql = "UPDATE \"QualityProfiles\" SET \"FormatItems\" = @FormatItems WHERE \"Id\" = @Id";
|
||||
conn.Execute(updateProfilesSql, qualityProfiles, transaction: tran);
|
||||
}
|
||||
|
||||
private void MigrateNamingConfigs(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var updatedNamingConfigs = new List<object>();
|
||||
|
||||
using (IDbCommand namingConfigCmd = conn.CreateCommand())
|
||||
{
|
||||
namingConfigCmd.Transaction = tran;
|
||||
namingConfigCmd.CommandText = @"SELECT * FROM ""NamingConfig"" LIMIT 1";
|
||||
using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader())
|
||||
{
|
||||
var standardBookFormatIndex = namingConfigReader.GetOrdinal("StandardBookFormat");
|
||||
|
||||
while (namingConfigReader.Read())
|
||||
{
|
||||
var standardBookFormat = NameReplace(namingConfigReader.GetString(standardBookFormatIndex));
|
||||
|
||||
updatedNamingConfigs.Add(new
|
||||
{
|
||||
StandardBookFormat = standardBookFormat,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updateProfileSql = "UPDATE \"NamingConfig\" SET \"StandardBookFormat\" = @StandardBookFormat";
|
||||
conn.Execute(updateProfileSql, updatedNamingConfigs, transaction: tran);
|
||||
}
|
||||
|
||||
private string NameReplace(string oldTokenString)
|
||||
{
|
||||
var newTokenString = oldTokenString.Replace("Preferred Words", "Custom Formats")
|
||||
.Replace("Preferred.Words", "Custom.Formats")
|
||||
.Replace("Preferred-Words", "Custom-Formats")
|
||||
.Replace("Preferred_Words", "Custom_Formats");
|
||||
|
||||
return newTokenString;
|
||||
}
|
||||
|
||||
private class PreferredWord026
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
private class QualityProfile026
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormat026
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string PreferredName { get; set; }
|
||||
public bool IncludeCustomFormatWhenRenaming { get; set; }
|
||||
public string Specifications { get; set; }
|
||||
public int Score { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormatSpec026
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public CustomFormatReleaseTitleSpec026 Body { get; set; }
|
||||
}
|
||||
|
||||
private class CustomFormatReleaseTitleSpec026
|
||||
{
|
||||
public int Order { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Negate { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.History;
|
||||
@@ -27,6 +28,7 @@ using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Notifications;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
using NzbDrone.Core.Profiles.Metadata;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
@@ -175,6 +177,8 @@ namespace NzbDrone.Core.Datastore
|
||||
.Ignore(d => d.GroupWeight)
|
||||
.Ignore(d => d.Weight);
|
||||
|
||||
Mapper.Entity<CustomFormat>("CustomFormats").RegisterModel();
|
||||
|
||||
Mapper.Entity<QualityProfile>("QualityProfiles").RegisterModel();
|
||||
Mapper.Entity<MetadataProfile>("MetadataProfiles").RegisterModel();
|
||||
Mapper.Entity<Log>("Logs").RegisterModel();
|
||||
@@ -220,6 +224,8 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.AddTypeHandler(new DapperTimeSpanConverter());
|
||||
SqlMapper.AddTypeHandler(new DapperQualityIntConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<QualityProfileQualityItem>>(new QualityIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem>>(new CustomFormatIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ICustomFormatSpecification>>(new CustomFormatSpecificationListConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<QualityModel>(new QualityIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<IDictionary<string, string>>());
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
var comparers = new List<CompareDelegate>
|
||||
{
|
||||
CompareQuality,
|
||||
ComparePreferredWordScore,
|
||||
CompareCustomFormatScore,
|
||||
CompareProtocol,
|
||||
CompareIndexerPriority,
|
||||
ComparePeersIfTorrent,
|
||||
@@ -76,9 +76,9 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
CompareBy(x.RemoteBook, y.RemoteBook, remoteBook => remoteBook.ParsedBookInfo.Quality.Revision));
|
||||
}
|
||||
|
||||
private int ComparePreferredWordScore(DownloadDecision x, DownloadDecision y)
|
||||
private int CompareCustomFormatScore(DownloadDecision x, DownloadDecision y)
|
||||
{
|
||||
return CompareBy(x.RemoteBook, y.RemoteBook, remoteBook => remoteBook.PreferredWordScore);
|
||||
return CompareBy(x.RemoteBook, y.RemoteBook, remoteBook => remoteBook.CustomFormatScore);
|
||||
}
|
||||
|
||||
private int CompareProtocol(DownloadDecision x, DownloadDecision y)
|
||||
|
||||
@@ -5,6 +5,7 @@ using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download.Aggregation;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -23,17 +24,20 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
public class DownloadDecisionMaker : IMakeDownloadDecision
|
||||
{
|
||||
private readonly IEnumerable<IDecisionEngineSpecification> _specifications;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IRemoteBookAggregationService _aggregationService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DownloadDecisionMaker(IEnumerable<IDecisionEngineSpecification> specifications,
|
||||
IParsingService parsingService,
|
||||
ICustomFormatCalculationService formatService,
|
||||
IRemoteBookAggregationService aggregationService,
|
||||
Logger logger)
|
||||
{
|
||||
_specifications = specifications;
|
||||
_parsingService = parsingService;
|
||||
_formatCalculator = formatService;
|
||||
_aggregationService = aggregationService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -89,6 +93,9 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
if (parsedBookInfo != null && !parsedBookInfo.AuthorName.IsNullOrWhiteSpace())
|
||||
{
|
||||
var remoteBook = _parsingService.Map(parsedBookInfo, searchCriteria);
|
||||
remoteBook.Release = report;
|
||||
|
||||
_aggregationService.Augment(remoteBook);
|
||||
|
||||
// try parsing again using the search criteria, in case it parsed but parsed incorrectly
|
||||
if ((remoteBook.Author == null || remoteBook.Books.Empty()) && searchCriteria != null)
|
||||
@@ -134,6 +141,10 @@ namespace NzbDrone.Core.DecisionEngine
|
||||
else
|
||||
{
|
||||
_aggregationService.Augment(remoteBook);
|
||||
|
||||
remoteBook.CustomFormats = _formatCalculator.ParseCustomFormat(remoteBook, remoteBook.Release.Size);
|
||||
remoteBook.CustomFormatScore = remoteBook?.Author?.QualityProfile?.Value.CalculateCustomFormatScore(remoteBook.CustomFormats) ?? 0;
|
||||
|
||||
remoteBook.DownloadAllowed = remoteBook.Books.Any();
|
||||
decision = GetDecisionForReport(remoteBook, searchCriteria);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class CustomFormatAllowedbyProfileSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteBook subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var minScore = subject.Author.QualityProfile.Value.MinFormatScore;
|
||||
var score = subject.CustomFormatScore;
|
||||
|
||||
if (score < minScore)
|
||||
{
|
||||
return Decision.Reject("Custom Formats {0} have score {1} below Author profile minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore);
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
@@ -13,14 +14,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly Logger _logger;
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
private readonly ICustomFormatCalculationService _formatService;
|
||||
|
||||
public CutoffSpecification(UpgradableSpecification upgradableSpecification,
|
||||
IPreferredWordService preferredWordServiceCalculator,
|
||||
ICustomFormatCalculationService formatService,
|
||||
Logger logger)
|
||||
{
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -38,11 +39,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
_logger.Debug("Comparing file quality with report. Existing files contain {0}", currentQualities.ConcatToString());
|
||||
|
||||
var customFormats = _formatService.ParseCustomFormat(file);
|
||||
|
||||
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
|
||||
currentQualities,
|
||||
_preferredWordServiceCalculator.Calculate(subject.Author, file.GetSceneOrFileName(), subject.Release.IndexerId),
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore))
|
||||
customFormats,
|
||||
subject.ParsedBookInfo.Quality))
|
||||
{
|
||||
_logger.Debug("Cutoff already met by existing files, rejecting.");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -14,17 +15,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
private readonly ICustomFormatCalculationService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QueueSpecification(IQueueService queueService,
|
||||
UpgradableSpecification upgradableSpecification,
|
||||
IPreferredWordService preferredWordServiceCalculator,
|
||||
Logger logger)
|
||||
UpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatService,
|
||||
Logger logger)
|
||||
{
|
||||
_queueService = queueService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -54,13 +55,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
_logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteBook.ParsedBookInfo.Quality);
|
||||
|
||||
var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Author, queueItem.Title, subject.Release?.IndexerId ?? 0);
|
||||
var queuedItemCustomFormats = _formatService.ParseCustomFormat(remoteBook, (long)queueItem.Size);
|
||||
|
||||
if (!_upgradableSpecification.CutoffNotMet(qualityProfile,
|
||||
new List<QualityModel> { remoteBook.ParsedBookInfo.Quality },
|
||||
queuedItemPreferredWordScore,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore))
|
||||
queuedItemCustomFormats,
|
||||
subject.ParsedBookInfo.Quality))
|
||||
{
|
||||
return Decision.Reject("Release in queue already meets cutoff: {0}", remoteBook.ParsedBookInfo.Quality);
|
||||
}
|
||||
@@ -69,9 +69,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradable(qualityProfile,
|
||||
remoteBook.ParsedBookInfo.Quality,
|
||||
queuedItemPreferredWordScore,
|
||||
queuedItemCustomFormats,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore))
|
||||
subject.CustomFormats))
|
||||
{
|
||||
return Decision.Reject("Release in queue is of equal or higher preference: {0}", remoteBook.ParsedBookInfo.Quality);
|
||||
}
|
||||
@@ -80,7 +80,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradeAllowed(qualityProfile,
|
||||
remoteBook.ParsedBookInfo.Quality,
|
||||
subject.ParsedBookInfo.Quality))
|
||||
queuedItemCustomFormats,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.CustomFormats))
|
||||
{
|
||||
return Decision.Reject("Another release is queued and the Quality profile does not allow upgrades");
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
var title = subject.Release.Title;
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Author.Tags, subject.Release.IndexerId);
|
||||
|
||||
var required = releaseProfiles.Where(r => r.Required.IsNotNullOrWhiteSpace());
|
||||
var ignored = releaseProfiles.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
|
||||
var required = releaseProfiles.Where(r => r.Required.Any());
|
||||
var ignored = releaseProfiles.Where(r => r.Ignored.Any());
|
||||
|
||||
foreach (var r in required)
|
||||
{
|
||||
var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var requiredTerms = r.Required;
|
||||
|
||||
var foundTerms = ContainsAny(requiredTerms, title);
|
||||
if (foundTerms.Empty())
|
||||
@@ -50,7 +50,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
|
||||
foreach (var r in ignored)
|
||||
{
|
||||
var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var ignoredTerms = r.Ignored;
|
||||
|
||||
var foundTerms = ContainsAny(ignoredTerms, title);
|
||||
if (foundTerms.Any())
|
||||
|
||||
@@ -5,7 +5,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
@@ -16,21 +15,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
private readonly IUpgradableSpecification _upgradableSpecification;
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DelaySpecification(IPendingReleaseService pendingReleaseService,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
IDelayProfileService delayProfileService,
|
||||
IMediaFileService mediaFileService,
|
||||
IPreferredWordService preferredWordServiceCalculator,
|
||||
Logger logger)
|
||||
{
|
||||
_pendingReleaseService = pendingReleaseService;
|
||||
_upgradableSpecification = qualityUpgradableSpecification;
|
||||
_delayProfileService = delayProfileService;
|
||||
_mediaFileService = mediaFileService;
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -80,13 +76,29 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
}
|
||||
|
||||
// If quality meets or exceeds the best allowed quality in the profile accept it immediately
|
||||
var bestQualityInProfile = qualityProfile.LastAllowedQuality();
|
||||
var isBestInProfile = qualityComparer.Compare(subject.ParsedBookInfo.Quality.Quality, bestQualityInProfile) >= 0;
|
||||
|
||||
if (isBestInProfile && isPreferredProtocol)
|
||||
if (delayProfile.BypassIfHighestQuality)
|
||||
{
|
||||
var bestQualityInProfile = qualityProfile.LastAllowedQuality();
|
||||
var isBestInProfile = qualityComparer.Compare(subject.ParsedBookInfo.Quality.Quality, bestQualityInProfile) >= 0;
|
||||
|
||||
if (isBestInProfile && isPreferredProtocol)
|
||||
{
|
||||
_logger.Debug("Quality is highest in profile for preferred protocol, will not delay");
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
|
||||
// If quality meets or exceeds the best allowed quality in the profile accept it immediately
|
||||
if (delayProfile.BypassIfAboveCustomFormatScore)
|
||||
{
|
||||
var score = subject.CustomFormatScore;
|
||||
var minimum = delayProfile.MinimumCustomFormatScore;
|
||||
|
||||
if (score >= minimum && isPreferredProtocol)
|
||||
{
|
||||
_logger.Debug("Custom format score ({0}) meets minimum ({1}) for preferred protocol, will not delay", score, minimum);
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
|
||||
var bookIds = subject.Books.Select(e => e.Id);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -15,20 +16,20 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly ICustomFormatCalculationService _formatService;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public HistorySpecification(IHistoryService historyService,
|
||||
UpgradableSpecification qualityUpgradableSpecification,
|
||||
IConfigService configService,
|
||||
IPreferredWordService preferredWordServiceCalculator,
|
||||
Logger logger)
|
||||
UpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatService,
|
||||
IConfigService configService,
|
||||
Logger logger)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_upgradableSpecification = qualityUpgradableSpecification;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatService = formatService;
|
||||
_configService = configService;
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -60,23 +61,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||
continue;
|
||||
}
|
||||
|
||||
// The author will be the same as the one in history since it's the same book.
|
||||
// Instead of fetching the author from the DB reuse the known author.
|
||||
var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Author, mostRecent.SourceTitle, subject.Release?.IndexerId ?? 0);
|
||||
var customFormats = _formatService.ParseCustomFormat(mostRecent, subject.Author);
|
||||
|
||||
// The series will be the same as the one in history since it's the same episode.
|
||||
// Instead of fetching the series from the DB reuse the known series.
|
||||
var cutoffUnmet = _upgradableSpecification.CutoffNotMet(
|
||||
subject.Author.QualityProfile,
|
||||
new List<QualityModel> { mostRecent.Quality },
|
||||
preferredWordScore,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore);
|
||||
customFormats,
|
||||
subject.ParsedBookInfo.Quality);
|
||||
|
||||
var upgradeable = _upgradableSpecification.IsUpgradable(
|
||||
subject.Author.QualityProfile,
|
||||
mostRecent.Quality,
|
||||
preferredWordScore,
|
||||
customFormats,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore);
|
||||
subject.CustomFormats);
|
||||
|
||||
if (!cutoffUnmet)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
@@ -9,11 +11,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public interface IUpgradableSpecification
|
||||
{
|
||||
bool IsUpgradable(QualityProfile profile, QualityModel currentQualities, int currentScore, QualityModel newQuality, int newScore);
|
||||
bool IsUpgradable(QualityProfile profile, QualityModel currentQuality, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats);
|
||||
bool QualityCutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null);
|
||||
bool CutoffNotMet(QualityProfile profile, List<QualityModel> currentQualities, int currentScore, QualityModel newQuality = null, int newScore = 0);
|
||||
bool CutoffNotMet(QualityProfile profile, List<QualityModel> currentQualities, List<CustomFormat> currentFormats, QualityModel newQuality = null);
|
||||
bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality);
|
||||
bool IsUpgradeAllowed(QualityProfile qualityProfile, QualityModel currentQuality, QualityModel newQuality);
|
||||
bool IsUpgradeAllowed(QualityProfile qualityProfile, QualityModel currentQuality, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats);
|
||||
}
|
||||
|
||||
public class UpgradableSpecification : IUpgradableSpecification
|
||||
@@ -61,14 +63,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return ProfileComparisonResult.Upgrade;
|
||||
}
|
||||
|
||||
private bool IsPreferredWordUpgradable(int currentScore, int newScore)
|
||||
{
|
||||
_logger.Debug("Comparing preferred word score. Current: {0} New: {1}", currentScore, newScore);
|
||||
|
||||
return newScore > currentScore;
|
||||
}
|
||||
|
||||
public bool IsUpgradable(QualityProfile qualityProfile, QualityModel currentQualities, int currentScore, QualityModel newQuality, int newScore)
|
||||
public bool IsUpgradable(QualityProfile qualityProfile, QualityModel currentQualities, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats)
|
||||
{
|
||||
var qualityUpgrade = IsQualityUpgradable(qualityProfile, currentQualities, newQuality);
|
||||
|
||||
@@ -84,19 +79,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsPreferredWordUpgradable(currentScore, newScore))
|
||||
var currentFormatScore = qualityProfile.CalculateCustomFormatScore(currentCustomFormats);
|
||||
var newFormatScore = qualityProfile.CalculateCustomFormatScore(newCustomFormats);
|
||||
|
||||
if (newFormatScore <= currentFormatScore)
|
||||
{
|
||||
_logger.Debug("Existing item has a better preferred word score, skipping");
|
||||
_logger.Debug("New item's custom formats [{0}] do not improve on [{1}], skipping",
|
||||
newCustomFormats.ConcatToString(),
|
||||
currentCustomFormats.ConcatToString());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Debug("New item has a better preferred word score");
|
||||
_logger.Debug("New item has a better custom format score");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool QualityCutoffNotMet(QualityProfile profile, QualityModel currentQuality, QualityModel newQuality = null)
|
||||
{
|
||||
var cutoffCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality.Id, profile.Cutoff);
|
||||
var cutoff = profile.UpgradeAllowed ? profile.Cutoff : profile.FirstAllowedQuality().Id;
|
||||
var cutoffCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality.Id, cutoff);
|
||||
|
||||
if (cutoffCompare < 0)
|
||||
{
|
||||
@@ -111,7 +113,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CutoffNotMet(QualityProfile profile, List<QualityModel> currentQualities, int currentScore, QualityModel newQuality = null, int newScore = 0)
|
||||
private bool CustomFormatCutoffNotMet(QualityProfile profile, List<CustomFormat> currentFormats)
|
||||
{
|
||||
var score = profile.CalculateCustomFormatScore(currentFormats);
|
||||
return score < profile.CutoffFormatScore;
|
||||
}
|
||||
|
||||
public bool CutoffNotMet(QualityProfile profile, List<QualityModel> currentQualities, List<CustomFormat> currentFormats, QualityModel newQuality = null)
|
||||
{
|
||||
foreach (var quality in currentQualities)
|
||||
{
|
||||
@@ -121,7 +129,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
}
|
||||
}
|
||||
|
||||
if (IsPreferredWordUpgradable(currentScore, newScore))
|
||||
if (CustomFormatCutoffNotMet(profile, currentFormats))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -145,16 +153,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsUpgradeAllowed(QualityProfile qualityProfile, QualityModel currentQuality, QualityModel newQuality)
|
||||
public bool IsUpgradeAllowed(QualityProfile qualityProfile, QualityModel currentQuality, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats)
|
||||
{
|
||||
var isQualityUpgrade = IsQualityUpgradable(qualityProfile, currentQuality, newQuality);
|
||||
var isCustomFormatUpgrade = qualityProfile.CalculateCustomFormatScore(newCustomFormats) > qualityProfile.CalculateCustomFormatScore(currentCustomFormats);
|
||||
|
||||
return CheckUpgradeAllowed(qualityProfile, isQualityUpgrade);
|
||||
return CheckUpgradeAllowed(qualityProfile, isQualityUpgrade, isCustomFormatUpgrade);
|
||||
}
|
||||
|
||||
private bool CheckUpgradeAllowed(QualityProfile qualityProfile, ProfileComparisonResult isQualityUpgrade)
|
||||
private bool CheckUpgradeAllowed(QualityProfile qualityProfile, ProfileComparisonResult isQualityUpgrade, bool isCustomFormatUpgrade)
|
||||
{
|
||||
if (isQualityUpgrade == ProfileComparisonResult.Upgrade && !qualityProfile.UpgradeAllowed)
|
||||
if ((isQualityUpgrade == ProfileComparisonResult.Upgrade || isCustomFormatUpgrade) && qualityProfile.UpgradeAllowed)
|
||||
{
|
||||
_logger.Debug("Quality profile allows upgrading");
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((isQualityUpgrade == ProfileComparisonResult.Upgrade || isCustomFormatUpgrade) && !qualityProfile.UpgradeAllowed)
|
||||
{
|
||||
_logger.Debug("Quality profile does not allow upgrades, skipping");
|
||||
return false;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -11,12 +12,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
public class UpgradeAllowedSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly ICustomFormatCalculationService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeAllowedSpecification(UpgradableSpecification upgradableSpecification,
|
||||
Logger logger)
|
||||
Logger logger,
|
||||
ICustomFormatCalculationService formatService)
|
||||
{
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -35,11 +39,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileCustomFormats = _formatService.ParseCustomFormat(file, subject.Author);
|
||||
_logger.Debug("Comparing file quality with report. Existing files contain {0}", file.Quality);
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradeAllowed(qualityProfile,
|
||||
file.Quality,
|
||||
subject.ParsedBookInfo.Quality))
|
||||
fileCustomFormats,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.CustomFormats))
|
||||
{
|
||||
_logger.Debug("Upgrading is not allowed by the quality profile");
|
||||
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class UpgradeDiskSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly UpgradableSpecification _upgradableSpecification;
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
private readonly ICustomFormatCalculationService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification,
|
||||
IPreferredWordService preferredWordServiceCalculator,
|
||||
ICacheManager cacheManager,
|
||||
ICustomFormatCalculationService formatService,
|
||||
Logger logger)
|
||||
{
|
||||
_upgradableSpecification = qualityUpgradableSpecification;
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -35,13 +35,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var customFormats = _formatService.ParseCustomFormat(file);
|
||||
|
||||
if (!_upgradableSpecification.IsUpgradable(subject.Author.QualityProfile,
|
||||
file.Quality,
|
||||
_preferredWordServiceCalculator.Calculate(subject.Author, file.GetSceneOrFileName(), subject.Release?.IndexerId ?? 0),
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.PreferredWordScore))
|
||||
file.Quality,
|
||||
customFormats,
|
||||
subject.ParsedBookInfo.Quality,
|
||||
subject.CustomFormats))
|
||||
{
|
||||
return Decision.Reject("Existing files on disk is of equal or higher preference: {0}", file.Quality);
|
||||
return Decision.Reject("Existing files on disk is of equal or higher preference: {0}", file.Quality.Quality.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
|
||||
namespace NzbDrone.Core.Download.Aggregation.Aggregators
|
||||
{
|
||||
public class AggregatePreferredWordScore : IAggregateRemoteBook
|
||||
{
|
||||
private readonly IPreferredWordService _preferredWordServiceCalculator;
|
||||
|
||||
public AggregatePreferredWordScore(IPreferredWordService preferredWordServiceCalculator)
|
||||
{
|
||||
_preferredWordServiceCalculator = preferredWordServiceCalculator;
|
||||
}
|
||||
|
||||
public RemoteBook Aggregate(RemoteBook remoteBook)
|
||||
{
|
||||
remoteBook.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteBook.Author, remoteBook.Release.Title, remoteBook.Release.IndexerId);
|
||||
|
||||
return remoteBook;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ namespace NzbDrone.Core.Download.History
|
||||
history.Data.Add("Indexer", message.Book.Release.Indexer);
|
||||
history.Data.Add("DownloadClient", message.DownloadClient);
|
||||
history.Data.Add("DownloadClientName", message.DownloadClientName);
|
||||
history.Data.Add("PreferredWordScore", message.Book.PreferredWordScore.ToString());
|
||||
history.Data.Add("CustomFormatScore", message.Book.CustomFormatScore.ToString());
|
||||
|
||||
_repository.Insert(history);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Books.Events;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download.Aggregation;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Jobs;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -42,6 +44,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly IRemoteBookAggregationService _aggregationService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -52,6 +56,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
IDelayProfileService delayProfileService,
|
||||
ITaskManager taskManager,
|
||||
IConfigService configService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IRemoteBookAggregationService aggregationService,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
@@ -62,6 +68,8 @@ namespace NzbDrone.Core.Download.Pending
|
||||
_delayProfileService = delayProfileService;
|
||||
_taskManager = taskManager;
|
||||
_configService = configService;
|
||||
_formatCalculator = formatCalculator;
|
||||
_aggregationService = aggregationService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -311,6 +319,9 @@ namespace NzbDrone.Core.Download.Pending
|
||||
Release = release.Release
|
||||
};
|
||||
|
||||
_aggregationService.Augment(release.RemoteBook);
|
||||
release.RemoteBook.CustomFormats = _formatCalculator.ParseCustomFormat(release.RemoteBook, release.Release.Size);
|
||||
|
||||
result.Add(release);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Books.Events;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.History;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -32,6 +33,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IDownloadHistoryService _downloadHistoryService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICached<TrackedDownload> _cache;
|
||||
|
||||
@@ -40,10 +42,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
IHistoryService historyService,
|
||||
IEventAggregator eventAggregator,
|
||||
IDownloadHistoryService downloadHistoryService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
Logger logger)
|
||||
{
|
||||
_parsingService = parsingService;
|
||||
_historyService = historyService;
|
||||
_cache = cacheManager.GetCache<TrackedDownload>(GetType());
|
||||
_formatCalculator = formatCalculator;
|
||||
_eventAggregator = eventAggregator;
|
||||
_downloadHistoryService = downloadHistoryService;
|
||||
_cache = cacheManager.GetCache<TrackedDownload>(GetType());
|
||||
@@ -189,7 +194,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
}
|
||||
}
|
||||
|
||||
// Track it so it can be displayed in the queue even though we can't determine which author it is for
|
||||
// Calculate custom formats
|
||||
if (trackedDownload.RemoteBook != null)
|
||||
{
|
||||
trackedDownload.RemoteBook.CustomFormats = _formatCalculator.ParseCustomFormat(trackedDownload.RemoteBook, downloadItem.TotalSize);
|
||||
}
|
||||
|
||||
// Track it so it can be displayed in the queue even though we can't determine which artist it is for
|
||||
if (trackedDownload.RemoteBook == null)
|
||||
{
|
||||
_logger.Trace("No Book found for download '{0}'", trackedDownload.DownloadItem.Title);
|
||||
|
||||
@@ -118,14 +118,20 @@ namespace NzbDrone.Core.History
|
||||
|
||||
public List<EntityHistory> Since(DateTime date, EntityHistoryEventType? eventType)
|
||||
{
|
||||
var builder = Builder().Where<EntityHistory>(x => x.Date >= date);
|
||||
var builder = Builder()
|
||||
.Join<EntityHistory, Author>((h, a) => h.AuthorId == a.Id)
|
||||
.Where<EntityHistory>(x => x.Date >= date);
|
||||
|
||||
if (eventType.HasValue)
|
||||
{
|
||||
builder.Where<EntityHistory>(h => h.EventType == eventType);
|
||||
}
|
||||
|
||||
return Query(builder).OrderBy(h => h.Date).ToList();
|
||||
return _database.QueryJoined<EntityHistory, Author>(builder, (history, author) =>
|
||||
{
|
||||
history.Author = author;
|
||||
return history;
|
||||
}).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class CleanupQualityProfileFormatItems : IHousekeepingTask
|
||||
{
|
||||
private readonly IQualityProfileFormatItemsCleanupRepository _repository;
|
||||
private readonly ICustomFormatRepository _customFormatRepository;
|
||||
|
||||
public CleanupQualityProfileFormatItems(IQualityProfileFormatItemsCleanupRepository repository,
|
||||
ICustomFormatRepository customFormatRepository)
|
||||
{
|
||||
_repository = repository;
|
||||
_customFormatRepository = customFormatRepository;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
var test = _customFormatRepository.All();
|
||||
var customFormats = _customFormatRepository.All().ToDictionary(c => c.Id);
|
||||
var profiles = _repository.All();
|
||||
var updatedProfiles = new List<QualityProfile>();
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
var formatItems = new List<ProfileFormatItem>();
|
||||
|
||||
// Make sure the profile doesn't include formats that have been removed
|
||||
profile.FormatItems.ForEach(p =>
|
||||
{
|
||||
if (p.Format != null && customFormats.ContainsKey(p.Format.Id))
|
||||
{
|
||||
formatItems.Add(p);
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the profile includes all available formats
|
||||
foreach (var customFormat in customFormats)
|
||||
{
|
||||
if (formatItems.None(f => f.Format.Id == customFormat.Key))
|
||||
{
|
||||
formatItems.Insert(0, new ProfileFormatItem
|
||||
{
|
||||
Format = customFormat.Value,
|
||||
Score = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var previousIds = profile.FormatItems.Select(i => i.Format.Id).ToList();
|
||||
var ids = formatItems.Select(i => i.Format.Id).ToList();
|
||||
|
||||
// Update the profile if any formats were added or removed
|
||||
if (ids.Except(previousIds).Any() || previousIds.Except(ids).Any())
|
||||
{
|
||||
profile.FormatItems = formatItems;
|
||||
|
||||
if (profile.FormatItems.Empty())
|
||||
{
|
||||
profile.MinFormatScore = 0;
|
||||
profile.CutoffFormatScore = 0;
|
||||
}
|
||||
|
||||
updatedProfiles.Add(profile);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedProfiles.Any())
|
||||
{
|
||||
_repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IQualityProfileFormatItemsCleanupRepository : IBasicRepository<QualityProfile>
|
||||
{
|
||||
}
|
||||
|
||||
public class QualityProfileFormatItemsCleanupRepository : BasicRepository<QualityProfile>, IQualityProfileFormatItemsCleanupRepository
|
||||
{
|
||||
public QualityProfileFormatItemsCleanupRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,10 @@
|
||||
"BookStudio": "Book Studio",
|
||||
"BookTitle": "Book Title",
|
||||
"Branch": "Branch",
|
||||
"BypassIfAboveCustomFormatScore": "Bypass if Above Custom Format Score",
|
||||
"BypassIfAboveCustomFormatScoreHelpText": "Enable bypass when release has a score higher than the configured minimum custom format score",
|
||||
"BypassIfHighestQuality": "Bypass if Highest Quality",
|
||||
"BypassIfHighestQualityHelpText": "Bypass delay when release has the highest enabled quality in the quality profile",
|
||||
"BypassProxyForLocalAddresses": "Bypass Proxy for Local Addresses",
|
||||
"Calendar": "Calendar",
|
||||
"CalendarWeekColumnHeaderHelpText": "Shown above each column when week is the active view",
|
||||
@@ -144,6 +148,9 @@
|
||||
"CreateEmptyAuthorFoldersHelpText": "Create missing author folders during disk scan",
|
||||
"CreateGroup": "Create group",
|
||||
"CutoffHelpText": "Once this quality is reached Readarr will no longer download books",
|
||||
"Custom": "Custom",
|
||||
"CustomFilters": "Custom Filters",
|
||||
"CustomFormatScore": "Custom Format Score",
|
||||
"CutoffUnmet": "Cutoff Unmet",
|
||||
"DataAllBooks": "Monitor all books",
|
||||
"Database": "Database",
|
||||
@@ -429,6 +436,8 @@
|
||||
"MIA": "MIA",
|
||||
"MinimumAge": "Minimum Age",
|
||||
"MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.",
|
||||
"MinimumCustomFormatScore": "Minimum Custom Format Score",
|
||||
"MinimumCustomFormatScoreHelpText": "Minimum Custom Format Score required to bypass delay for the preferred protocol",
|
||||
"MinimumFreeSpace": "Minimum Free Space",
|
||||
"MinimumFreeSpaceWhenImportingHelpText": "Prevent import if it would leave less than this amount of disk space available",
|
||||
"MinimumLimits": "Minimum Limits",
|
||||
@@ -820,6 +829,8 @@
|
||||
"UnableToLoadImportListExclusions": "Unable to load Import List Exclusions",
|
||||
"UnableToLoadIndexerOptions": "Unable to load indexer options",
|
||||
"UnableToLoadIndexers": "Unable to load Indexers",
|
||||
"UnableToLoadInteractiveSearch": "Unable to load interactive search results",
|
||||
"UnableToLoadInteractiveSerach": "Unable to load results for this album search. Try again later",
|
||||
"UnableToLoadLists": "Unable to load Lists",
|
||||
"UnableToLoadMediaManagementSettings": "Unable to load Media Management settings",
|
||||
"UnableToLoadMetadata": "Unable to load Metadata",
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
public long Size { get; set; }
|
||||
public DateTime Modified { 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; }
|
||||
@@ -44,7 +45,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
|
||||
if (Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return System.IO.Path.GetFileName(Path);
|
||||
return System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation
|
||||
}
|
||||
|
||||
localTrack.Size = _diskProvider.GetFileSize(localTrack.Path);
|
||||
localTrack.SceneName = localTrack.SceneSource ? SceneNameCalculator.GetSceneName(localTrack) : null;
|
||||
|
||||
foreach (var augmenter in _trackAugmenters)
|
||||
{
|
||||
@@ -54,6 +55,8 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Aggregation
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = $"Unable to augment information for file: '{localTrack.Path}'. Author: {localTrack.Author} Error: {ex.Message}";
|
||||
|
||||
_logger.Warn(ex, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
|
||||
public class ImportDecisionMakerInfo
|
||||
{
|
||||
public DownloadClientItem DownloadClientItem { get; set; }
|
||||
public ParsedTrackInfo ParsedTrackInfo { get; set; }
|
||||
public ParsedBookInfo ParsedBookInfo { get; set; }
|
||||
}
|
||||
|
||||
public class ImportDecisionMakerConfig
|
||||
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Tuple<List<LocalBook>, List<ImportDecision<LocalBook>>> GetLocalTracks(List<IFileInfo> musicFiles, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter)
|
||||
public Tuple<List<LocalBook>, List<ImportDecision<LocalBook>>> GetLocalTracks(List<IFileInfo> musicFiles, DownloadClientItem downloadClientItem, ParsedBookInfo folderInfo, FilterFilesType filter)
|
||||
{
|
||||
var watch = new System.Diagnostics.Stopwatch();
|
||||
watch.Start();
|
||||
@@ -149,7 +149,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
|
||||
idOverrides = idOverrides ?? new IdentificationOverrides();
|
||||
itemInfo = itemInfo ?? new ImportDecisionMakerInfo();
|
||||
|
||||
var trackData = GetLocalTracks(musicFiles, itemInfo.DownloadClientItem, itemInfo.ParsedTrackInfo, config.Filter);
|
||||
var trackData = GetLocalTracks(musicFiles, itemInfo.DownloadClientItem, itemInfo.ParsedBookInfo, config.Filter);
|
||||
var localTracks = trackData.Item1;
|
||||
var decisions = trackData.Item2;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -11,6 +12,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
{
|
||||
public ManualImportItem()
|
||||
{
|
||||
CustomFormats = new List<CustomFormat>();
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
@@ -22,6 +24,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
public QualityModel Quality { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public List<CustomFormat> CustomFormats { get; set; }
|
||||
public IEnumerable<Rejection> Rejections { get; set; }
|
||||
public ParsedTrackInfo Tags { get; set; }
|
||||
public bool AdditionalFile { get; set; }
|
||||
|
||||
@@ -11,6 +11,7 @@ using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
@@ -43,6 +44,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
private readonly IProvideBookInfo _bookInfo;
|
||||
private readonly IMetadataTagService _metadataTagService;
|
||||
private readonly IImportApprovedBooks _importApprovedBooks;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly IDownloadedBooksImportService _downloadedTracksImportService;
|
||||
private readonly IProvideImportItemService _provideImportItemService;
|
||||
@@ -60,6 +62,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
IProvideBookInfo bookInfo,
|
||||
IMetadataTagService metadataTagService,
|
||||
IImportApprovedBooks importApprovedBooks,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
IDownloadedBooksImportService downloadedTracksImportService,
|
||||
IProvideImportItemService provideImportItemService,
|
||||
@@ -77,6 +80,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
_bookInfo = bookInfo;
|
||||
_metadataTagService = metadataTagService;
|
||||
_importApprovedBooks = importApprovedBooks;
|
||||
_formatCalculator = formatCalculator;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_downloadedTracksImportService = downloadedTracksImportService;
|
||||
_provideImportItemService = provideImportItemService;
|
||||
@@ -156,7 +160,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
var itemInfo = new ImportDecisionMakerInfo
|
||||
{
|
||||
DownloadClientItem = downloadClientItem,
|
||||
ParsedTrackInfo = Parser.Parser.ParseTitle(directoryInfo.Name)
|
||||
ParsedBookInfo = Parser.Parser.ParseBookTitle(directoryInfo.Name)
|
||||
};
|
||||
var config = new ImportDecisionMakerConfig
|
||||
{
|
||||
@@ -272,6 +276,8 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Manual
|
||||
if (decision.Item.Author != null)
|
||||
{
|
||||
item.Author = decision.Item.Author;
|
||||
|
||||
item.CustomFormats = _formatCalculator.ParseCustomFormat(decision.Item);
|
||||
}
|
||||
|
||||
if (decision.Item.Book != null)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.BookImport
|
||||
{
|
||||
public static class SceneNameCalculator
|
||||
{
|
||||
public static string GetSceneName(LocalBook localEpisode)
|
||||
{
|
||||
var downloadClientInfo = localEpisode.DownloadClientBookInfo;
|
||||
|
||||
if (downloadClientInfo != null && !downloadClientInfo.Discography)
|
||||
{
|
||||
return Parser.Parser.RemoveFileExtension(downloadClientInfo.ReleaseTitle);
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath());
|
||||
|
||||
if (SceneChecker.IsSceneTitle(fileName))
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
|
||||
var folderTitle = localEpisode.FolderTrackInfo?.ReleaseTitle;
|
||||
|
||||
if (localEpisode.FolderTrackInfo?.Discography == false &&
|
||||
folderTitle.IsNotNullOrWhiteSpace() &&
|
||||
SceneChecker.IsSceneTitle(folderTitle))
|
||||
{
|
||||
return folderTitle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
@@ -11,11 +12,15 @@ namespace NzbDrone.Core.MediaFiles.BookImport.Specifications
|
||||
public class UpgradeSpecification : IImportDecisionEngineSpecification<LocalBook>
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICustomFormatCalculationService _customFormatCalculationService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public UpgradeSpecification(IConfigService configService, Logger logger)
|
||||
public UpgradeSpecification(IConfigService configService,
|
||||
ICustomFormatCalculationService customFormatCalculationService,
|
||||
Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_customFormatCalculationService = customFormatCalculationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||
var idInfo = new ImportDecisionMakerInfo
|
||||
{
|
||||
DownloadClientItem = downloadClientItem,
|
||||
ParsedTrackInfo = trackInfo
|
||||
ParsedBookInfo = folderInfo
|
||||
};
|
||||
var idConfig = new ImportDecisionMakerConfig
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
@@ -17,6 +19,8 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
ReleaseTitle = remoteBook.Release.Title;
|
||||
Indexer = remoteBook.Release.Indexer;
|
||||
Size = remoteBook.Release.Size;
|
||||
CustomFormats = remoteBook.CustomFormats?.Select(x => x.Name).ToList();
|
||||
CustomFormatScore = remoteBook.CustomFormatScore;
|
||||
}
|
||||
|
||||
public string Quality { get; set; }
|
||||
@@ -25,5 +29,7 @@ namespace NzbDrone.Core.Notifications.Webhook
|
||||
public string ReleaseTitle { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public long Size { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public List<string> CustomFormats { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
@@ -18,7 +19,7 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
public interface IBuildFileNames
|
||||
{
|
||||
string BuildBookFileName(Author author, Edition edition, BookFile bookFile, NamingConfig namingConfig = null, List<string> preferredWords = null);
|
||||
string BuildBookFileName(Author author, Edition edition, BookFile bookFile, NamingConfig namingConfig = null, List<CustomFormat> customFormats = null);
|
||||
string BuildBookFilePath(Author author, Edition edition, string fileName, string extension);
|
||||
string BuildBookPath(Author author);
|
||||
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
|
||||
@@ -29,7 +30,7 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
private readonly INamingConfigService _namingConfigService;
|
||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
||||
private readonly IPreferredWordService _preferredWordService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly ICached<BookFormat[]> _trackFormatCache;
|
||||
private readonly Logger _logger;
|
||||
|
||||
@@ -59,17 +60,17 @@ namespace NzbDrone.Core.Organizer
|
||||
public FileNameBuilder(INamingConfigService namingConfigService,
|
||||
IQualityDefinitionService qualityDefinitionService,
|
||||
ICacheManager cacheManager,
|
||||
IPreferredWordService preferredWordService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
Logger logger)
|
||||
{
|
||||
_namingConfigService = namingConfigService;
|
||||
_qualityDefinitionService = qualityDefinitionService;
|
||||
_preferredWordService = preferredWordService;
|
||||
_formatCalculator = formatCalculator;
|
||||
_trackFormatCache = cacheManager.GetCache<BookFormat[]>(GetType(), "bookFormat");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string BuildBookFileName(Author author, Edition edition, BookFile bookFile, NamingConfig namingConfig = null, List<string> preferredWords = null)
|
||||
public string BuildBookFileName(Author author, Edition edition, BookFile bookFile, NamingConfig namingConfig = null, List<CustomFormat> customFormats = null)
|
||||
{
|
||||
if (namingConfig == null)
|
||||
{
|
||||
@@ -95,7 +96,7 @@ namespace NzbDrone.Core.Organizer
|
||||
AddBookFileTokens(tokenHandlers, bookFile);
|
||||
AddQualityTokens(tokenHandlers, author, bookFile);
|
||||
AddMediaInfoTokens(tokenHandlers, bookFile);
|
||||
AddPreferredWords(tokenHandlers, author, bookFile, preferredWords);
|
||||
AddCustomFormats(tokenHandlers, author, bookFile, customFormats);
|
||||
|
||||
var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var components = new List<string>();
|
||||
@@ -369,14 +370,15 @@ namespace NzbDrone.Core.Organizer
|
||||
tokenHandlers["{MediaInfo AudioSampleRate}"] = m => MediaInfoFormatter.FormatAudioSampleRate(bookFile.MediaInfo);
|
||||
}
|
||||
|
||||
private void AddPreferredWords(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Author author, BookFile bookFile, List<string> preferredWords = null)
|
||||
private void AddCustomFormats(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Author author, BookFile bookFile, List<CustomFormat> customFormats = null)
|
||||
{
|
||||
if (preferredWords == null)
|
||||
if (customFormats == null)
|
||||
{
|
||||
preferredWords = _preferredWordService.GetMatchingPreferredWords(author, bookFile.GetSceneOrFileName());
|
||||
bookFile.Author = author;
|
||||
customFormats = _formatCalculator.ParseCustomFormat(bookFile, author);
|
||||
}
|
||||
|
||||
tokenHandlers["{Preferred Words}"] = m => string.Join(" ", preferredWords);
|
||||
tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming));
|
||||
}
|
||||
|
||||
private string ReplaceTokens(string pattern, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, NamingConfig namingConfig)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
@@ -22,7 +23,7 @@ namespace NzbDrone.Core.Organizer
|
||||
private static Edition _standardEdition;
|
||||
private static BookFile _singleTrackFile;
|
||||
private static BookFile _multiTrackFile;
|
||||
private static List<string> _preferredWords;
|
||||
private static List<CustomFormat> _customFormats;
|
||||
|
||||
public FileNameSampleService(IBuildFileNames buildFileNames)
|
||||
{
|
||||
@@ -63,6 +64,20 @@ namespace NzbDrone.Core.Organizer
|
||||
Book = _standardBook
|
||||
};
|
||||
|
||||
_customFormats = new List<CustomFormat>
|
||||
{
|
||||
new CustomFormat
|
||||
{
|
||||
Name = "Surround Sound",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
},
|
||||
new CustomFormat
|
||||
{
|
||||
Name = "x264",
|
||||
IncludeCustomFormatWhenRenaming = true
|
||||
}
|
||||
};
|
||||
|
||||
var mediaInfo = new MediaInfoModel()
|
||||
{
|
||||
AudioFormat = "Flac Audio",
|
||||
@@ -95,11 +110,6 @@ namespace NzbDrone.Core.Organizer
|
||||
Part = 1,
|
||||
PartCount = 2
|
||||
};
|
||||
|
||||
_preferredWords = new List<string>
|
||||
{
|
||||
"iNTERNAL"
|
||||
};
|
||||
}
|
||||
|
||||
public SampleResult GetStandardTrackSample(NamingConfig nameSpec)
|
||||
@@ -137,7 +147,7 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
try
|
||||
{
|
||||
return _buildFileNames.BuildBookFileName(author, bookFile.Edition.Value, bookFile, nameSpec, _preferredWords);
|
||||
return _buildFileNames.BuildBookFileName(author, bookFile.Edition.Value, bookFile, nameSpec, _customFormats);
|
||||
}
|
||||
catch (NamingFormatException)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public long Size { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public ParsedTrackInfo FileTrackInfo { get; set; }
|
||||
public ParsedTrackInfo FolderTrackInfo { get; set; }
|
||||
public ParsedBookInfo FolderTrackInfo { get; set; }
|
||||
public ParsedBookInfo DownloadClientBookInfo { get; set; }
|
||||
public List<string> AcoustIdResults { get; set; }
|
||||
public Author Author { get; set; }
|
||||
@@ -27,6 +27,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public bool AdditionalFile { get; set; }
|
||||
public bool SceneSource { get; set; }
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string SceneName { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
@@ -15,6 +17,10 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string ReleaseHash { get; set; }
|
||||
public string ReleaseVersion { get; set; }
|
||||
public string ReleaseTitle { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
@@ -14,11 +15,13 @@ namespace NzbDrone.Core.Parser.Model
|
||||
public List<Book> Books { get; set; }
|
||||
public bool DownloadAllowed { get; set; }
|
||||
public TorrentSeedConfiguration SeedConfiguration { get; set; }
|
||||
public int PreferredWordScore { get; set; }
|
||||
public List<CustomFormat> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
|
||||
public RemoteBook()
|
||||
{
|
||||
Books = new List<Book>();
|
||||
CustomFormats = new List<CustomFormat>();
|
||||
}
|
||||
|
||||
public bool IsRecentBook()
|
||||
|
||||
@@ -489,7 +489,7 @@ namespace NzbDrone.Core.Parser
|
||||
Logger.Trace(regex);
|
||||
try
|
||||
{
|
||||
var result = ParseBookMatchCollection(match);
|
||||
var result = ParseBookMatchCollection(match, releaseTitle);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
@@ -780,7 +780,7 @@ namespace NzbDrone.Core.Parser
|
||||
return parseResult.AuthorName;
|
||||
}
|
||||
|
||||
private static ParsedBookInfo ParseBookMatchCollection(MatchCollection matchCollection)
|
||||
private static ParsedBookInfo ParseBookMatchCollection(MatchCollection matchCollection, string releaseTitle)
|
||||
{
|
||||
var authorName = matchCollection[0].Groups["author"].Value.Replace('.', ' ').Replace('_', ' ');
|
||||
var bookTitle = matchCollection[0].Groups["book"].Value.Replace('.', ' ').Replace('_', ' ');
|
||||
@@ -794,7 +794,10 @@ namespace NzbDrone.Core.Parser
|
||||
|
||||
ParsedBookInfo result;
|
||||
|
||||
result = new ParsedBookInfo();
|
||||
result = new ParsedBookInfo
|
||||
{
|
||||
ReleaseTitle = releaseTitle
|
||||
};
|
||||
|
||||
result.AuthorName = authorName;
|
||||
result.BookTitle = bookTitle;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers;
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace NzbDrone.Core.Profiles.Delay
|
||||
public int UsenetDelay { get; set; }
|
||||
public int TorrentDelay { get; set; }
|
||||
public int Order { get; set; }
|
||||
public bool BypassIfHighestQuality { get; set; }
|
||||
public bool BypassIfAboveCustomFormatScore { get; set; }
|
||||
public int MinimumCustomFormatScore { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public DelayProfile()
|
||||
|
||||
11
src/NzbDrone.Core/Profiles/ProfileFormatItem.cs
Normal file
11
src/NzbDrone.Core/Profiles/ProfileFormatItem.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class ProfileFormatItem : IEmbeddedDocument
|
||||
{
|
||||
public CustomFormat Format { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
@@ -7,11 +8,33 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
{
|
||||
public class QualityProfile : ModelBase
|
||||
{
|
||||
public QualityProfile()
|
||||
{
|
||||
FormatItems = new List<ProfileFormatItem>();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItem> FormatItems { get; set; }
|
||||
public List<QualityProfileQualityItem> Items { get; set; }
|
||||
|
||||
public Quality FirstAllowedQuality()
|
||||
{
|
||||
var firstAllowed = Items.First(q => q.Allowed);
|
||||
|
||||
if (firstAllowed.Quality != null)
|
||||
{
|
||||
return firstAllowed.Quality;
|
||||
}
|
||||
|
||||
// Returning any item from the group will work,
|
||||
// returning the first because it's the true first quality.
|
||||
return firstAllowed.Items.First().Quality;
|
||||
}
|
||||
|
||||
public Quality LastAllowedQuality()
|
||||
{
|
||||
var lastAllowed = Items.Last(q => q.Allowed);
|
||||
@@ -63,5 +86,10 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
|
||||
return new QualityIndex();
|
||||
}
|
||||
|
||||
public int CalculateCustomFormatScore(List<CustomFormat> formats)
|
||||
{
|
||||
return FormatItems.Where(x => formats.Contains(x.Format)).Sum(x => x.Score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -10,9 +13,44 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
|
||||
public class QualityProfileRepository : BasicRepository<QualityProfile>, IProfileRepository
|
||||
{
|
||||
public QualityProfileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
private readonly ICustomFormatService _customFormatService;
|
||||
|
||||
public QualityProfileRepository(IMainDatabase database,
|
||||
IEventAggregator eventAggregator,
|
||||
ICustomFormatService customFormatService)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
_customFormatService = customFormatService;
|
||||
}
|
||||
|
||||
protected override List<QualityProfile> Query(SqlBuilder builder)
|
||||
{
|
||||
var cfs = _customFormatService.All().ToDictionary(c => c.Id);
|
||||
|
||||
var profiles = base.Query(builder);
|
||||
|
||||
// Do the conversions from Id to full CustomFormat object here instead of in
|
||||
// CustomFormatIntConverter to remove need to for a static property containing
|
||||
// all the custom formats
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
var formatItems = new List<ProfileFormatItem>();
|
||||
|
||||
foreach (var formatItem in profile.FormatItems)
|
||||
{
|
||||
// Skip any format that has been removed, but the profile wasn't updated properly
|
||||
if (cfs.ContainsKey(formatItem.Format.Id))
|
||||
{
|
||||
formatItem.Format = cfs[formatItem.Format.Id];
|
||||
|
||||
formatItems.Add(formatItem);
|
||||
}
|
||||
}
|
||||
|
||||
profile.FormatItems = formatItems;
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public bool Exists(int id)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.CustomFormats.Events;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
@@ -21,17 +24,22 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
QualityProfile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed);
|
||||
}
|
||||
|
||||
public class QualityProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
||||
public class QualityProfileService : IProfileService,
|
||||
IHandle<ApplicationStartedEvent>,
|
||||
IHandle<CustomFormatAddedEvent>,
|
||||
IHandle<CustomFormatDeletedEvent>
|
||||
{
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IImportListFactory _importListFactory;
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly IRootFolderService _rootFolderService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public QualityProfileService(IProfileRepository profileRepository,
|
||||
IAuthorService authorService,
|
||||
IImportListFactory importListFactory,
|
||||
ICustomFormatService formatService,
|
||||
IRootFolderService rootFolderService,
|
||||
Logger logger)
|
||||
{
|
||||
@@ -39,6 +47,7 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
_authorService = authorService;
|
||||
_importListFactory = importListFactory;
|
||||
_rootFolderService = rootFolderService;
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -103,6 +112,39 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
Quality.FLAC);
|
||||
}
|
||||
|
||||
public void Handle(CustomFormatAddedEvent message)
|
||||
{
|
||||
var all = All();
|
||||
|
||||
foreach (var profile in all)
|
||||
{
|
||||
profile.FormatItems.Insert(0, new ProfileFormatItem
|
||||
{
|
||||
Score = 0,
|
||||
Format = message.CustomFormat
|
||||
});
|
||||
|
||||
Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(CustomFormatDeletedEvent message)
|
||||
{
|
||||
var all = All();
|
||||
foreach (var profile in all)
|
||||
{
|
||||
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != message.CustomFormat.Id).ToList();
|
||||
|
||||
if (profile.FormatItems.Empty())
|
||||
{
|
||||
profile.MinFormatScore = 0;
|
||||
profile.CutoffFormatScore = 0;
|
||||
}
|
||||
|
||||
Update(profile);
|
||||
}
|
||||
}
|
||||
|
||||
public QualityProfile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed)
|
||||
{
|
||||
var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.GroupWeight);
|
||||
@@ -141,11 +183,20 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||
groupId++;
|
||||
}
|
||||
|
||||
var formatItems = _formatService.All().Select(format => new ProfileFormatItem
|
||||
{
|
||||
Score = 0,
|
||||
Format = format
|
||||
}).ToList();
|
||||
|
||||
var qualityProfile = new QualityProfile
|
||||
{
|
||||
Name = name,
|
||||
Cutoff = profileCutoff,
|
||||
Items = items
|
||||
Items = items,
|
||||
MinFormatScore = 0,
|
||||
CutoffFormatScore = 0,
|
||||
FormatItems = formatItems
|
||||
};
|
||||
|
||||
return qualityProfile;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Books;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public interface IPreferredWordService
|
||||
{
|
||||
int Calculate(Author author, string title, int indexerId);
|
||||
List<string> GetMatchingPreferredWords(Author author, string title);
|
||||
}
|
||||
|
||||
public class PreferredWordService : IPreferredWordService
|
||||
{
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
private readonly ITermMatcherService _termMatcherService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public PreferredWordService(IReleaseProfileService releaseProfileService, ITermMatcherService termMatcherService, Logger logger)
|
||||
{
|
||||
_releaseProfileService = releaseProfileService;
|
||||
_termMatcherService = termMatcherService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public int Calculate(Author author, string title, int indexerId)
|
||||
{
|
||||
_logger.Trace("Calculating preferred word score for '{0}'", title);
|
||||
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(author.Tags, indexerId);
|
||||
var matchingPairs = new List<KeyValuePair<string, int>>();
|
||||
|
||||
foreach (var releaseProfile in releaseProfiles)
|
||||
{
|
||||
foreach (var preferredPair in releaseProfile.Preferred)
|
||||
{
|
||||
var term = preferredPair.Key;
|
||||
|
||||
if (_termMatcherService.IsMatch(term, title))
|
||||
{
|
||||
matchingPairs.Add(preferredPair);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var score = matchingPairs.Sum(p => p.Value);
|
||||
|
||||
_logger.Trace("Calculated preferred word score for '{0}': {1}", title, score);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
public List<string> GetMatchingPreferredWords(Author author, string title)
|
||||
{
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(author.Tags, 0);
|
||||
var matchingPairs = new List<KeyValuePair<string, int>>();
|
||||
|
||||
_logger.Trace("Calculating preferred word score for '{0}'", title);
|
||||
|
||||
foreach (var releaseProfile in releaseProfiles)
|
||||
{
|
||||
if (!releaseProfile.IncludePreferredWhenRenaming)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var preferredPair in releaseProfile.Preferred)
|
||||
{
|
||||
var term = preferredPair.Key;
|
||||
var matchingTerm = _termMatcherService.MatchingTerm(term, title);
|
||||
|
||||
if (matchingTerm.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
matchingPairs.Add(new KeyValuePair<string, int>(matchingTerm, preferredPair.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchingPairs.OrderByDescending(p => p.Value)
|
||||
.Select(p => p.Key)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,16 @@ namespace NzbDrone.Core.Profiles.Releases
|
||||
public class ReleaseProfile : ModelBase
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public List<KeyValuePair<string, int>> Preferred { get; set; }
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfile()
|
||||
{
|
||||
Enabled = true;
|
||||
Preferred = new List<KeyValuePair<string, int>>();
|
||||
IncludePreferredWhenRenaming = true;
|
||||
Required = new List<string>();
|
||||
Ignored = new List<string>();
|
||||
Tags = new HashSet<int>();
|
||||
IndexerId = 0;
|
||||
}
|
||||
|
||||
@@ -19,14 +19,11 @@ namespace NzbDrone.Core.Profiles.Releases
|
||||
|
||||
public class ReleaseProfileService : IReleaseProfileService
|
||||
{
|
||||
private readonly ReleaseProfilePreferredComparer _preferredComparer;
|
||||
private readonly IRestrictionRepository _repo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ReleaseProfileService(IRestrictionRepository repo, Logger logger)
|
||||
{
|
||||
_preferredComparer = new ReleaseProfilePreferredComparer();
|
||||
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -34,7 +31,6 @@ namespace NzbDrone.Core.Profiles.Releases
|
||||
public List<ReleaseProfile> All()
|
||||
{
|
||||
var all = _repo.All().ToList();
|
||||
all.ForEach(r => r.Preferred.Sort(_preferredComparer));
|
||||
|
||||
return all;
|
||||
}
|
||||
@@ -68,15 +64,11 @@ namespace NzbDrone.Core.Profiles.Releases
|
||||
|
||||
public ReleaseProfile Add(ReleaseProfile restriction)
|
||||
{
|
||||
restriction.Preferred.Sort(_preferredComparer);
|
||||
|
||||
return _repo.Insert(restriction);
|
||||
}
|
||||
|
||||
public ReleaseProfile Update(ReleaseProfile restriction)
|
||||
{
|
||||
restriction.Preferred.Sort(_preferredComparer);
|
||||
|
||||
return _repo.Update(restriction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ namespace NzbDrone.Core.Profiles.Releases.TermMatchers
|
||||
{
|
||||
public sealed class CaseInsensitiveTermMatcher : ITermMatcher
|
||||
{
|
||||
private readonly string _originalTerm;
|
||||
private readonly string _term;
|
||||
|
||||
public CaseInsensitiveTermMatcher(string term)
|
||||
{
|
||||
_originalTerm = term;
|
||||
_term = term.ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -18,7 +20,7 @@ namespace NzbDrone.Core.Profiles.Releases.TermMatchers
|
||||
{
|
||||
if (value.ToLowerInvariant().Contains(_term))
|
||||
{
|
||||
return _term;
|
||||
return _originalTerm;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Readarr.Http;
|
||||
@@ -11,10 +12,13 @@ namespace Readarr.Api.V1.Blocklist
|
||||
public class BlocklistController : Controller
|
||||
{
|
||||
private readonly IBlocklistService _blocklistService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
|
||||
public BlocklistController(IBlocklistService blocklistService)
|
||||
public BlocklistController(IBlocklistService blocklistService,
|
||||
ICustomFormatCalculationService formatCalculator)
|
||||
{
|
||||
_blocklistService = blocklistService;
|
||||
_formatCalculator = formatCalculator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -23,7 +27,7 @@ namespace Readarr.Api.V1.Blocklist
|
||||
var pagingResource = Request.ReadPagingResourceFromRequest<BlocklistResource>();
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<BlocklistResource, NzbDrone.Core.Blocklisting.Blocklist>("date", SortDirection.Descending);
|
||||
|
||||
return pagingSpec.ApplyToPage(_blocklistService.Paged, BlocklistResourceMapper.MapToResource);
|
||||
return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator));
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Author;
|
||||
using Readarr.Api.V1.CustomFormats;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Blocklist
|
||||
@@ -13,6 +15,7 @@ namespace Readarr.Api.V1.Blocklist
|
||||
public List<int> BookIds { get; set; }
|
||||
public string SourceTitle { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
@@ -23,7 +26,7 @@ namespace Readarr.Api.V1.Blocklist
|
||||
|
||||
public static class BlocklistResourceMapper
|
||||
{
|
||||
public static BlocklistResource MapToResource(this NzbDrone.Core.Blocklisting.Blocklist model)
|
||||
public static BlocklistResource MapToResource(this NzbDrone.Core.Blocklisting.Blocklist model, ICustomFormatCalculationService formatCalculator)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
@@ -38,6 +41,7 @@ namespace Readarr.Api.V1.Blocklist
|
||||
BookIds = model.BookIds,
|
||||
SourceTitle = model.SourceTitle,
|
||||
Quality = model.Quality,
|
||||
CustomFormats = formatCalculator.ParseCustomFormat(model, model.Author).ToResource(false),
|
||||
Date = model.Date,
|
||||
Protocol = model.Protocol,
|
||||
Indexer = model.Indexer,
|
||||
|
||||
114
src/Readarr.Api.V1/CustomFormats/CustomFormatController.cs
Normal file
114
src/Readarr.Api.V1/CustomFormats/CustomFormatController.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Readarr.Http;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.CustomFormats
|
||||
{
|
||||
[V1ApiController]
|
||||
public class CustomFormatController : RestController<CustomFormatResource>
|
||||
{
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly List<ICustomFormatSpecification> _specifications;
|
||||
|
||||
public CustomFormatController(ICustomFormatService formatService,
|
||||
List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
_formatService = formatService;
|
||||
_specifications = specifications;
|
||||
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Name)
|
||||
.Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
|
||||
SharedValidator.RuleFor(c => c.Specifications).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c).Custom((customFormat, context) =>
|
||||
{
|
||||
if (!customFormat.Specifications.Any())
|
||||
{
|
||||
context.AddFailure("Must contain at least one Condition");
|
||||
}
|
||||
|
||||
if (customFormat.Specifications.Any(s => s.Name.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure("Condition name(s) cannot be empty or consist of only spaces");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override CustomFormatResource GetResourceById(int id)
|
||||
{
|
||||
return _formatService.GetById(id).ToResource(true);
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Create(CustomFormatResource customFormatResource)
|
||||
{
|
||||
var model = customFormatResource.ToModel(_specifications);
|
||||
return Created(_formatService.Insert(model).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<CustomFormatResource> Update(CustomFormatResource resource)
|
||||
{
|
||||
var model = resource.ToModel(_specifications);
|
||||
_formatService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<CustomFormatResource> GetAll()
|
||||
{
|
||||
return _formatService.All().ToResource(true);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteFormat(int id)
|
||||
{
|
||||
_formatService.Delete(id);
|
||||
}
|
||||
|
||||
[HttpGet("schema")]
|
||||
public object GetTemplates()
|
||||
{
|
||||
var schema = _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList();
|
||||
|
||||
var presets = GetPresets();
|
||||
|
||||
foreach (var item in schema)
|
||||
{
|
||||
item.Presets = presets.Where(x => x.GetType().Name == item.Implementation).Select(x => x.ToSchema()).ToList();
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
private IEnumerable<ICustomFormatSpecification> GetPresets()
|
||||
{
|
||||
yield return new ReleaseTitleSpecification
|
||||
{
|
||||
Name = "Preferred Words",
|
||||
Value = @"\b(SPARKS|Framestor)\b"
|
||||
};
|
||||
|
||||
var formats = _formatService.All();
|
||||
foreach (var format in formats)
|
||||
{
|
||||
foreach (var condition in format.Specifications)
|
||||
{
|
||||
var preset = condition.Clone();
|
||||
preset.Name = $"{format.Name}: {preset.Name}";
|
||||
yield return preset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Readarr.Api.V1/CustomFormats/CustomFormatResource.cs
Normal file
75
src/Readarr.Api.V1/CustomFormats/CustomFormatResource.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using Readarr.Http.ClientSchema;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.CustomFormats
|
||||
{
|
||||
public class CustomFormatResource : RestResource
|
||||
{
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public override int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool? IncludeCustomFormatWhenRenaming { get; set; }
|
||||
public List<CustomFormatSpecificationSchema> Specifications { get; set; }
|
||||
}
|
||||
|
||||
public static class CustomFormatResourceMapper
|
||||
{
|
||||
public static CustomFormatResource ToResource(this CustomFormat model, bool includeDetails)
|
||||
{
|
||||
var resource = new CustomFormatResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name
|
||||
};
|
||||
|
||||
if (includeDetails)
|
||||
{
|
||||
resource.IncludeCustomFormatWhenRenaming = model.IncludeCustomFormatWhenRenaming;
|
||||
resource.Specifications = model.Specifications.Select(x => x.ToSchema()).ToList();
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
public static List<CustomFormatResource> ToResource(this IEnumerable<CustomFormat> models, bool includeDetails)
|
||||
{
|
||||
return models.Select(m => m.ToResource(includeDetails)).ToList();
|
||||
}
|
||||
|
||||
public static CustomFormat ToModel(this CustomFormatResource resource, List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
return new CustomFormat
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming ?? false,
|
||||
Specifications = resource.Specifications?.Select(x => MapSpecification(x, specifications)).ToList() ?? new List<ICustomFormatSpecification>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ICustomFormatSpecification MapSpecification(CustomFormatSpecificationSchema resource, List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
var matchingSpec =
|
||||
specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation);
|
||||
|
||||
if (matchingSpec is null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"{resource.Implementation} is not a valid specification implementation");
|
||||
}
|
||||
|
||||
var type = matchingSpec.GetType();
|
||||
|
||||
var spec = (ICustomFormatSpecification)SchemaBuilder.ReadFromSchema(resource.Fields, type);
|
||||
spec.Name = resource.Name;
|
||||
spec.Negate = resource.Negate;
|
||||
spec.Required = resource.Required;
|
||||
return spec;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using Readarr.Http.ClientSchema;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.CustomFormats
|
||||
{
|
||||
public class CustomFormatSpecificationSchema : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Implementation { get; set; }
|
||||
public string ImplementationName { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public bool Negate { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public List<Field> Fields { get; set; }
|
||||
public List<CustomFormatSpecificationSchema> Presets { get; set; }
|
||||
}
|
||||
|
||||
public static class CustomFormatSpecificationSchemaMapper
|
||||
{
|
||||
public static CustomFormatSpecificationSchema ToSchema(this ICustomFormatSpecification model)
|
||||
{
|
||||
return new CustomFormatSpecificationSchema
|
||||
{
|
||||
Name = model.Name,
|
||||
Implementation = model.GetType().Name,
|
||||
ImplementationName = model.ImplementationName,
|
||||
InfoLink = model.InfoLink,
|
||||
Negate = model.Negate,
|
||||
Required = model.Required,
|
||||
Fields = SchemaBuilder.ToSchema(model)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
@@ -17,21 +19,27 @@ namespace Readarr.Api.V1.History
|
||||
public class HistoryController : Controller
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly IUpgradableSpecification _upgradableSpecification;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
private readonly IAuthorService _authorService;
|
||||
|
||||
public HistoryController(IHistoryService historyService,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
IFailedDownloadService failedDownloadService)
|
||||
IFailedDownloadService failedDownloadService,
|
||||
IAuthorService authorService)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_formatCalculator = formatCalculator;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_failedDownloadService = failedDownloadService;
|
||||
_authorService = authorService;
|
||||
}
|
||||
|
||||
protected HistoryResource MapToResource(EntityHistory model, bool includeAuthor, bool includeBook)
|
||||
{
|
||||
var resource = model.ToResource();
|
||||
var resource = model.ToResource(_formatCalculator);
|
||||
|
||||
if (includeAuthor)
|
||||
{
|
||||
@@ -91,12 +99,24 @@ namespace Readarr.Api.V1.History
|
||||
[HttpGet("author")]
|
||||
public List<HistoryResource> GetAuthorHistory(int authorId, int? bookId = null, EntityHistoryEventType? eventType = null, bool includeAuthor = false, bool includeBook = false)
|
||||
{
|
||||
var author = _authorService.GetAuthor(authorId);
|
||||
|
||||
if (bookId.HasValue)
|
||||
{
|
||||
return _historyService.GetByBook(bookId.Value, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
|
||||
return _historyService.GetByBook(bookId.Value, eventType).Select(h =>
|
||||
{
|
||||
h.Author = author;
|
||||
|
||||
return MapToResource(h, includeAuthor, includeBook);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return _historyService.GetByAuthor(authorId, eventType).Select(h => MapToResource(h, includeAuthor, includeBook)).ToList();
|
||||
return _historyService.GetByAuthor(authorId, eventType).Select(h =>
|
||||
{
|
||||
h.Author = author;
|
||||
|
||||
return MapToResource(h, includeAuthor, includeBook);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[HttpPost("failed/{id}")]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Author;
|
||||
using Readarr.Api.V1.Books;
|
||||
using Readarr.Api.V1.CustomFormats;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.History
|
||||
@@ -14,6 +16,7 @@ namespace Readarr.Api.V1.History
|
||||
public int AuthorId { get; set; }
|
||||
public string SourceTitle { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public bool QualityCutoffNotMet { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
@@ -28,7 +31,7 @@ namespace Readarr.Api.V1.History
|
||||
|
||||
public static class HistoryResourceMapper
|
||||
{
|
||||
public static HistoryResource ToResource(this EntityHistory model)
|
||||
public static HistoryResource ToResource(this EntityHistory model, ICustomFormatCalculationService formatCalculator)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
@@ -43,6 +46,7 @@ namespace Readarr.Api.V1.History
|
||||
AuthorId = model.AuthorId,
|
||||
SourceTitle = model.SourceTitle,
|
||||
Quality = model.Quality,
|
||||
CustomFormats = formatCalculator.ParseCustomFormat(model, model.Author).ToResource(false),
|
||||
|
||||
//QualityCutoffNotMet
|
||||
Date = model.Date,
|
||||
|
||||
@@ -6,6 +6,7 @@ using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.CustomFormats;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Indexers
|
||||
@@ -40,7 +41,8 @@ namespace Readarr.Api.V1.Indexers
|
||||
public string InfoUrl { get; set; }
|
||||
public bool DownloadAllowed { get; set; }
|
||||
public int ReleaseWeight { get; set; }
|
||||
public int PreferredWordScore { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
|
||||
public string MagnetUrl { get; set; }
|
||||
public string InfoHash { get; set; }
|
||||
@@ -94,8 +96,9 @@ namespace Readarr.Api.V1.Indexers
|
||||
InfoUrl = releaseInfo.InfoUrl,
|
||||
DownloadAllowed = remoteBook.DownloadAllowed,
|
||||
|
||||
//ReleaseWeight
|
||||
PreferredWordScore = remoteBook.PreferredWordScore,
|
||||
// ReleaseWeight
|
||||
CustomFormatScore = remoteBook.CustomFormatScore,
|
||||
CustomFormats = remoteBook.CustomFormats.ToResource(false),
|
||||
|
||||
MagnetUrl = torrentInfo.MagnetUrl,
|
||||
InfoHash = torrentInfo.InfoHash,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Profiles.Delay;
|
||||
@@ -13,6 +13,9 @@ namespace Readarr.Api.V1.Profiles.Delay
|
||||
public DownloadProtocol PreferredProtocol { get; set; }
|
||||
public int UsenetDelay { get; set; }
|
||||
public int TorrentDelay { get; set; }
|
||||
public bool BypassIfHighestQuality { get; set; }
|
||||
public bool BypassIfAboveCustomFormatScore { get; set; }
|
||||
public int MinimumCustomFormatScore { get; set; }
|
||||
public int Order { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
}
|
||||
@@ -35,6 +38,9 @@ namespace Readarr.Api.V1.Profiles.Delay
|
||||
PreferredProtocol = model.PreferredProtocol,
|
||||
UsenetDelay = model.UsenetDelay,
|
||||
TorrentDelay = model.TorrentDelay,
|
||||
BypassIfHighestQuality = model.BypassIfHighestQuality,
|
||||
BypassIfAboveCustomFormatScore = model.BypassIfAboveCustomFormatScore,
|
||||
MinimumCustomFormatScore = model.MinimumCustomFormatScore,
|
||||
Order = model.Order,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
@@ -56,6 +62,9 @@ namespace Readarr.Api.V1.Profiles.Delay
|
||||
PreferredProtocol = resource.PreferredProtocol,
|
||||
UsenetDelay = resource.UsenetDelay,
|
||||
TorrentDelay = resource.TorrentDelay,
|
||||
BypassIfHighestQuality = resource.BypassIfHighestQuality,
|
||||
BypassIfAboveCustomFormatScore = resource.BypassIfAboveCustomFormatScore,
|
||||
MinimumCustomFormatScore = resource.MinimumCustomFormatScore,
|
||||
Order = resource.Order,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Readarr.Http;
|
||||
@@ -11,28 +14,45 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
[V1ApiController]
|
||||
public class QualityProfileController : RestController<QualityProfileResource>
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IProfileService _qualityProfileService;
|
||||
private readonly ICustomFormatService _formatService;
|
||||
|
||||
public QualityProfileController(IProfileService profileService)
|
||||
public QualityProfileController(IProfileService qualityProfileService, ICustomFormatService formatService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_qualityProfileService = qualityProfileService;
|
||||
_formatService = formatService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = _formatService.All().Select(f => f.Id).ToList();
|
||||
var ids = items.Select(i => i.Format);
|
||||
|
||||
return all.Except(ids).Empty();
|
||||
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
||||
SharedValidator.RuleFor(c => c).Custom((profile, context) =>
|
||||
{
|
||||
if (profile.FormatItems.Where(x => x.Score > 0).Sum(x => x.Score) < profile.MinFormatScore &&
|
||||
profile.FormatItems.Max(x => x.Score) < profile.MinFormatScore)
|
||||
{
|
||||
context.AddFailure("Minimum Custom Format Score can never be satisfied");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<QualityProfileResource> Create(QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
model = _qualityProfileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
_qualityProfileService.Delete(id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
@@ -40,20 +60,20 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
_qualityProfileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
protected override QualityProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
return _qualityProfileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<QualityProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
return _qualityProfileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
@@ -11,6 +13,9 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
public class QualityProfileQualityItemResource : RestResource
|
||||
@@ -26,6 +31,13 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
}
|
||||
}
|
||||
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
{
|
||||
public static QualityProfileResource ToResource(this QualityProfile model)
|
||||
@@ -41,7 +53,10 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
Name = model.Name,
|
||||
UpgradeAllowed = model.UpgradeAllowed,
|
||||
Cutoff = model.Cutoff,
|
||||
Items = model.Items.ConvertAll(ToResource)
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
MinFormatScore = model.MinFormatScore,
|
||||
CutoffFormatScore = model.CutoffFormatScore,
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +77,16 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
|
||||
{
|
||||
return new ProfileFormatItemResource
|
||||
{
|
||||
Format = model.Format.Id,
|
||||
Name = model.Format.Name,
|
||||
Score = model.Score
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfile ToModel(this QualityProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
@@ -75,7 +100,10 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
Name = resource.Name,
|
||||
UpgradeAllowed = resource.UpgradeAllowed,
|
||||
Cutoff = resource.Cutoff,
|
||||
Items = resource.Items.ConvertAll(ToModel)
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
MinFormatScore = resource.MinFormatScore,
|
||||
CutoffFormatScore = resource.CutoffFormatScore,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +124,15 @@ namespace Readarr.Api.V1.Profiles.Quality
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
|
||||
{
|
||||
return new ProfileFormatItem
|
||||
{
|
||||
Format = new CustomFormat { Id = resource.Format },
|
||||
Score = resource.Score
|
||||
};
|
||||
}
|
||||
|
||||
public static List<QualityProfileResource> ToResource(this IEnumerable<QualityProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Readarr.Api.V1.Profiles.Release
|
||||
|
||||
SharedValidator.RuleFor(r => r).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty())
|
||||
if (restriction.Ignored.Empty() && restriction.Required.Empty())
|
||||
{
|
||||
context.AddFailure("Either 'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
@@ -33,11 +33,6 @@ namespace Readarr.Api.V1.Profiles.Release
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
|
||||
}
|
||||
|
||||
if (restriction.Preferred.Any(p => p.Key.IsNullOrWhiteSpace()))
|
||||
{
|
||||
context.AddFailure("Preferred", "Term cannot be empty or consist of only spaces");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ namespace Readarr.Api.V1.Profiles.Release
|
||||
public class ReleaseProfileResource : RestResource
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public string Required { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public List<KeyValuePair<string, int>> Preferred { get; set; }
|
||||
public bool IncludePreferredWhenRenaming { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
@@ -37,8 +35,6 @@ namespace Readarr.Api.V1.Profiles.Release
|
||||
Enabled = model.Enabled,
|
||||
Required = model.Required,
|
||||
Ignored = model.Ignored,
|
||||
Preferred = model.Preferred,
|
||||
IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming,
|
||||
IndexerId = model.IndexerId,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
@@ -58,8 +54,6 @@ namespace Readarr.Api.V1.Profiles.Release
|
||||
Enabled = resource.Enabled,
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
Preferred = resource.Preferred,
|
||||
IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming,
|
||||
IndexerId = resource.IndexerId,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Readarr.Api.V1.Author;
|
||||
using Readarr.Api.V1.Books;
|
||||
using Readarr.Api.V1.CustomFormats;
|
||||
using Readarr.Http.REST;
|
||||
|
||||
namespace Readarr.Api.V1.Queue
|
||||
@@ -18,6 +19,7 @@ namespace Readarr.Api.V1.Queue
|
||||
public AuthorResource Author { get; set; }
|
||||
public BookResource Book { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public decimal Size { get; set; }
|
||||
public string Title { get; set; }
|
||||
public decimal Sizeleft { get; set; }
|
||||
@@ -53,6 +55,7 @@ namespace Readarr.Api.V1.Queue
|
||||
Author = includeAuthor && model.Author != null ? model.Author.ToResource() : null,
|
||||
Book = includeBook && model.Book != null ? model.Book.ToResource() : null,
|
||||
Quality = model.Quality,
|
||||
CustomFormats = model.RemoteBook?.CustomFormats?.ToResource(false),
|
||||
Size = model.Size,
|
||||
Title = model.Title,
|
||||
Sizeleft = model.Sizeleft,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace Readarr.Http.ClientSchema
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user