diff --git a/src/Jackett.Common/Definitions/bibliotik.yml b/src/Jackett.Common/Definitions/bibliotik.yml index a86796d31..2a65cd1ca 100644 --- a/src/Jackett.Common/Definitions/bibliotik.yml +++ b/src/Jackett.Common/Definitions/bibliotik.yml @@ -18,7 +18,7 @@ caps: modes: search: [q] - book-search: [q, author, title] + book-search: [q, title, author] settings: - name: username diff --git a/src/Jackett.Common/Indexers/CardigannIndexer.cs b/src/Jackett.Common/Indexers/CardigannIndexer.cs index 3bb75cc18..b2e1259dc 100644 --- a/src/Jackett.Common/Indexers/CardigannIndexer.cs +++ b/src/Jackett.Common/Indexers/CardigannIndexer.cs @@ -111,16 +111,8 @@ namespace Jackett.Common.Indexers DefaultSiteLink += "/"; Language = Definition.Language; Type = Definition.Type; - TorznabCaps = new TorznabCapabilities - { - BookSearchAvailable = Definition.Caps.Modes.Any(c => c.Key == "book-search" && c.Value.Contains("author") && c.Value.Contains("title")) - }; - if (Definition.Caps.Modes.ContainsKey("tv-search")) - TorznabCaps.ParseTvSearchParams(Definition.Caps.Modes["tv-search"]); - if (Definition.Caps.Modes.ContainsKey("movie-search")) - TorznabCaps.ParseMovieSearchParams(Definition.Caps.Modes["movie-search"]); - if (Definition.Caps.Modes.ContainsKey("music-search")) - TorznabCaps.ParseMusicSearchParams(Definition.Caps.Modes["music-search"]); + TorznabCaps = new TorznabCapabilities(); + TorznabCaps.ParseCardigannSearchModes(Definition.Caps.Modes); // init config Data configData = new ConfigurationData(); diff --git a/src/Jackett.Common/Models/TorznabCapabilities.cs b/src/Jackett.Common/Models/TorznabCapabilities.cs index b8c0a1bc6..0737d1212 100644 --- a/src/Jackett.Common/Models/TorznabCapabilities.cs +++ b/src/Jackett.Common/Models/TorznabCapabilities.cs @@ -31,6 +31,13 @@ namespace Jackett.Common.Models Year } + public enum BookSearchParam + { + Q, + Title, + Author + } + public class TorznabCapabilities { public int? LimitsMax { get; set; } @@ -59,9 +66,12 @@ namespace Jackett.Common.Models public bool MusicSearchLabelAvailable => (MusicSearchParams.Contains(MusicSearchParam.Label)); public bool MusicSearchYearAvailable => (MusicSearchParams.Contains(MusicSearchParam.Year)); - public bool BookSearchAvailable { get; set; } + public List BookSearchParams; + public bool BookSearchAvailable => (BookSearchParams.Count > 0); + public bool BookSearchTitleAvailable => (BookSearchParams.Contains(BookSearchParam.Title)); + public bool BookSearchAuthorAvailable => (BookSearchParams.Contains(BookSearchParam.Author)); - public List Categories { get; private set; } + public List Categories { get; set; } public TorznabCapabilities() { @@ -69,11 +79,41 @@ namespace Jackett.Common.Models TvSearchParams = new List(); MovieSearchParams = new List(); MusicSearchParams = new List(); - BookSearchAvailable = false; + BookSearchParams = new List(); Categories = new List(); } - public void ParseTvSearchParams(IEnumerable paramsList) + public void ParseCardigannSearchModes(Dictionary> modes) + { + if (modes == null || !modes.Any()) + throw new Exception("At least one search mode is required"); + if (!modes.ContainsKey("search")) + throw new Exception("The search mode 'search' is mandatory"); + foreach (var entry in modes) + switch (entry.Key) + { + case "search": + if (entry.Value == null || entry.Value.Count != 1 || entry.Value[0] != "q") + throw new Exception("In search mode 'search' only 'q' parameter is supported and it's mandatory"); + break; + case "tv-search": + ParseTvSearchParams(entry.Value); + break; + case "movie-search": + ParseMovieSearchParams(entry.Value); + break; + case "music-search": + ParseMusicSearchParams(entry.Value); + break; + case "book-search": + ParseBookSearchParams(entry.Value); + break; + default: + throw new Exception($"Unsupported search mode: {entry.Key}"); + } + } + + private void ParseTvSearchParams(IEnumerable paramsList) { if (paramsList == null) return; @@ -87,7 +127,7 @@ namespace Jackett.Common.Models throw new Exception($"Not supported tv-search param: {paramStr}"); } - public void ParseMovieSearchParams(IEnumerable paramsList) + private void ParseMovieSearchParams(IEnumerable paramsList) { if (paramsList == null) return; @@ -101,7 +141,7 @@ namespace Jackett.Common.Models throw new Exception($"Not supported movie-search param: {paramStr}"); } - public void ParseMusicSearchParams(IEnumerable paramsList) + private void ParseMusicSearchParams(IEnumerable paramsList) { if (paramsList == null) return; @@ -112,7 +152,21 @@ namespace Jackett.Common.Models else throw new Exception($"Duplicate music-search param: {paramStr}"); else - throw new Exception($"Not supported Music-search param: {paramStr}"); + throw new Exception($"Not supported music-search param: {paramStr}"); + } + + private void ParseBookSearchParams(IEnumerable paramsList) + { + if (paramsList == null) + return; + foreach (var paramStr in paramsList) + if (Enum.TryParse(paramStr, true, out BookSearchParam param)) + if (!BookSearchParams.Contains(param)) + BookSearchParams.Add(param); + else + throw new Exception($"Duplicate book-search param: {paramStr}"); + else + throw new Exception($"Not supported book-search param: {paramStr}"); } private string SupportedTvSearchParams() @@ -155,15 +209,14 @@ namespace Jackett.Common.Models return string.Join(",", parameters); } - private string SupportedBookSearchParams + private string SupportedBookSearchParams() { - get - { - var parameters = new List() { "q" }; - if (BookSearchAvailable) - parameters.Add("author,title"); - return string.Join(",", parameters); - } + var parameters = new List { "q" }; // q is always enabled + if (BookSearchTitleAvailable) + parameters.Add("title"); + if (BookSearchAuthorAvailable) + parameters.Add("author"); + return string.Join(",", parameters); } public bool SupportsCategories(int[] categories) @@ -212,7 +265,7 @@ namespace Jackett.Common.Models ), new XElement("book-search", new XAttribute("available", BookSearchAvailable ? "yes" : "no"), - new XAttribute("supportedParams", SupportedBookSearchParams) + new XAttribute("supportedParams", SupportedBookSearchParams()) ) ), new XElement("categories", @@ -241,7 +294,7 @@ namespace Jackett.Common.Models lhs.TvSearchParams = lhs.TvSearchParams.Union(rhs.TvSearchParams).ToList(); lhs.MovieSearchParams = lhs.MovieSearchParams.Union(rhs.MovieSearchParams).ToList(); lhs.MusicSearchParams = lhs.MusicSearchParams.Union(rhs.MusicSearchParams).ToList(); - lhs.BookSearchAvailable = lhs.BookSearchAvailable || rhs.BookSearchAvailable; + lhs.BookSearchParams = lhs.BookSearchParams.Union(rhs.BookSearchParams).ToList(); lhs.Categories.AddRange(rhs.Categories.Where(x => x.ID < 100000).Except(lhs.Categories)); // exclude indexer specific categories (>= 100000) return lhs; } diff --git a/src/Jackett.Test/Common/Models/TorznabCapabilitiesTests.cs b/src/Jackett.Test/Common/Models/TorznabCapabilitiesTests.cs index 43e56f8ec..3e2c3b11e 100644 --- a/src/Jackett.Test/Common/Models/TorznabCapabilitiesTests.cs +++ b/src/Jackett.Test/Common/Models/TorznabCapabilitiesTests.cs @@ -36,29 +36,47 @@ namespace Jackett.Test.Common.Models Assert.False(torznabCaps.MusicSearchLabelAvailable); Assert.False(torznabCaps.MusicSearchYearAvailable); + Assert.IsEmpty(torznabCaps.BookSearchParams); Assert.False(torznabCaps.BookSearchAvailable); + Assert.False(torznabCaps.BookSearchTitleAvailable); + Assert.False(torznabCaps.BookSearchAuthorAvailable); Assert.IsEmpty(torznabCaps.Categories); } [Test] - public void TestParseMovieSearchParams() + public void TestParseCardigannSearchModes() { var torznabCaps = new TorznabCapabilities(); - torznabCaps.ParseMovieSearchParams(null); - Assert.IsEmpty(torznabCaps.MovieSearchParams); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List {"q"}}, + {"tv-search", new List {"q"}}, + {"movie-search", new List {"q"}}, + {"music-search", new List {"q"}}, + {"book-search", new List {"q"}} + }); + Assert.True(torznabCaps.SearchAvailable); + Assert.True(torznabCaps.TvSearchAvailable); + Assert.True(torznabCaps.MovieSearchAvailable); + Assert.True(torznabCaps.MusicSearchAvailable); + Assert.True(torznabCaps.BookSearchAvailable); torznabCaps = new TorznabCapabilities(); - torznabCaps.ParseMovieSearchParams(new List()); - Assert.IsEmpty(torznabCaps.MovieSearchParams); + try + { + torznabCaps.ParseCardigannSearchModes(null); // null search modes + Assert.Fail(); + } + catch (Exception) + { + // ignored + } torznabCaps = new TorznabCapabilities(); - torznabCaps.ParseMovieSearchParams(new List {"q", "imdbid"}); - Assert.AreEqual(new List { MovieSearchParam.Q, MovieSearchParam.ImdbId }, torznabCaps.MovieSearchParams); - - torznabCaps = new TorznabCapabilities(); - try { - torznabCaps.ParseMovieSearchParams(new List {"q", "q"}); // duplicate param + try + { + torznabCaps.ParseCardigannSearchModes(new Dictionary>()); // empty search modes Assert.Fail(); } catch (Exception) @@ -68,7 +86,247 @@ namespace Jackett.Test.Common.Models torznabCaps = new TorznabCapabilities(); try { - torznabCaps.ParseMovieSearchParams(new List {"bad"}); // unsupported param + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"bad", new List {"q"}} // bad search mode + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List {"bad"}} // search mode with bad parameters + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + } + + [Test] + public void TestParseTvSearchParams() + { + var torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"tv-search", null} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"tv-search", new List()} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"tv-search", new List {"q", "tvdbid"}} + }); + Assert.AreEqual(new List { TvSearchParam.Q, TvSearchParam.TvdbId }, torznabCaps.TvSearchParams); + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"tv-search", new List {"q", "q"}} // duplicate param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"tv-search", new List {"bad"}} // unsupported param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + } + + [Test] + public void TestParseMovieSearchParams() + { + var torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"movie-search", null} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"movie-search", new List()} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"movie-search", new List {"q", "imdbid"}} + }); + Assert.AreEqual(new List { MovieSearchParam.Q, MovieSearchParam.ImdbId }, torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"movie-search", new List {"q", "q"}} // duplicate param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"movie-search", new List {"bad"}} // unsupported param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + } + + [Test] + public void TestParseMusicSearchParams() + { + var torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"music-search", null} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"music-search", new List()} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"music-search", new List {"q", "label"}} + }); + Assert.AreEqual(new List { MusicSearchParam.Q, MusicSearchParam.Label }, torznabCaps.MusicSearchParams); + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"music-search", new List {"q", "q"}} // duplicate param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"music-search", new List {"bad"}} // unsupported param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + } + + [Test] + public void TestParseBookSearchParams() + { + var torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"book-search", null} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"book-search", new List()} + }); + Assert.IsEmpty(torznabCaps.MovieSearchParams); + + torznabCaps = new TorznabCapabilities(); + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"book-search", new List {"q", "title"}} + }); + Assert.AreEqual(new List { BookSearchParam.Q, BookSearchParam.Title }, torznabCaps.BookSearchParams); + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"book-search", new List {"q", "q"}} // duplicate param + }); + Assert.Fail(); + } + catch (Exception) + { + // ignored + } + + torznabCaps = new TorznabCapabilities(); + try { + torznabCaps.ParseCardigannSearchModes(new Dictionary> + { + {"search", new List{"q"}}, + {"book-search", new List {"bad"}} // unsupported param + }); Assert.Fail(); } catch (Exception) @@ -88,7 +346,6 @@ namespace Jackett.Test.Common.Models Assert.True(xDocument.Root?.Element("searching")?.HasElements); Assert.False(xDocument.Root?.Element("categories")?.HasElements); - // TODO: remove params when it's disabled. Review Torznab specs // test all features disabled torznabCaps = new TorznabCapabilities { @@ -109,7 +366,6 @@ namespace Jackett.Test.Common.Models Assert.AreEqual("no", xDoumentSearching?.Element("book-search")?.Attribute("available")?.Value); Assert.AreEqual("q", xDoumentSearching?.Element("book-search")?.Attribute("supportedParams")?.Value); - // TODO: book parameters should be configurable? // test all features enabled torznabCaps = new TorznabCapabilities { @@ -126,7 +382,10 @@ namespace Jackett.Test.Common.Models { MusicSearchParam.Q, MusicSearchParam.Album, MusicSearchParam.Artist, MusicSearchParam.Label, MusicSearchParam.Year }, - BookSearchAvailable = true + BookSearchParams = new List + { + BookSearchParam.Q, BookSearchParam.Title, BookSearchParam.Author + }, }; xDocument = torznabCaps.GetXDocument(); xDoumentSearching = xDocument.Root?.Element("searching"); @@ -141,7 +400,7 @@ namespace Jackett.Test.Common.Models Assert.AreEqual("yes", xDoumentSearching?.Element("audio-search")?.Attribute("available")?.Value); Assert.AreEqual("q,album,artist,label,year", xDoumentSearching?.Element("audio-search")?.Attribute("supportedParams")?.Value); Assert.AreEqual("yes", xDoumentSearching?.Element("book-search")?.Attribute("available")?.Value); - Assert.AreEqual("q,author,title", xDoumentSearching?.Element("book-search")?.Attribute("supportedParams")?.Value); + Assert.AreEqual("q,title,author", xDoumentSearching?.Element("book-search")?.Attribute("supportedParams")?.Value); // test categories torznabCaps = new TorznabCapabilities diff --git a/src/Jackett.Test/Torznab/TorznabTests.cs b/src/Jackett.Test/Torznab/TorznabTests.cs index 53a1a684d..b99e897d7 100644 --- a/src/Jackett.Test/Torznab/TorznabTests.cs +++ b/src/Jackett.Test/Torznab/TorznabTests.cs @@ -55,7 +55,10 @@ namespace Jackett.Test.Torznab Assert.False(TorznabCaps.MusicSearchArtistAvailable); Assert.False(TorznabCaps.MusicSearchLabelAvailable); Assert.False(TorznabCaps.MusicSearchYearAvailable); + Assert.IsEmpty(TorznabCaps.BookSearchParams); Assert.False(TorznabCaps.BookSearchAvailable); + Assert.False(TorznabCaps.BookSearchTitleAvailable); + Assert.False(TorznabCaps.BookSearchAuthorAvailable); Assert.AreEqual(0, TorznabCaps.Categories.Count); // add "int" category (parent category) @@ -247,7 +250,10 @@ namespace Jackett.Test.Torznab Links = new List{ "https://example.com" }, Caps = new capabilitiesBlock { - Modes = new Dictionary>() + Modes = new Dictionary> + { + {"search", new List{"q"}} + } }, Search = new searchBlock() }; @@ -271,7 +277,10 @@ namespace Jackett.Test.Torznab Assert.False(indexer.TorznabCaps.MusicSearchArtistAvailable); Assert.False(indexer.TorznabCaps.MusicSearchLabelAvailable); Assert.False(indexer.TorznabCaps.MusicSearchYearAvailable); + Assert.IsEmpty(indexer.TorznabCaps.BookSearchParams); Assert.False(indexer.TorznabCaps.BookSearchAvailable); + Assert.False(indexer.TorznabCaps.BookSearchTitleAvailable); + Assert.False(indexer.TorznabCaps.BookSearchAuthorAvailable); Assert.AreEqual(0, indexer.TorznabCaps.Categories.Count); definition = new IndexerDefinition // test categories (same as in C# indexer) @@ -279,7 +288,10 @@ namespace Jackett.Test.Torznab Links = new List{ "https://example.com" }, Caps = new capabilitiesBlock { - Modes = new Dictionary>(), + Modes = new Dictionary> + { + {"search", new List{"q"}} + }, Categories = new Dictionary { {"1", TorznabCatType.Movies.Name}, // integer cat (has children) @@ -334,7 +346,7 @@ namespace Jackett.Test.Torznab {"tv-search", new List{ "q", "season", "ep", "imdbid", "tvdbid", "rid" }}, {"movie-search", new List{ "q", "imdbid", "tmdbid" }}, {"music-search", new List{ "q", "album", "artist", "label", "year" }}, - {"book-search", new List{ "q", "author", "title" }} + {"book-search", new List{ "q", "title", "author" }} }, Categories = new Dictionary() }, @@ -343,22 +355,52 @@ namespace Jackett.Test.Torznab indexer = new CardigannIndexer(null, null, null, null, definition); Assert.True(indexer.TorznabCaps.SearchAvailable); + Assert.AreEqual( + new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep, TvSearchParam.ImdbId, TvSearchParam.TvdbId, TvSearchParam.RId + }, + indexer.TorznabCaps.TvSearchParams + ); Assert.True(indexer.TorznabCaps.TvSearchAvailable); + Assert.True(indexer.TorznabCaps.TvSearchSeasonAvailable); + Assert.True(indexer.TorznabCaps.TvSearchEpAvailable); // TODO: SupportsImdbTVSearch is disabled in Jackett.Common.Models.TorznabCapabilities.TvSearchImdbAvailable Assert.False(indexer.TorznabCaps.TvSearchImdbAvailable); Assert.True(indexer.TorznabCaps.TvSearchTvdbAvailable); Assert.True(indexer.TorznabCaps.TvSearchTvRageAvailable); Assert.AreEqual( - new List { MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId }, + new List + { + MovieSearchParam.Q, MovieSearchParam.ImdbId, MovieSearchParam.TmdbId + }, indexer.TorznabCaps.MovieSearchParams ); Assert.True(indexer.TorznabCaps.MovieSearchAvailable); Assert.True(indexer.TorznabCaps.MovieSearchImdbAvailable); Assert.True(indexer.TorznabCaps.MovieSearchTmdbAvailable); - // TODO: improve this assert - Assert.AreEqual(5, indexer.TorznabCaps.MusicSearchParams.Count); + Assert.AreEqual( + new List + { + MusicSearchParam.Q, MusicSearchParam.Album, MusicSearchParam.Artist, MusicSearchParam.Label, MusicSearchParam.Year + }, + indexer.TorznabCaps.MusicSearchParams + ); Assert.True(indexer.TorznabCaps.MusicSearchAvailable); + Assert.True(indexer.TorznabCaps.MusicSearchAlbumAvailable); + Assert.True(indexer.TorznabCaps.MusicSearchArtistAvailable); + Assert.True(indexer.TorznabCaps.MusicSearchLabelAvailable); + Assert.True(indexer.TorznabCaps.MusicSearchYearAvailable); + Assert.AreEqual( + new List + { + BookSearchParam.Q, BookSearchParam.Title, BookSearchParam.Author + }, + indexer.TorznabCaps.BookSearchParams + ); Assert.True(indexer.TorznabCaps.BookSearchAvailable); + Assert.True(indexer.TorznabCaps.BookSearchTitleAvailable); + Assert.True(indexer.TorznabCaps.BookSearchAuthorAvailable); // test Jackett UI categories (internal JSON) => same code path as C# indexer // test Torznab caps (XML) => same code path as C# indexer