New: Custom Formats

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
This commit is contained in:
Qstick
2022-01-23 23:42:41 -06:00
parent 4a3062deae
commit dbb6ef7664
185 changed files with 6974 additions and 810 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")]

View File

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

View File

@@ -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")]

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Annotations
{
public interface ISelectOptionsConverter
{
List<SelectOption> GetSelectOptions();
}
}

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

View File

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

View 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;
// }
}
}

View 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)
{
}
}
}

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -207,7 +207,7 @@ namespace NzbDrone.Core.MediaFiles
var idInfo = new ImportDecisionMakerInfo
{
DownloadClientItem = downloadClientItem,
ParsedTrackInfo = trackInfo
ParsedBookInfo = folderInfo
};
var idConfig = new ImportDecisionMakerConfig
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -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}")]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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