core: rewrite category code from scratch. resolves #8049 (#10031)

* Core: Categories are stored in a real tree
* Sorting: First Torznab categories sorted by Id and then custom cats sorted by Name
* Filtering: Results with child category are not removed when searching by parent category. Details in #8049
* Jacket UI: Add parent category when at least one child category exists
* Torznab (caps): Remove non existent children categories. Remove duplicated categories. Details in #10006
This commit is contained in:
Diego Heras
2020-11-01 12:07:24 +01:00
committed by GitHub
parent 4464d97e2f
commit 7a2e52659a
12 changed files with 360 additions and 153 deletions

View File

@@ -266,9 +266,21 @@ namespace Jackett.Common.Indexers
if (query.Categories.Length == 0)
return results;
var filteredResults = results.Where(
result => result.Category?.Any() != true || query.Categories.Intersect(result.Category).Any() ||
TorznabCatType.QueryContainsParentCategory(query.Categories, result.Category));
// TODO: move this code to TorznabCapabilitiesCategories and use indexer tree instead of general
// expand parent categories from the query
var expandedQueryCats = new List<int>();
foreach (var queryCategory in query.Categories)
{
expandedQueryCats.Add(queryCategory);
var parentCat = TorznabCatType.ParentCats.FirstOrDefault(c => c.ID == queryCategory);
if (parentCat != null)
expandedQueryCats.AddRange(parentCat.SubCategories.Select(c => c.ID));
}
var filteredResults = results.Where(result =>
result.Category?.Any() != true ||
expandedQueryCats.Intersect(result.Category).Any()
);
return filteredResults;
}

View File

@@ -51,14 +51,11 @@ namespace Jackett.Common.Models.DTO
site_link = indexer.SiteLink;
language = indexer.Language;
last_error = indexer.LastError;
potatoenabled = indexer.TorznabCaps.Categories.GetTorznabCategories().Any(i => TorznabCatType.Movies.Contains(i));
potatoenabled = indexer.TorznabCaps.Categories.GetTorznabCategoryTree().Any(i => TorznabCatType.Movies.Contains(i));
alternativesitelinks = indexer.AlternativeSiteLinks;
caps = indexer.TorznabCaps.Categories.GetTorznabCategories()
.GroupBy(p => p.ID)
.Select(g => g.First())
.OrderBy(c => c.ID < 100000 ? "z" + c.ID.ToString() : c.Name)
caps = indexer.TorznabCaps.Categories.GetTorznabCategoryList(true)
.Select(c => new Capability
{
ID = c.ID.ToString(),

View File

@@ -250,7 +250,7 @@ namespace Jackett.Common.Models
new XAttribute("available", MusicSearchAvailable ? "yes" : "no"),
new XAttribute("supportedParams", SupportedMusicSearchParams())
),
// inconsistend but apparently already used by various newznab indexers (see #1896)
// inconsistent but apparently already used by various newznab indexers (see #1896)
new XElement("audio-search",
new XAttribute("available", MusicSearchAvailable ? "yes" : "no"),
new XAttribute("supportedParams", SupportedMusicSearchParams())
@@ -261,7 +261,7 @@ namespace Jackett.Common.Models
)
),
new XElement("categories",
from c in Categories.GetTorznabCategories().OrderBy(x => x.ID < 100000 ? "z" + x.ID.ToString() : x.Name)
from c in Categories.GetTorznabCategoryTree(true)
select new XElement("category",
new XAttribute("id", c.ID),
new XAttribute("name", c.Name),

View File

@@ -6,21 +6,52 @@ namespace Jackett.Common.Models
{
public class TorznabCapabilitiesCategories
{
private readonly List<TorznabCategory> _categories = new List<TorznabCategory>();
private readonly List<CategoryMapping> _categoryMapping = new List<CategoryMapping>();
public List<TorznabCategory> GetTorznabCategories() => _categories;
private readonly List<TorznabCategory> _torznabCategoryTree = new List<TorznabCategory>();
public List<string> GetTrackerCategories() => _categoryMapping.Select(x => x.TrackerCategory).ToList();
public List<TorznabCategory> GetTorznabCategoryTree(bool sorted = false)
{
if (!sorted)
return _torznabCategoryTree;
// we build a new tree, original is unsorted
// first torznab categories ordered by id and then custom cats ordered by name
var sortedTree = _torznabCategoryTree
.Select(c =>
{
var sortedSubCats = c.SubCategories.OrderBy(x => x.ID);
var newCat = new TorznabCategory(c.ID, c.Name);
newCat.SubCategories.AddRange(sortedSubCats);
return newCat;
}).OrderBy(x => x.ID > 100000 ? "zzz" + x.Name : x.ID.ToString()).ToList();
return sortedTree;
}
public List<TorznabCategory> GetTorznabCategoryList(bool sorted = false)
{
var tree = GetTorznabCategoryTree(sorted);
// create a flat list (without subcategories)
var newFlatList = new List<TorznabCategory>();
foreach (var cat in tree)
{
newFlatList.Add(cat.CopyWithoutSubCategories());
newFlatList.AddRange(cat.SubCategories);
}
return newFlatList;
}
public void AddCategoryMapping(string trackerCategory, TorznabCategory torznabCategory, string trackerCategoryDesc = null)
{
// add torznab cat
_categoryMapping.Add(new CategoryMapping(trackerCategory, trackerCategoryDesc, torznabCategory.ID));
AddTorznabCategoryTree(torznabCategory);
if (!_categories.Contains(torznabCategory))
_categories.Add(torznabCategory);
// add 1:1 categories
// TODO: fix this. it's only working for integer "trackerCategory"
// create custom cats (1:1 categories)
if (trackerCategoryDesc != null && trackerCategory != null)
{
//TODO convert to int.TryParse() to avoid using throw as flow control
@@ -28,8 +59,7 @@ namespace Jackett.Common.Models
{
var trackerCategoryInt = int.Parse(trackerCategory);
var customCat = new TorznabCategory(trackerCategoryInt + 100000, trackerCategoryDesc);
if (!_categories.Contains(customCat))
_categories.Add(customCat);
AddTorznabCategoryTree(customCat);
}
catch (FormatException)
{
@@ -131,15 +161,54 @@ namespace Jackett.Common.Models
{
if (categories == null)
return false;
var subCategories = _categories.SelectMany(c => c.SubCategories);
var allCategories = _categories.Concat(subCategories);
var subCategories = _torznabCategoryTree.SelectMany(c => c.SubCategories);
var allCategories = _torznabCategoryTree.Concat(subCategories);
var supportsCategory = allCategories.Any(i => categories.Any(c => c == i.ID));
return supportsCategory;
}
public void Concat(TorznabCapabilitiesCategories rhs) =>
public void Concat(TorznabCapabilitiesCategories rhs)
{
// exclude indexer specific categories (>= 100000)
// we don't concat _categoryMapping because it makes no sense for the aggregate indexer
_categories.AddRange(rhs._categories.Where(x => x.ID < 100000).Except(_categories));
rhs.GetTorznabCategoryList().Where(x => x.ID < 100000).ToList().ForEach(AddTorznabCategoryTree);
}
private void AddTorznabCategoryTree(TorznabCategory torznabCategory)
{
// build the category tree
if (TorznabCatType.ParentCats.Contains(torznabCategory))
{
// parent cat
if (!_torznabCategoryTree.Contains(torznabCategory))
_torznabCategoryTree.Add(torznabCategory.CopyWithoutSubCategories());
}
else
{
// child or custom cat
var parentCat = TorznabCatType.ParentCats.FirstOrDefault(c => c.Contains(torznabCategory));
if (parentCat != null)
{
// child cat
var nodeCat = _torznabCategoryTree.FirstOrDefault(c => c.Equals(parentCat));
if (nodeCat != null)
{
// parent cat already exists
if (!nodeCat.Contains(torznabCategory))
nodeCat.SubCategories.Add(torznabCategory);
}
else
{
// create parent cat and add child
nodeCat = parentCat.CopyWithoutSubCategories();
nodeCat.SubCategories.Add(torznabCategory);
_torznabCategoryTree.Add(nodeCat);
}
}
else
// custom cat
_torznabCategoryTree.Add(torznabCategory);
}
}
}
}

View File

@@ -265,27 +265,8 @@ namespace Jackett.Common.Models
Other.SubCategories.AddRange(new List<TorznabCategory> {OtherMisc, OtherHashed});
}
public static bool QueryContainsParentCategory(int[] queryCats, ICollection<int> releaseCats)
{
//return (from releaseCat in releaseCats
// select AllCats.FirstOrDefault(c => c.ID == releaseCat)
// into cat
// where cat != null && queryCats != null
// select cat.SubCategories.Any(c => queryCats.Contains(c.ID)))
// .FirstOrDefault();
// Is equal to:
foreach (var releaseCat in releaseCats)
{
var cat = AllCats.FirstOrDefault(c => c.ID == releaseCat);
if (cat != null && queryCats != null)
return cat.SubCategories.Any(c => queryCats.Contains(c.ID));
}
return false;
}
public static string GetCatDesc(int newznabcat) =>
AllCats.FirstOrDefault(c => c.ID == newznabcat)?.Name ?? string.Empty;
public static string GetCatDesc(int torznabCatId) =>
AllCats.FirstOrDefault(c => c.ID == torznabCatId)?.Name ?? string.Empty;
public static TorznabCategory GetCatByName(string name) => AllCats.FirstOrDefault(c => c.Name == name);
}

View File

@@ -34,5 +34,7 @@ namespace Jackett.Common.Models
// Get Hash code should be calculated off read only properties.
// ID is not readonly
public override int GetHashCode() => ID;
public TorznabCategory CopyWithoutSubCategories() => new TorznabCategory(ID, Name);
}
}

View File

@@ -38,7 +38,7 @@ namespace Jackett.Test.Common.Indexers
Assert.False(caps.BookSearchAvailable);
Assert.False(caps.BookSearchTitleAvailable);
Assert.False(caps.BookSearchAuthorAvailable);
Assert.AreEqual(0, caps.Categories.GetTorznabCategories().Count);
Assert.AreEqual(0, caps.Categories.GetTorznabCategoryTree().Count);
}
[Test]
@@ -66,27 +66,25 @@ namespace Jackett.Test.Common.Indexers
var filteredResults = indexer._FilterResults(query, results).ToList();
Assert.AreEqual(4, filteredResults.Count);
// TODO: fix this, we should return MoviesSD and null
query = new TorznabQuery // with child category
{
Categories = new [] { TorznabCatType.MoviesSD.ID }
};
filteredResults = indexer._FilterResults(query, results).ToList();
Assert.AreEqual(2, filteredResults.Count);
Assert.AreEqual(TorznabCatType.MoviesSD.ID, filteredResults[0].Category.First());
Assert.AreEqual(null, filteredResults[1].Category);
query = new TorznabQuery // with parent category
{
Categories = new [] { TorznabCatType.Movies.ID }
};
filteredResults = indexer._FilterResults(query, results).ToList();
Assert.AreEqual(3, filteredResults.Count);
Assert.AreEqual(TorznabCatType.Movies.ID, filteredResults[0].Category.First());
Assert.AreEqual(TorznabCatType.MoviesSD.ID, filteredResults[1].Category.First());
Assert.AreEqual(null, filteredResults[2].Category);
// TODO: fix this, we should return Movies, MoviesSD and null
query = new TorznabQuery // with parent category
{
Categories = new [] { TorznabCatType.Movies.ID }
};
filteredResults = indexer._FilterResults(query, results).ToList();
Assert.AreEqual(2, filteredResults.Count);
Assert.AreEqual(TorznabCatType.Movies.ID, filteredResults[0].Category.First());
Assert.AreEqual(null, filteredResults[1].Category);
query = new TorznabQuery // with custom category
{
Categories = new [] { 100004 }
@@ -104,9 +102,17 @@ namespace Jackett.Test.Common.Indexers
// you can find more complex tests in TorznabCapabilitiesCategoriesTests.cs
indexer._AddCategoryMapping("11", TorznabCatType.MoviesSD, "MoviesSD");
Assert.AreEqual(2, indexer.TorznabCaps.Categories.GetTorznabCategories().Count);
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
new TorznabCategory(100011, "MoviesSD")
};
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, indexer.TorznabCaps.Categories.GetTorznabCategoryTree());
indexer._AddCategoryMapping(14, TorznabCatType.MoviesHD);
Assert.AreEqual(3, indexer.TorznabCaps.Categories.GetTorznabCategories().Count);
expected[0].SubCategories.Add(TorznabCatType.MoviesHD.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, indexer.TorznabCaps.Categories.GetTorznabCategoryTree());
}
[Test]
@@ -115,7 +121,7 @@ namespace Jackett.Test.Common.Indexers
var indexer = new TestWebIndexer();
indexer._AddMultiCategoryMapping(TorznabCatType.MoviesHD,19, 18);
Assert.AreEqual(1, indexer.TorznabCaps.Categories.GetTorznabCategories().Count);
Assert.AreEqual(1, indexer.TorznabCaps.Categories.GetTorznabCategoryTree().Count);
}
[Test]

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Jackett.Common.Indexers;
using Jackett.Common.Models;
using Jackett.Test.TestHelpers;
using NUnit.Framework;
namespace Jackett.Test.Common.Indexers
@@ -48,7 +49,7 @@ namespace Jackett.Test.Common.Indexers
Assert.False(indexer.TorznabCaps.BookSearchAvailable);
Assert.False(indexer.TorznabCaps.BookSearchTitleAvailable);
Assert.False(indexer.TorznabCaps.BookSearchAuthorAvailable);
Assert.AreEqual(0, indexer.TorznabCaps.Categories.GetTorznabCategories().Count);
Assert.AreEqual(0, indexer.TorznabCaps.Categories.GetTorznabCategoryTree().Count);
definition = new IndexerDefinition // test categories (same as in C# indexer)
{
@@ -91,16 +92,20 @@ namespace Jackett.Test.Common.Indexers
};
indexer = new CardigannIndexer(null, null, null, null, definition);
// TODO: test duplicates
var cats = indexer.TorznabCaps.Categories.GetTorznabCategories();
Assert.AreEqual(7, cats.Count);
Assert.AreEqual(2000, cats[0].ID);
Assert.AreEqual(2030, cats[1].ID);
Assert.AreEqual(7030, cats[2].ID);
Assert.AreEqual(1040, cats[3].ID);
Assert.AreEqual(100044, cats[4].ID);
Assert.AreEqual(1030, cats[5].ID);
Assert.AreEqual(100045, cats[6].ID);
// test categories
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.Console.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100045, "Console/Xbox_c2")
};
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, indexer.TorznabCaps.Categories.GetTorznabCategoryTree());
definition = new IndexerDefinition // test search modes
{

View File

@@ -37,14 +37,16 @@ namespace Jackett.Test.Common.Models.DTO
// test Jackett UI categories (internal JSON)
var dto = new Indexer(indexer);
var dtoCaps = dto.caps.ToList();
Assert.AreEqual(7, dtoCaps.Count);
Assert.AreEqual("100044", dtoCaps[0].ID);
Assert.AreEqual("100045", dtoCaps[1].ID);
Assert.AreEqual("1030", dtoCaps[2].ID);
Assert.AreEqual("1040", dtoCaps[3].ID);
Assert.AreEqual("2000", dtoCaps[4].ID);
Assert.AreEqual("2030", dtoCaps[5].ID);
Assert.AreEqual(9, dtoCaps.Count);
Assert.AreEqual("1000", dtoCaps[0].ID);
Assert.AreEqual("1030", dtoCaps[1].ID);
Assert.AreEqual("1040", dtoCaps[2].ID);
Assert.AreEqual("2000", dtoCaps[3].ID);
Assert.AreEqual("2030", dtoCaps[4].ID);
Assert.AreEqual("7000", dtoCaps[5].ID);
Assert.AreEqual("7030", dtoCaps[6].ID);
Assert.AreEqual("100044", dtoCaps[7].ID);
Assert.AreEqual("100040", dtoCaps[8].ID);
// movies categories enable potato search
Assert.True(dto.potatoenabled);

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Jackett.Common.Models;
using Jackett.Test.TestHelpers;
@@ -6,10 +7,6 @@ using Assert = NUnit.Framework.Assert;
namespace Jackett.Test.Common.Models
{
// TODO: add duplicates: different trackerCat but same newznabCat
// TODO: duplicates are not working well because we keep 2 internal lists with categories. One is de-duplicated
// and the other doesn't
[TestFixture]
public class TorznabCapabilitiesCategoriesTests
{
@@ -17,19 +14,10 @@ namespace Jackett.Test.Common.Models
public void TestConstructor()
{
var tcc = new TorznabCapabilitiesCategories();
Assert.IsEmpty(tcc.GetTorznabCategories());
Assert.IsEmpty(tcc.GetTorznabCategoryTree());
Assert.IsEmpty(tcc.GetTrackerCategories());
}
[Test]
public void TestGetTorznabCategories()
{
var tcc = CreateTestDataset();
var cats = tcc.GetTorznabCategories();
Assert.AreEqual(7, cats.Count);
Assert.AreEqual(2000, cats[0].ID);
}
[Test]
public void TestGetTrackerCategories()
{
@@ -39,44 +27,156 @@ namespace Jackett.Test.Common.Models
Assert.AreEqual("1", trackerCats[0]);
}
[Test]
public void TestGetTorznabCategoryTree()
{
var tcc = CreateTestDataset();
// unsorted tree
var cats = tcc.GetTorznabCategoryTree();
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.Console.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100040, "Console/Xbox_c2")
};
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, cats);
// sorted tree
cats = tcc.GetTorznabCategoryTree(true);
expected = new List<TorznabCategory>
{
TorznabCatType.Console.CopyWithoutSubCategories(),
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100040, "Console/Xbox_c2")
};
expected[0].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
expected[0].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, cats);
}
[Test]
public void TestGetTorznabCategoryList()
{
var tcc = CreateTestDataset();
// unsorted list
var cats = tcc.GetTorznabCategoryList();
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.MoviesSD.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.BooksComics.CopyWithoutSubCategories(),
TorznabCatType.Console.CopyWithoutSubCategories(),
TorznabCatType.ConsoleXBox.CopyWithoutSubCategories(),
TorznabCatType.ConsoleWii.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100040, "Console/Xbox_c2")
};
TestCategories.CompareCategoryTrees(expected, cats);
// sorted list
cats = tcc.GetTorznabCategoryList(true);
expected = new List<TorznabCategory>
{
TorznabCatType.Console.CopyWithoutSubCategories(),
TorznabCatType.ConsoleWii.CopyWithoutSubCategories(),
TorznabCatType.ConsoleXBox.CopyWithoutSubCategories(),
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.MoviesSD.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.BooksComics.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100040, "Console/Xbox_c2")
};
TestCategories.CompareCategoryTrees(expected, cats);
}
[Test]
public void TestAddCategoryMapping()
{
var tcc = new TorznabCapabilitiesCategories();
var cats = tcc.GetTorznabCategories();
var cats = tcc.GetTorznabCategoryTree();
// add "int" category (parent category)
// + Movies
tcc.AddCategoryMapping("1", TorznabCatType.Movies);
Assert.AreEqual(1, cats.Count);
Assert.AreEqual(2000, cats[0].ID);
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories()
};
TestCategories.CompareCategoryTrees(expected, cats);
// add "string" category (child category)
// - Movies
// + MoviesSD
tcc.AddCategoryMapping("mov_sd", TorznabCatType.MoviesSD);
Assert.AreEqual(2, cats.Count);
Assert.AreEqual(2030, cats[1].ID);
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, cats);
// add subcategory of books (child category)
// - Movies
// - MoviesSD
// + Books
// + BooksComics
tcc.AddCategoryMapping("33", TorznabCatType.BooksComics);
Assert.AreEqual(3, cats.Count);
Assert.AreEqual(7030, cats[2].ID);
expected.Add(TorznabCatType.Books.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, cats);
// add int category with description => custom category. it's converted into 2 different categories
// - Movies
// - MoviesSD
// - Books
// - BooksComics
// + Console
// + ConsoleXBox
// + Custom Cat "Console/Xbox_c"
tcc.AddCategoryMapping("44", TorznabCatType.ConsoleXBox, "Console/Xbox_c");
Assert.AreEqual(5, cats.Count);
Assert.AreEqual(1040, cats[3].ID);
Assert.AreEqual(100044, cats[4].ID);
expected.Add(TorznabCatType.Console.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected.Add(new TorznabCategory(100044, "Console/Xbox_c"));
TestCategories.CompareCategoryTrees(expected, cats);
// TODO: we should add a way to add custom categories for string categories
// https://github.com/Sonarr/Sonarr/wiki/Implementing-a-Torznab-indexer#caps-endpoint
// add string category with description. it's converted into 1 category
// - Movies
// - MoviesSD
// - Books
// - BooksComics
// - Console
// - ConsoleXBox
// + ConsoleWii
// - Custom Cat "Console/Xbox_c"
tcc.AddCategoryMapping("con_wii", TorznabCatType.ConsoleWii, "Console/Wii_c");
Assert.AreEqual(6, cats.Count);
Assert.AreEqual(1030, cats[5].ID);
expected[2].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, cats);
// add another int category with description that maps to ConsoleXbox (there are 2 tracker cats => 1 torznab cat)
// - Movies
// - MoviesSD
// - Books
// - BooksComics
// - Console
// - ConsoleXBox (this is not added again)
// - ConsoleWii
// - Custom Cat "Console/Xbox_c"
// + Custom Cat "Console/Xbox_c2"
tcc.AddCategoryMapping("45", TorznabCatType.ConsoleXBox, "Console/Xbox_c2");
Assert.AreEqual(7, cats.Count);
Assert.AreEqual(100045, cats[6].ID); // 1040 is duplicated and it is not added
expected.Add(new TorznabCategory(100045, "Console/Xbox_c2"));
TestCategories.CompareCategoryTrees(expected, cats);
}
[Test]
@@ -117,7 +217,7 @@ namespace Jackett.Test.Common.Models
trackerCats = tcc.MapTorznabCapsToTrackers(query);
Assert.AreEqual(2, trackerCats.Count);
Assert.AreEqual("44", trackerCats[0]);
Assert.AreEqual("45", trackerCats[1]);
Assert.AreEqual("40", trackerCats[1]);
query = new TorznabQuery // custom cat
{
@@ -155,10 +255,10 @@ namespace Jackett.Test.Common.Models
Assert.AreEqual(2, torznabCats.Count);
Assert.AreEqual(1040, torznabCats[0]);
Assert.AreEqual(100044, torznabCats[1]);
torznabCats = tcc.MapTrackerCatToNewznab("45").ToList();
torznabCats = tcc.MapTrackerCatToNewznab("40").ToList();
Assert.AreEqual(2, torznabCats.Count);
Assert.AreEqual(1040, torznabCats[0]);
Assert.AreEqual(100045, torznabCats[1]);
Assert.AreEqual(100040, torznabCats[1]);
// TODO: this is wrong, we are returning cat 109999 which doesn't exist
//torznabCats = tcc.MapTrackerCatToNewznab("9999").ToList(); // unknown cat
@@ -182,7 +282,7 @@ namespace Jackett.Test.Common.Models
torznabCats = tcc.MapTrackerCatDescToNewznab("Console/Xbox_c2").ToList();
Assert.AreEqual(2, torznabCats.Count);
Assert.AreEqual(1040, torznabCats[0]);
Assert.AreEqual(100045, torznabCats[1]);
Assert.AreEqual(100040, torznabCats[1]);
torznabCats = tcc.MapTrackerCatDescToNewznab("Console/Wii_c").ToList();
Assert.AreEqual(1, torznabCats.Count);
@@ -203,9 +303,8 @@ namespace Jackett.Test.Common.Models
Assert.True(tcc.SupportsCategories(new []{ TorznabCatType.Movies.ID })); // parent cat
Assert.True(tcc.SupportsCategories(new []{ TorznabCatType.MoviesSD.ID })); // child cat
Assert.True(tcc.SupportsCategories(new []{ TorznabCatType.Movies.ID, TorznabCatType.MoviesSD.ID })); // parent & child
Assert.True(tcc.SupportsCategories(new []{ 100044 })); // custom cat
// TODO: fix this
//Assert.False(tcc.SupportsCategories(new []{ TorznabCatType.Movies3D.ID })); // not supported child cat
Assert.True(tcc.SupportsCategories(new []{ 100040 })); // custom cat
Assert.False(tcc.SupportsCategories(new []{ TorznabCatType.Movies3D.ID })); // not supported child cat
Assert.False(tcc.SupportsCategories(new []{ 9999 })); // unknown cat
Assert.False(tcc.SupportsCategories(new int[]{})); // empty list
Assert.False(tcc.SupportsCategories(null)); // null
@@ -216,10 +315,36 @@ namespace Jackett.Test.Common.Models
{
var lhs = new TorznabCapabilitiesCategories();
var rhs = CreateTestDataset();
lhs.Concat(rhs);
Assert.AreEqual(5, lhs.GetTorznabCategories().Count); // removed custom cats
var expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.Console.CopyWithoutSubCategories()
};
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, lhs.GetTorznabCategoryTree()); // removed custom cats
Assert.AreEqual(0, lhs.GetTrackerCategories().Count); // removed tracker mapping
lhs = CreateTestDataset();
rhs = CreateTestDataset();
lhs.Concat(rhs);
expected = new List<TorznabCategory>
{
TorznabCatType.Movies.CopyWithoutSubCategories(),
TorznabCatType.Books.CopyWithoutSubCategories(),
TorznabCatType.Console.CopyWithoutSubCategories(),
new TorznabCategory(100044, "Console/Xbox_c"),
new TorznabCategory(100040, "Console/Xbox_c2")
};
expected[0].SubCategories.Add(TorznabCatType.MoviesSD.CopyWithoutSubCategories());
expected[1].SubCategories.Add(TorznabCatType.BooksComics.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleXBox.CopyWithoutSubCategories());
expected[2].SubCategories.Add(TorznabCatType.ConsoleWii.CopyWithoutSubCategories());
TestCategories.CompareCategoryTrees(expected, lhs.GetTorznabCategoryTree()); // check there are not duplicates
}
private static TorznabCapabilitiesCategories CreateTestDataset()

View File

@@ -42,7 +42,7 @@ namespace Jackett.Test.Common.Models
Assert.False(torznabCaps.BookSearchTitleAvailable);
Assert.False(torznabCaps.BookSearchAuthorAvailable);
Assert.IsEmpty(torznabCaps.Categories.GetTorznabCategories());
Assert.IsEmpty(torznabCaps.Categories.GetTorznabCategoryTree());
Assert.IsEmpty(torznabCaps.Categories.GetTrackerCategories());
}
@@ -405,40 +405,32 @@ namespace Jackett.Test.Common.Models
Assert.AreEqual("q,title,author", xDocumentSearching?.Element("book-search")?.Attribute("supportedParams")?.Value);
// test categories
torznabCaps = new TorznabCapabilities();
torznabCaps.Categories.AddCategoryMapping("c1", TorznabCatType.MoviesSD); // child category
torznabCaps = new TorznabCapabilities(); // child category
torznabCaps.Categories.AddCategoryMapping("c1", TorznabCatType.MoviesSD);
xDocument = torznabCaps.GetXDocument();
var xDocumentCategories = xDocument.Root?.Element("categories")?.Elements("category").ToList();
Assert.AreEqual(1, xDocumentCategories?.Count);
Assert.AreEqual(TorznabCatType.MoviesSD.ID.ToString(), xDocumentCategories?.First().Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.Name, xDocumentCategories?.First().Attribute("name")?.Value);
Assert.AreEqual(TorznabCatType.Movies.ID.ToString(), xDocumentCategories?[0].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.Movies.Name, xDocumentCategories?[0].Attribute("name")?.Value);
var xDocumentSubCategories = xDocumentCategories?[0]?.Elements("subcat").ToList();
Assert.AreEqual(1, xDocumentSubCategories?.Count);
Assert.AreEqual(TorznabCatType.MoviesSD.ID.ToString(), xDocumentSubCategories?[0].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.Name, xDocumentSubCategories?[0].Attribute("name")?.Value);
// TODO: child category is duplicated. should we add just parent and child without other subcats?
torznabCaps = new TorznabCapabilities();
torznabCaps.Categories.AddCategoryMapping("c1", TorznabCatType.Movies); // parent and child category
torznabCaps = new TorznabCapabilities(); // parent (with description generates a custom cat) and child category
torznabCaps.Categories.AddCategoryMapping("1", TorznabCatType.Movies, "Classic Movies");
torznabCaps.Categories.AddCategoryMapping("c2", TorznabCatType.MoviesSD);
xDocument = torznabCaps.GetXDocument();
xDocumentCategories = xDocument.Root?.Element("categories")?.Elements("category").ToList();
Assert.AreEqual(2, xDocumentCategories?.Count);
Assert.AreEqual(TorznabCatType.Movies.ID.ToString(), xDocumentCategories?.First().Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.Movies.Name, xDocumentCategories?.First().Attribute("name")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.ID.ToString(), xDocumentCategories?[1].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.Name, xDocumentCategories?[1].Attribute("name")?.Value);
var xDocumentSubCategories = xDocumentCategories?.First()?.Elements("subcat").ToList();
Assert.AreEqual(9, xDocumentSubCategories?.Count);
Assert.AreEqual(TorznabCatType.MoviesForeign.ID.ToString(), xDocumentSubCategories?.First().Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesForeign.Name, xDocumentSubCategories?.First().Attribute("name")?.Value);
torznabCaps = new TorznabCapabilities();
torznabCaps.Categories.AddCategoryMapping("c1", new TorznabCategory(100001, "CustomCat")); // custom category
torznabCaps.Categories.AddCategoryMapping("c2", TorznabCatType.MoviesSD);
xDocument = torznabCaps.GetXDocument();
xDocumentCategories = xDocument.Root?.Element("categories")?.Elements("category").ToList();
Assert.AreEqual(2, xDocumentCategories?.Count);
Assert.AreEqual("100001", xDocumentCategories?[0].Attribute("id")?.Value); // custom cats are first in the list
Assert.AreEqual("CustomCat", xDocumentCategories?[0].Attribute("name")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.ID.ToString(), xDocumentCategories?[1].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.Name, xDocumentCategories?[1].Attribute("name")?.Value);
Assert.AreEqual(TorznabCatType.Movies.ID.ToString(), xDocumentCategories?[0].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.Movies.Name, xDocumentCategories?[0].Attribute("name")?.Value);
xDocumentSubCategories = xDocumentCategories?[0]?.Elements("subcat").ToList();
Assert.AreEqual(1, xDocumentSubCategories?.Count);
Assert.AreEqual(TorznabCatType.MoviesSD.ID.ToString(), xDocumentSubCategories?[0].Attribute("id")?.Value);
Assert.AreEqual(TorznabCatType.MoviesSD.Name, xDocumentSubCategories?[0].Attribute("name")?.Value);
Assert.AreEqual("100001", xDocumentCategories?[1].Attribute("id")?.Value); // custom cats are first in the list
Assert.AreEqual("Classic Movies", xDocumentCategories?[1].Attribute("name")?.Value);
}
[Test]
@@ -450,15 +442,17 @@ namespace Jackett.Test.Common.Models
// test Torznab caps (XML) => more in Common.Model.TorznabCapabilitiesTests
var xDocument = torznabCaps.GetXDocument();
var xDocumentCategories = xDocument.Root?.Element("categories")?.Elements("category").ToList();
Assert.AreEqual(7, xDocumentCategories?.Count);
Assert.AreEqual("100044", xDocumentCategories?[0].Attribute("id")?.Value);
Assert.AreEqual("100045", xDocumentCategories?[1].Attribute("id")?.Value);
Assert.AreEqual("1030", xDocumentCategories?[2].Attribute("id")?.Value);
Assert.AreEqual("1040", xDocumentCategories?[3].Attribute("id")?.Value);
Assert.AreEqual("2000", xDocumentCategories?[4].Attribute("id")?.Value); // Movies
Assert.AreEqual("2030", xDocumentCategories?[5].Attribute("id")?.Value);
Assert.AreEqual("7030", xDocumentCategories?[6].Attribute("id")?.Value);
Assert.AreEqual(9, xDocumentCategories?[4]?.Elements("subcat").ToList().Count); // Movies
Assert.AreEqual(5, xDocumentCategories?.Count);
Assert.AreEqual("1000", xDocumentCategories?[0].Attribute("id")?.Value);
Assert.AreEqual(2, xDocumentCategories?[0]?.Elements("subcat").ToList().Count);
Assert.AreEqual("2000", xDocumentCategories?[1].Attribute("id")?.Value);
Assert.AreEqual(1, xDocumentCategories?[1]?.Elements("subcat").ToList().Count);
Assert.AreEqual("7000", xDocumentCategories?[2].Attribute("id")?.Value);
Assert.AreEqual(1, xDocumentCategories?[2]?.Elements("subcat").ToList().Count);
Assert.AreEqual("100044", xDocumentCategories?[3].Attribute("id")?.Value);
Assert.AreEqual(0, xDocumentCategories?[3]?.Elements("subcat").ToList().Count);
Assert.AreEqual("100040", xDocumentCategories?[4].Attribute("id")?.Value);
Assert.AreEqual(0, xDocumentCategories?[4]?.Elements("subcat").ToList().Count);
}
[Test]
@@ -473,7 +467,7 @@ namespace Jackett.Test.Common.Models
Assert.IsEmpty(res.MovieSearchParams);
Assert.IsEmpty(res.MusicSearchParams);
Assert.IsEmpty(res.BookSearchParams);
Assert.IsEmpty(res.Categories.GetTorznabCategories());
Assert.IsEmpty(res.Categories.GetTorznabCategoryTree());
torznabCaps1 = new TorznabCapabilities
{
@@ -502,7 +496,7 @@ namespace Jackett.Test.Common.Models
Assert.True(res.MovieSearchParams.Count == 2);
Assert.True(res.MusicSearchParams.Count == 2);
Assert.True(res.BookSearchParams.Count == 2);
Assert.True(res.Categories.GetTorznabCategories().Count == 3); // only CustomCat2 is removed
Assert.True(res.Categories.GetTorznabCategoryTree().Count == 3); // only CustomCat2 is removed
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Jackett.Common.Models;
using NUnit.Framework;
namespace Jackett.Test.TestHelpers
{
@@ -11,12 +13,24 @@ namespace Jackett.Test.TestHelpers
// - with and without description
// - parent and child categories
// - custom categories are not added automatically but they are created from other categories automatically
// - categories and subcategories are unsorted to test the sort when required
tcc.AddCategoryMapping("1", TorznabCatType.Movies);
tcc.AddCategoryMapping("mov_sd", TorznabCatType.MoviesSD);
tcc.AddCategoryMapping("33", TorznabCatType.BooksComics);
tcc.AddCategoryMapping("44", TorznabCatType.ConsoleXBox, "Console/Xbox_c");
tcc.AddCategoryMapping("con_wii", TorznabCatType.ConsoleWii, "Console/Wii_c");
tcc.AddCategoryMapping("45", TorznabCatType.ConsoleXBox, "Console/Xbox_c2");
tcc.AddCategoryMapping("40", TorznabCatType.ConsoleXBox, "Console/Xbox_c2");
}
public static void CompareCategoryTrees(List<TorznabCategory> tree1, List<TorznabCategory> tree2)
{
Assert.AreEqual(tree1.Count, tree2.Count);
for (var i = 0; i < tree1.Count; i++)
{
Assert.AreEqual(tree1[i].ID, tree2[i].ID);
Assert.AreEqual(tree1[i].Name, tree2[i].Name);
CompareCategoryTrees(tree1[i].SubCategories, tree2[i].SubCategories);
}
}
}
}