diff --git a/.editorconfig b/.editorconfig index 3be739aa89..2463530180 100644 --- a/.editorconfig +++ b/.editorconfig @@ -55,6 +55,7 @@ dotnet_diagnostic.IDE0018.severity = error # Stylecop Rules dotnet_diagnostic.SA0001.severity = none +dotnet_diagnostic.SA1200.severity = none dotnet_diagnostic.SA1025.severity = none dotnet_diagnostic.SA1101.severity = none dotnet_diagnostic.SA1116.severity = none diff --git a/README.md b/README.md index 0504dabdd2..a73d32209d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ All-in-one media manager for movies, books, and audiobooks. Aletheia (from Greek ἀλήθεια - "truth, disclosure") is a unified media management system forked from Radarr. It provides automated monitoring, downloading, and library management for multiple media types through a single interface. -**Current Status:** Early development. Movie functionality inherited from Radarr is working. Book and audiobook support is planned. +**Current Status:** Active development. Movie functionality inherited from Radarr is working. Multi-media foundation being implemented. ## Features @@ -15,20 +15,21 @@ Aletheia (from Greek ἀλήθεια - "truth, disclosure") is a unified media m - Metadata and artwork management - Integration with download clients and indexers -**Books (planned):** +**Books (in development):** - EPUB, MOBI, PDF quality tracking - Author and series hierarchy - Goodreads/Hardcover metadata -**Audiobooks (planned):** +**Audiobooks (in development):** - M4B, MP3, FLAC support -- Narrator tracking and duration metadata -- Audible metadata integration +- Narrator tracking (competitive differentiator) +- Duration metadata and Audible integration **General:** - Usenet and BitTorrent support - SABnzbd, NZBGet, qBittorrent, Deluge, rTorrent, Transmission integration - Plex and Kodi integration +- Built-in archive extraction (Unpackerr functionality) ## Privacy @@ -69,13 +70,40 @@ dotnet run --project src/Radarr ## Roadmap -1. **Foundation** - Generalize database schema, implement hierarchical monitoring (Author → Series → Item) -2. **Multi-Media** - Add book and audiobook quality profiles, port metadata providers -3. **Interface** - Unified dashboard with type filters, type-specific detail views +See [ROADMAP.md](../ROADMAP.md) for detailed phase planning. + +**Completed:** +- Phase 0-1: Privacy & security fixes +- Phase 2: Foundation (fork, CI/CD, branding) +- Phase 2.5: Community standards, quality gates, Unpackerr absorption + +**Current:** +- Phase 3: Multi-media foundation (database generalization, indexer management) + +**Planned:** +- Phase 4: Books & audiobooks support +- Phase 5: TV shows +- Phase 6: Music (with fingerprinting and quality analysis) +- Phase 7: Subtitles (Bazarr replacement), podcasts, comics + +## Key Differences from Radarr + +| Feature | Radarr | Aletheia | +|---------|--------|----------| +| Media types | Movies only | Movies, books, audiobooks (planned: TV, music, podcasts) | +| Telemetry | Enabled by default | Disabled by default | +| Indexer management | External (Prowlarr) | Built-in (planned) | +| Archive extraction | External (Unpackerr) | Built-in | +| Narrator tracking | N/A | Native support for audiobooks | ## Contributing -Early development phase. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and code guidelines. +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code guidelines, and PR process. + +**Development standards:** +- Conventional commits (`feat:`, `fix:`, `docs:`, etc.) +- Feature branches + PRs to `develop` +- Pre-commit hooks for linting ## License diff --git a/frontend/src/Components/MediaTypeBadge.tsx b/frontend/src/Components/MediaTypeBadge.tsx new file mode 100644 index 0000000000..6b4f135fe5 --- /dev/null +++ b/frontend/src/Components/MediaTypeBadge.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import Label from 'Components/Label'; +import { kinds, sizes } from 'Helpers/Props'; +import { MediaType } from 'Movie/Movie'; +import translate from 'Utilities/String/translate'; + +interface MediaTypeBadgeProps { + mediaType?: MediaType; + className?: string; +} + +function getKindForMediaType(mediaType?: MediaType) { + switch (mediaType) { + case 'book': + return kinds.INFO; + case 'audiobook': + return kinds.SUCCESS; + case 'movie': + default: + return kinds.PRIMARY; + } +} + +function getLabelForMediaType(mediaType?: MediaType) { + switch (mediaType) { + case 'book': + return translate('Book'); + case 'audiobook': + return translate('Audiobook'); + case 'movie': + default: + return translate('Movie'); + } +} + +function MediaTypeBadge({ mediaType, className }: MediaTypeBadgeProps) { + return ( + + ); +} + +export default MediaTypeBadge; diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx index 9894478406..e6908d87b4 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx @@ -7,6 +7,7 @@ import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import MediaTypeBadge from 'Components/MediaTypeBadge'; import MovieTagList from 'Components/MovieTagList'; import RottenTomatoRating from 'Components/RottenTomatoRating'; import TmdbRating from 'Components/TmdbRating'; @@ -90,6 +91,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { originalTitle, originalLanguage, tags = [], + mediaType, } = movie; const { sizeOnDisk = 0 } = statistics; @@ -234,6 +236,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { ) : null} + {mediaType && mediaType !== 'movie' ? ( +
+ +
+ ) : null} + {showMonitored ? (
{monitored ? translate('Monitored') : translate('Unmonitored')} diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index b7b4ee6b28..741adedfa8 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -4,6 +4,8 @@ import { MovieFile } from 'MovieFile/MovieFile'; export type MovieMonitor = 'movieOnly' | 'movieAndCollection' | 'none'; +export type MediaType = 'movie' | 'book' | 'audiobook'; + export type MovieStatus = | 'tba' | 'announced' @@ -58,6 +60,7 @@ export interface MovieAddOptions { interface Movie extends ModelBase { tmdbId: number; imdbId?: string; + mediaType?: MediaType; sortTitle: string; overview: string; youTubeTrailerId?: string; diff --git a/src/NzbDrone.Core/Datastore/Migration/243_add_mediatype_to_indexers.cs b/src/NzbDrone.Core/Datastore/Migration/243_add_mediatype_to_indexers.cs new file mode 100644 index 0000000000..13935c59f1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/243_add_mediatype_to_indexers.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(243)] + public class add_mediatype_to_indexers : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Indexers").AddColumn("SupportedMediaTypes").AsString().WithDefaultValue("[\"Movie\"]"); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index b0412ce210..1606f5cf56 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -26,6 +26,7 @@ using NzbDrone.Core.Jobs; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.AlternativeTitles; @@ -202,6 +203,7 @@ private static void RegisterMappers() SqlMapper.AddTypeHandler(new DapperLanguageIntConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>(new LanguageIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); + SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter>()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter(new QualityIntConverter(), new LanguageIntConverter())); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter()); diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/AudiobookSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/AudiobookSearchCriteria.cs new file mode 100644 index 0000000000..44560117c1 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/AudiobookSearchCriteria.cs @@ -0,0 +1,34 @@ +namespace NzbDrone.Core.IndexerSearch.Definitions +{ + public class AudiobookSearchCriteria : SearchCriteriaBase + { + public string Author { get; set; } + public string Title { get; set; } + public string Narrator { get; set; } + public string ASIN { get; set; } + public string ISBN { get; set; } + public int? Year { get; set; } + public bool? Abridged { get; set; } + + public override string ToString() + { + if (!string.IsNullOrWhiteSpace(Author) && !string.IsNullOrWhiteSpace(Title)) + { + var result = $"[{Author} - {Title}"; + if (!string.IsNullOrWhiteSpace(Narrator)) + { + result += $" (narrated by {Narrator})"; + } + + return result + "]"; + } + + if (!string.IsNullOrWhiteSpace(ASIN)) + { + return $"[ASIN: {ASIN}]"; + } + + return $"[{Title ?? Author ?? "Unknown"}]"; + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/BookSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/BookSearchCriteria.cs new file mode 100644 index 0000000000..1370e79b2d --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/BookSearchCriteria.cs @@ -0,0 +1,26 @@ +namespace NzbDrone.Core.IndexerSearch.Definitions +{ + public class BookSearchCriteria : SearchCriteriaBase + { + public string Author { get; set; } + public string Title { get; set; } + public string ISBN { get; set; } + public string Publisher { get; set; } + public int? Year { get; set; } + + public override string ToString() + { + if (!string.IsNullOrWhiteSpace(Author) && !string.IsNullOrWhiteSpace(Title)) + { + return $"[{Author} - {Title}]"; + } + + if (!string.IsNullOrWhiteSpace(ISBN)) + { + return $"[ISBN: {ISBN}]"; + } + + return $"[{Title ?? Author ?? "Unknown"}]"; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/FileList/FileListRequestGenerator.cs b/src/NzbDrone.Core/Indexers/FileList/FileListRequestGenerator.cs index 6f081a4de8..d1c25c9127 100644 --- a/src/NzbDrone.Core/Indexers/FileList/FileListRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/FileList/FileListRequestGenerator.cs @@ -40,6 +40,16 @@ public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private IEnumerable GetRequest(string searchType, string parameters) { if (Settings.Categories.Empty()) diff --git a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs index ed8612b2f1..7d21db9f3f 100644 --- a/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/HDBits/HDBitsRequestGenerator.cs @@ -34,6 +34,16 @@ public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private bool TryAddSearchParameters(TorrentQuery query, SearchCriteriaBase searchCriteria) { if (searchCriteria.Movie.MovieMetadata.Value.ImdbId.IsNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs index 3f4e3ba2db..30f56604ce 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using NzbDrone.Common.Http; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -12,6 +13,7 @@ public interface IIndexer : IProvider bool SupportsRss { get; } bool SupportsSearch { get; } DownloadProtocol Protocol { get; } + IEnumerable SupportedMediaTypes { get; } Task> FetchRecent(); Task> Fetch(MovieSearchCriteria searchCriteria); diff --git a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs index ef32b2ab09..2100a2089e 100644 --- a/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IIndexerRequestGenerator.cs @@ -8,6 +8,8 @@ public interface IIndexerRequestGenerator { IndexerPageableRequestChain GetRecentRequests(); IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria); + IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria); + IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria); Func> GetCookies { get; set; } Action, DateTime?> CookiesUpdater { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs index fbc810067a..c7995ebb44 100644 --- a/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/IPTorrents/IPTorrentsRequestGenerator.cs @@ -23,6 +23,16 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private IEnumerable GetRssRequests() { yield return new IndexerRequest(Settings.BaseUrl, HttpAccept.Rss); diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 335486bc28..9c1e5318dc 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -29,6 +30,7 @@ public abstract class IndexerBase : IIndexer public abstract bool SupportsRss { get; } public abstract bool SupportsSearch { get; } + public virtual IEnumerable SupportedMediaTypes => new[] { MediaType.Movie }; public IndexerBase(IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) { diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs index 6bd4297030..db03429bb0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs +++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Equ; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers @@ -13,6 +15,7 @@ public class IndexerDefinition : ProviderDefinition, IEquatable { MediaType.Movie }; } [MemberwiseEqualityIgnore] @@ -29,6 +32,7 @@ public IndexerDefinition() public bool EnableInteractiveSearch { get; set; } public int DownloadClientId { get; set; } public int Priority { get; set; } + public List SupportedMediaTypes { get; set; } [MemberwiseEqualityIgnore] public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch; diff --git a/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouse.cs b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouse.cs new file mode 100644 index 0000000000..f28b1f4f4f --- /dev/null +++ b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaTypes; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.Indexers.MyAnonamouse +{ + public class MyAnonamouse : HttpIndexerBase + { + public override string Name => "MyAnonamouse"; + public override DownloadProtocol Protocol => DownloadProtocol.Torrent; + public override bool SupportsRss => true; + public override bool SupportsSearch => true; + public override int PageSize => 100; + + public override IEnumerable SupportedMediaTypes => new[] { MediaType.Book, MediaType.Audiobook }; + + public MyAnonamouse(IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) + : base(httpClient, indexerStatusService, configService, parsingService, logger) + { + } + + public override IIndexerRequestGenerator GetRequestGenerator() + { + return new MyAnonamouseRequestGenerator(Settings, _logger); + } + + public override IParseIndexerResponse GetParser() + { + return new MyAnonamouseParser(Settings, _logger); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseApi.cs b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseApi.cs new file mode 100644 index 0000000000..acd25674ed --- /dev/null +++ b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseApi.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Indexers.MyAnonamouse +{ + public class MyAnonamouseResponse + { + public string Error { get; set; } + public IReadOnlyCollection Data { get; set; } + public string Message { get; set; } + } + + public class MyAnonamouseTorrent + { + public int Id { get; set; } + public string Title { get; set; } + + [JsonProperty(PropertyName = "author_info")] + public string AuthorInfo { get; set; } + + public string Description { get; set; } + + [JsonProperty(PropertyName = "lang_code")] + public string LanguageCode { get; set; } + + public string Filetype { get; set; } + public bool Vip { get; set; } + public bool Free { get; set; } + + [JsonProperty(PropertyName = "personal_freeleech")] + public bool PersonalFreeLeech { get; set; } + + [JsonProperty(PropertyName = "fl_vip")] + public bool FreeVip { get; set; } + + public string Category { get; set; } + public string Added { get; set; } + + [JsonProperty(PropertyName = "times_completed")] + public int Grabs { get; set; } + + public int Seeders { get; set; } + public int Leechers { get; set; } + public int NumFiles { get; set; } + public string Size { get; set; } + } + + public class MyAnonamouseBuyFreeleechResponse + { + public bool Success { get; set; } + public string Error { get; set; } + } + + public class MyAnonamouseUserDataResponse + { + [JsonProperty(PropertyName = "classname")] + public string UserClass { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseParser.cs b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseParser.cs new file mode 100644 index 0000000000..3910c59d8c --- /dev/null +++ b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseParser.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using Newtonsoft.Json; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Indexers.MyAnonamouse +{ + public class MyAnonamouseParser : IParseIndexerResponse + { + private readonly MyAnonamouseSettings _settings; + private readonly Logger _logger; + + public MyAnonamouseParser(MyAnonamouseSettings settings, Logger logger = null) + { + _settings = settings; + _logger = logger ?? LogManager.GetCurrentClassLogger(); + } + + public IList ParseResponse(IndexerResponse indexerResponse) + { + var torrentInfos = new List(); + + if (indexerResponse.HttpResponse.StatusCode == HttpStatusCode.Forbidden) + { + throw new IndexerException(indexerResponse, "[403 Forbidden] - mam_id expired or invalid"); + } + + if (indexerResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new IndexerException(indexerResponse, + "Unexpected response status {0} code from API request", + indexerResponse.HttpResponse.StatusCode); + } + + MyAnonamouseResponse jsonResponse; + + try + { + jsonResponse = JsonConvert.DeserializeObject(indexerResponse.Content); + } + catch (JsonException ex) + { + throw new IndexerException(indexerResponse, "Failed to parse JSON response from MyAnonamouse: {0}", ex.Message); + } + + if (jsonResponse == null) + { + throw new IndexerException(indexerResponse, "Empty response from MyAnonamouse"); + } + + if (jsonResponse.Error.IsNotNullOrWhiteSpace() && jsonResponse.Error.StartsWithIgnoreCase("Nothing returned, out of")) + { + return torrentInfos; + } + + if (jsonResponse.Data == null) + { + throw new IndexerException(indexerResponse, + "Unexpected response content: {0}", + jsonResponse.Message ?? "Check the logs for more information"); + } + + foreach (var item in jsonResponse.Data) + { + var id = item.Id; + var title = item.Title; + + if (item.AuthorInfo != null) + { + try + { + var authorInfo = JsonConvert.DeserializeObject>(item.AuthorInfo); + var author = authorInfo?.Take(5).Select(v => v.Value).Join(", "); + + if (author.IsNotNullOrWhiteSpace()) + { + title += " by " + author; + } + } + catch (JsonException ex) + { + _logger.Debug(ex, "Failed to parse author info for torrent {0}", id); + } + } + + var flags = new List(); + + if (item.LanguageCode.IsNotNullOrWhiteSpace()) + { + flags.Add(item.LanguageCode); + } + + if (item.Filetype.IsNotNullOrWhiteSpace()) + { + flags.Add(item.Filetype.ToUpper()); + } + + if (flags.Count > 0) + { + title += " [" + flags.Join(" / ") + "]"; + } + + if (item.Vip) + { + title += " [VIP]"; + } + + var isFreeLeech = item.Free || item.PersonalFreeLeech || item.FreeVip; + + torrentInfos.Add(new TorrentInfo + { + Guid = $"MyAnonamouse-{id}", + Title = title, + Size = ParseSize(item.Size), + DownloadUrl = GetDownloadUrl(id), + InfoUrl = GetInfoUrl(id), + Seeders = item.Seeders, + Peers = item.Leechers + item.Seeders, + PublishDate = ParseDate(item.Added), + IndexerFlags = GetIndexerFlags(isFreeLeech) + }); + } + + return torrentInfos; + } + + private static IndexerFlags GetIndexerFlags(bool isFreeLeech) + { + IndexerFlags flags = 0; + + if (isFreeLeech) + { + flags |= IndexerFlags.G_Freeleech; + } + + return flags; + } + + private static long ParseSize(string sizeString) + { + if (sizeString.IsNullOrWhiteSpace()) + { + return 0; + } + + if (long.TryParse(sizeString, out var size)) + { + return size; + } + + var parts = sizeString.Trim().Split(' '); + if (parts.Length != 2 || !double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + { + return 0; + } + + var unit = parts[1].ToUpperInvariant(); + return unit switch + { + "B" => (long)value, + "KB" => (long)(value * 1024), + "MB" => (long)(value * 1024 * 1024), + "GB" => (long)(value * 1024 * 1024 * 1024), + "TB" => (long)(value * 1024 * 1024 * 1024 * 1024), + _ => 0 + }; + } + + private static DateTime ParseDate(string dateString) + { + if (DateTime.TryParseExact(dateString, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var date)) + { + return date.ToLocalTime(); + } + + return DateTime.UtcNow; + } + + private string GetDownloadUrl(int torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/tor/download.php") + .AddQueryParam("tid", torrentId); + + return url.FullUri; + } + + private string GetInfoUrl(int torrentId) + { + var url = new HttpUri(_settings.BaseUrl) + .CombinePath("/t/") + .CombinePath(torrentId.ToString()); + + return url.FullUri; + } + + public Action, DateTime?> CookiesUpdater { get; set; } + } +} diff --git a/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseRequestGenerator.cs b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseRequestGenerator.cs new file mode 100644 index 0000000000..95e0e0c7bb --- /dev/null +++ b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseRequestGenerator.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.Indexers.MyAnonamouse +{ + public class MyAnonamouseRequestGenerator : IIndexerRequestGenerator + { + private static readonly Regex SanitizeSearchQueryRegex = new ("[^\\w]+", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + + public MyAnonamouseSettings Settings { get; set; } + + private readonly Logger _logger; + + public MyAnonamouseRequestGenerator(MyAnonamouseSettings settings, Logger logger) + { + Settings = settings; + _logger = logger; + } + + public virtual IndexerPageableRequestChain GetRecentRequests() + { + var pageableRequests = new IndexerPageableRequestChain(); + pageableRequests.Add(GetPagedRequests(string.Empty)); + return pageableRequests; + } + + public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + var searchTerm = BuildSearchTerm(searchCriteria); + + if (searchTerm.IsNotNullOrWhiteSpace()) + { + pageableRequests.Add(GetPagedRequests(searchTerm)); + } + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + var searchTerm = BuildSearchTerm(searchCriteria); + + if (searchTerm.IsNotNullOrWhiteSpace()) + { + pageableRequests.Add(GetPagedRequests(searchTerm)); + } + + return pageableRequests; + } + + private string BuildSearchTerm(SearchCriteriaBase searchCriteria) + { + var terms = new List(); + + if (searchCriteria is BookSearchCriteria bookCriteria) + { + if (bookCriteria.Author.IsNotNullOrWhiteSpace()) + { + terms.Add(bookCriteria.Author); + } + + if (bookCriteria.Title.IsNotNullOrWhiteSpace()) + { + terms.Add(bookCriteria.Title); + } + } + else if (searchCriteria is AudiobookSearchCriteria audiobookCriteria) + { + if (audiobookCriteria.Author.IsNotNullOrWhiteSpace()) + { + terms.Add(audiobookCriteria.Author); + } + + if (audiobookCriteria.Title.IsNotNullOrWhiteSpace()) + { + terms.Add(audiobookCriteria.Title); + } + } + + return string.Join(" ", terms); + } + + private IEnumerable GetPagedRequests(string term) + { + var sanitizedTerm = SanitizeSearchQueryRegex.Replace(term, " ").Trim(); + + if (term.IsNotNullOrWhiteSpace() && sanitizedTerm.IsNullOrWhiteSpace()) + { + _logger.Debug("Search term is empty after sanitization, skipping. Original: '{0}'", term); + yield break; + } + + var searchType = Settings.SearchType switch + { + (int)MyAnonamouseSearchType.Active => "active", + (int)MyAnonamouseSearchType.Freeleech => "fl", + (int)MyAnonamouseSearchType.FreeleechOrVip => "fl-VIP", + (int)MyAnonamouseSearchType.Vip => "VIP", + (int)MyAnonamouseSearchType.NotVip => "nVIP", + _ => "all" + }; + + var parameters = new NameValueCollection + { + { "tor[text]", sanitizedTerm }, + { "tor[searchType]", searchType }, + { "tor[srchIn][title]", "true" }, + { "tor[srchIn][author]", "true" }, + { "tor[srchIn][narrator]", "true" }, + { "tor[searchIn]", "torrents" }, + { "tor[sortType]", "default" }, + { "tor[perpage]", "100" }, + { "tor[startNumber]", "0" }, + { "thumbnails", "1" }, + { "description", "1" } + }; + + if (Settings.SearchInDescription) + { + parameters.Set("tor[srchIn][description]", "true"); + } + + if (Settings.SearchInSeries) + { + parameters.Set("tor[srchIn][series]", "true"); + } + + if (Settings.SearchInFilenames) + { + parameters.Set("tor[srchIn][filenames]", "true"); + } + + parameters.Set("tor[cat][]", "0"); + + var searchUrl = Settings.BaseUrl.TrimEnd('/') + "/tor/js/loadSearchJSONbasic.php"; + + if (parameters.Count > 0) + { + searchUrl += "?" + parameters.ToQueryString(); + } + + var requestBuilder = new HttpRequestBuilder(searchUrl) + .Accept(HttpAccept.Json); + + var cookies = GetCookies?.Invoke(); + + if (cookies != null && cookies.TryGetValue("mam_id", out var mamId) && mamId.IsNotNullOrWhiteSpace()) + { + requestBuilder.SetCookies(cookies); + } + else if (Settings.MamId.IsNotNullOrWhiteSpace()) + { + requestBuilder.SetCookies(new Dictionary { { "mam_id", Settings.MamId } }); + } + + yield return new IndexerRequest(requestBuilder.Build()); + } + + public Func> GetCookies { get; set; } + public Action, DateTime?> CookiesUpdater { get; set; } + } + + internal static class NameValueCollectionExtensions + { + public static string ToQueryString(this NameValueCollection nvc) + { + var items = new List(); + + foreach (var key in nvc.AllKeys) + { + items.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(nvc[key])}"); + } + + return string.Join("&", items); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseSettings.cs b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseSettings.cs new file mode 100644 index 0000000000..7081a99c3b --- /dev/null +++ b/src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseSettings.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Equ; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Indexers.MyAnonamouse +{ + public class MyAnonamouseSettingsValidator : AbstractValidator + { + public MyAnonamouseSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.MamId).NotEmpty(); + RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator()); + } + } + + public class MyAnonamouseSettings : PropertywiseEquatable, ITorrentIndexerSettings + { + private static readonly MyAnonamouseSettingsValidator Validator = new (); + + public MyAnonamouseSettings() + { + BaseUrl = "https://www.myanonamouse.net"; + MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; + SearchType = (int)MyAnonamouseSearchType.All; + SearchInDescription = false; + SearchInSeries = false; + SearchInFilenames = false; + MultiLanguages = Array.Empty(); + FailDownloads = Array.Empty(); + RequiredFlags = Array.Empty(); + } + + [FieldDefinition(0, Label = "IndexerSettingsApiUrl", Advanced = true, HelpText = "IndexerSettingsApiUrlHelpText")] + public string BaseUrl { get; set; } + + [FieldDefinition(1, Label = "IndexerMyAnonamouseMamId", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, HelpText = "IndexerMyAnonamouseMamIdHelpText")] + public string MamId { get; set; } + + [FieldDefinition(2, Label = "IndexerMyAnonamouseSearchType", Type = FieldType.Select, SelectOptions = typeof(MyAnonamouseSearchType), HelpText = "IndexerMyAnonamouseSearchTypeHelpText")] + public int SearchType { get; set; } + + [FieldDefinition(3, Label = "IndexerMyAnonamouseSearchInDescription", Type = FieldType.Checkbox, HelpText = "IndexerMyAnonamouseSearchInDescriptionHelpText", Advanced = true)] + public bool SearchInDescription { get; set; } + + [FieldDefinition(4, Label = "IndexerMyAnonamouseSearchInSeries", Type = FieldType.Checkbox, HelpText = "IndexerMyAnonamouseSearchInSeriesHelpText", Advanced = true)] + public bool SearchInSeries { get; set; } + + [FieldDefinition(5, Label = "IndexerMyAnonamouseSearchInFilenames", Type = FieldType.Checkbox, HelpText = "IndexerMyAnonamouseSearchInFilenamesHelpText", Advanced = true)] + public bool SearchInFilenames { get; set; } + + [FieldDefinition(6, Type = FieldType.Number, Label = "IndexerSettingsMinimumSeeders", HelpText = "IndexerSettingsMinimumSeedersHelpText", Advanced = true)] + public int MinimumSeeders { get; set; } + + [FieldDefinition(7)] + public SeedCriteriaSettings SeedCriteria { get; set; } = new (); + + [FieldDefinition(8, Type = FieldType.Checkbox, Label = "IndexerSettingsRejectBlocklistedTorrentHashes", HelpText = "IndexerSettingsRejectBlocklistedTorrentHashesHelpText", Advanced = true)] + public bool RejectBlocklistedTorrentHashesWhileGrabbing { get; set; } + + [FieldDefinition(9, Type = FieldType.Select, SelectOptions = typeof(RealLanguageFieldConverter), Label = "IndexerSettingsMultiLanguageRelease", HelpText = "IndexerSettingsMultiLanguageReleaseHelpText", Advanced = true)] + public IEnumerable MultiLanguages { get; set; } + + [FieldDefinition(10, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)] + public IEnumerable FailDownloads { get; set; } + + [FieldDefinition(11, Type = FieldType.Select, SelectOptions = typeof(IndexerFlags), Label = "IndexerSettingsRequiredFlags", HelpText = "IndexerSettingsRequiredFlagsHelpText", HelpLink = "https://wiki.servarr.com/radarr/settings#indexer-flags", Advanced = true)] + public IEnumerable RequiredFlags { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } + + public enum MyAnonamouseSearchType + { + [FieldOption("All Torrents")] + All = 0, + + [FieldOption("Only Active")] + Active = 1, + + [FieldOption("Freeleech")] + Freeleech = 2, + + [FieldOption("Freeleech or VIP")] + FreeleechOrVip = 3, + + [FieldOption("VIP")] + Vip = 4, + + [FieldOption("Not VIP")] + NotVip = 5 + } +} diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index 3f8c3edcf5..86408a984a 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.Parser; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -21,6 +22,7 @@ public class Newznab : HttpIndexerBase public override DownloadProtocol Protocol => DownloadProtocol.Usenet; public override int PageSize => GetProviderPageSize(); + public override IEnumerable SupportedMediaTypes => new[] { MediaType.Movie, MediaType.Book, MediaType.Audiobook }; public Newznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) : base(httpClient, indexerStatusService, configService, parsingService, logger) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs index 97a4651379..cf9550de25 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabRequestGenerator.cs @@ -113,6 +113,66 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + var categories = Settings.Categories.Any() + ? Settings.Categories + : new[] { NewznabStandardCategory.Books, NewznabStandardCategory.BooksEBook }; + + if (!string.IsNullOrWhiteSpace(searchCriteria.ISBN)) + { + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "book", $"&q={Uri.EscapeDataString(searchCriteria.ISBN)}")); + } + else if (!string.IsNullOrWhiteSpace(searchCriteria.Author) && !string.IsNullOrWhiteSpace(searchCriteria.Title)) + { + var query = $"{searchCriteria.Author} {searchCriteria.Title}"; + if (searchCriteria.Year.HasValue) + { + query += $" {searchCriteria.Year}"; + } + + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "book", $"&q={Uri.EscapeDataString(query)}")); + } + else if (!string.IsNullOrWhiteSpace(searchCriteria.Title)) + { + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "book", $"&q={Uri.EscapeDataString(searchCriteria.Title)}")); + } + + return pageableRequests; + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + var pageableRequests = new IndexerPageableRequestChain(); + + var categories = Settings.Categories.Any() + ? Settings.Categories + : new[] { NewznabStandardCategory.AudioAudiobook, NewznabStandardCategory.Audio }; + + if (!string.IsNullOrWhiteSpace(searchCriteria.ASIN)) + { + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "search", $"&q={Uri.EscapeDataString(searchCriteria.ASIN)}")); + } + else if (!string.IsNullOrWhiteSpace(searchCriteria.Author) && !string.IsNullOrWhiteSpace(searchCriteria.Title)) + { + var query = $"{searchCriteria.Author} {searchCriteria.Title}"; + if (!string.IsNullOrWhiteSpace(searchCriteria.Narrator)) + { + query += $" {searchCriteria.Narrator}"; + } + + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "search", $"&q={Uri.EscapeDataString(query)}")); + } + else if (!string.IsNullOrWhiteSpace(searchCriteria.Title)) + { + pageableRequests.Add(GetPagedRequests(MaxPages, categories, "search", $"&q={Uri.EscapeDataString(searchCriteria.Title)}")); + } + + return pageableRequests; + } + private void AddMovieIdPageableRequests(IndexerPageableRequestChain chain, int maxPages, IEnumerable categories, SearchCriteriaBase searchCriteria) { var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.Movie.MovieMetadata.Value.TmdbId > 0; diff --git a/src/NzbDrone.Core/Indexers/NewznabStandardCategory.cs b/src/NzbDrone.Core/Indexers/NewznabStandardCategory.cs new file mode 100644 index 0000000000..23b7da9295 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/NewznabStandardCategory.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using NzbDrone.Core.MediaTypes; + +namespace NzbDrone.Core.Indexers +{ + public static class NewznabStandardCategory + { + public static readonly int Console = 1000; + public static readonly int ConsoleNDS = 1010; + public static readonly int ConsolePSP = 1020; + public static readonly int ConsoleWii = 1030; + public static readonly int ConsoleXbox = 1040; + public static readonly int ConsoleXbox360 = 1050; + public static readonly int ConsoleWiiware = 1060; + public static readonly int ConsoleXbox360DLC = 1070; + public static readonly int ConsolePS3 = 1080; + public static readonly int ConsoleOther = 1090; + public static readonly int Console3DS = 1110; + public static readonly int ConsolePSVita = 1120; + public static readonly int ConsoleWiiU = 1130; + public static readonly int ConsoleXboxOne = 1140; + public static readonly int ConsolePS4 = 1180; + + public static readonly int Movies = 2000; + public static readonly int MoviesForeign = 2010; + public static readonly int MoviesOther = 2020; + public static readonly int MoviesSD = 2030; + public static readonly int MoviesHD = 2040; + public static readonly int MoviesUHD = 2045; + public static readonly int MoviesBluRay = 2050; + public static readonly int Movies3D = 2060; + public static readonly int MoviesDVD = 2070; + public static readonly int MoviesWEBDL = 2080; + + public static readonly int Audio = 3000; + public static readonly int AudioMP3 = 3010; + public static readonly int AudioVideo = 3020; + public static readonly int AudioAudiobook = 3030; + public static readonly int AudioLossless = 3040; + public static readonly int AudioOther = 3050; + public static readonly int AudioForeign = 3060; + + public static readonly int PC = 4000; + public static readonly int PC0day = 4010; + public static readonly int PCISO = 4020; + public static readonly int PCMac = 4030; + public static readonly int PCMobileOther = 4040; + public static readonly int PCGames = 4050; + public static readonly int PCMobileiOS = 4060; + public static readonly int PCMobileAndroid = 4070; + + public static readonly int TV = 5000; + public static readonly int TVWEBDL = 5010; + public static readonly int TVForeign = 5020; + public static readonly int TVSD = 5030; + public static readonly int TVHD = 5040; + public static readonly int TVUHD = 5045; + public static readonly int TVOther = 5050; + public static readonly int TVSport = 5060; + public static readonly int TVAnime = 5070; + public static readonly int TVDocumentary = 5080; + + public static readonly int XXX = 6000; + public static readonly int XXXDVD = 6010; + public static readonly int XXXWMV = 6020; + public static readonly int XXXXviD = 6030; + public static readonly int XXXx264 = 6040; + public static readonly int XXXOther = 6050; + public static readonly int XXXImageset = 6060; + public static readonly int XXXPacks = 6070; + + public static readonly int Books = 7000; + public static readonly int BooksMags = 7010; + public static readonly int BooksEBook = 7020; + public static readonly int BooksComics = 7030; + public static readonly int BooksTechnical = 7040; + public static readonly int BooksOther = 7050; + public static readonly int BooksForeign = 7060; + + public static readonly int Other = 8000; + public static readonly int OtherMisc = 8010; + public static readonly int OtherHashed = 8020; + + public static IReadOnlyList GetCategoriesForMediaType(MediaType mediaType) + { + return mediaType switch + { + MediaType.Movie => new[] + { + Movies, MoviesForeign, MoviesOther, MoviesSD, MoviesHD, + MoviesUHD, MoviesBluRay, Movies3D, MoviesDVD, MoviesWEBDL + }, + MediaType.TV => new[] + { + TV, TVWEBDL, TVForeign, TVSD, TVHD, TVUHD, + TVOther, TVSport, TVAnime, TVDocumentary + }, + MediaType.Music => new[] + { + Audio, AudioMP3, AudioVideo, AudioLossless, AudioOther, AudioForeign + }, + MediaType.Audiobook => new[] { AudioAudiobook, Audio }, + MediaType.Book => new[] + { + Books, BooksMags, BooksEBook, BooksTechnical, BooksOther, BooksForeign + }, + MediaType.Comic => new[] { BooksComics, Books }, + MediaType.Podcast => new[] { Audio, AudioOther }, + _ => new[] { Movies } + }; + } + + public static IReadOnlyList GetIgnoredCategoriesForMediaType(MediaType mediaType) + { + return mediaType switch + { + MediaType.Movie => new[] { Console, Audio, PC, XXX, Books }, + MediaType.TV => new[] { Console, Audio, PC, Movies, XXX, Books }, + MediaType.Music => new[] { Console, PC, Movies, XXX, Books, TV }, + MediaType.Audiobook => new[] { Console, PC, Movies, XXX, TV }, + MediaType.Book => new[] { Console, Audio, PC, Movies, XXX, TV }, + MediaType.Comic => new[] { Console, Audio, PC, Movies, XXX, TV }, + MediaType.Podcast => new[] { Console, PC, Movies, XXX, Books, TV }, + _ => new[] { Console, Audio, PC, XXX, Books } + }; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs index 97a08a420e..fdf76bac7b 100644 --- a/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/Nyaa/NyaaRequestGenerator.cs @@ -30,6 +30,16 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private IEnumerable GetPagedRequests(string term) { var baseUrl = $"{Settings.BaseUrl.TrimEnd('/')}/?page=rss{Settings.AdditionalParameters}"; diff --git a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs index cd5ee553b0..8c22d0159d 100644 --- a/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/PassThePopcorn/PassThePopcornRequestGenerator.cs @@ -43,6 +43,16 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private IEnumerable GetRequest(string searchParameters) { var request = diff --git a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs index 7897af38fb..a54b6217ab 100644 --- a/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/RssIndexerRequestGenerator.cs @@ -28,6 +28,16 @@ public virtual IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria return new IndexerPageableRequestChain(); } + public virtual IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public virtual IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public Func> GetCookies { get; set; } public Action, DateTime?> CookiesUpdater { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs index a8d8eeb1d4..557d862910 100644 --- a/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/TorrentPotato/TorrentPotatoRequestGenerator.cs @@ -74,6 +74,16 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return pageableRequests; } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + public Func> GetCookies { get; set; } public Action, DateTime?> CookiesUpdater { get; set; } } diff --git a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs index fbdad06350..d1596d7471 100644 --- a/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs +++ b/src/NzbDrone.Core/Indexers/TorrentRss/TorrentRssIndexerRequestGenerator.cs @@ -24,6 +24,16 @@ public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchC return new IndexerPageableRequestChain(); } + public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + + public IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria) + { + return new IndexerPageableRequestChain(); + } + private IEnumerable GetRssRequests(string searchParameters) { var request = new IndexerRequest(Settings.BaseUrl.Trim().TrimEnd('/'), HttpAccept.Rss); diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 75bdd67e59..9136bec549 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers.Newznab; +using NzbDrone.Core.MediaTypes; using NzbDrone.Core.Parser; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -22,6 +23,7 @@ public class Torznab : HttpIndexerBase public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public override int PageSize => GetProviderPageSize(); + public override IEnumerable SupportedMediaTypes => new[] { MediaType.Movie, MediaType.Book, MediaType.Audiobook }; public Torznab(INewznabCapabilitiesProvider capabilitiesProvider, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger) : base(httpClient, indexerStatusService, configService, parsingService, logger) @@ -61,7 +63,7 @@ private IndexerDefinition GetDefinition(string name, TorznabSettings settings) Name = name, Implementation = GetType().Name, Settings = settings, - Protocol = DownloadProtocol.Usenet, + Protocol = DownloadProtocol.Torrent, SupportsRss = SupportsRss, SupportsSearch = SupportsSearch }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 2b3bcda732..922f6a055f 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -97,6 +97,7 @@ "AptUpdater": "Use apt to install the update", "AudioInfo": "Audio Info", "AudioLanguages": "Audio Languages", + "Audiobook": "Audiobook", "AuthBasic": "Basic (Browser Popup)", "AuthForm": "Forms (Login Page)", "Authentication": "Authentication", @@ -163,6 +164,7 @@ "BlocklistReleases": "Blocklist Releases", "Blocklisted": "Blocklisted", "BlocklistedAt": "Blocklisted at {date}", + "Book": "Book", "Branch": "Branch", "BranchUpdate": "Branch to use to update {appName}", "BranchUpdateMechanism": "Branch used by external update mechanism", @@ -879,6 +881,16 @@ "IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {indexerNames}", "IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours", "IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {indexerNames}", + "IndexerMyAnonamouseMamId": "Mam Id", + "IndexerMyAnonamouseMamIdHelpText": "Mam Session Id (found in Preferences > Security)", + "IndexerMyAnonamouseSearchInDescription": "Search in Description", + "IndexerMyAnonamouseSearchInDescriptionHelpText": "Include torrent description in search", + "IndexerMyAnonamouseSearchInFilenames": "Search in Filenames", + "IndexerMyAnonamouseSearchInFilenamesHelpText": "Include filenames in search", + "IndexerMyAnonamouseSearchInSeries": "Search in Series", + "IndexerMyAnonamouseSearchInSeriesHelpText": "Include series information in search", + "IndexerMyAnonamouseSearchType": "Search Type", + "IndexerMyAnonamouseSearchTypeHelpText": "Specify the desired search type", "IndexerNewznabSettingsAdditionalParametersHelpText": "Additional Newznab parameters", "IndexerNewznabSettingsCategoriesHelpText": "Drop down list, at least one category must be selected.", "IndexerNyaaSettingsAdditionalParametersHelpText": "Please note if you change the category you will have to add required/restricted rules about the subgroups to avoid foreign language releases.", diff --git a/src/NzbDrone.Core/MediaTypes/MediaType.cs b/src/NzbDrone.Core/MediaTypes/MediaType.cs new file mode 100644 index 0000000000..5a6bbf305a --- /dev/null +++ b/src/NzbDrone.Core/MediaTypes/MediaType.cs @@ -0,0 +1,14 @@ +namespace NzbDrone.Core.MediaTypes +{ + public enum MediaType + { + Unknown = 0, + Movie = 1, + TV = 2, + Music = 3, + Book = 4, + Audiobook = 5, + Podcast = 6, + Comic = 7 + } +}