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
+ }
+}