mirror of
https://github.com/Radarr/Radarr
synced 2026-01-24 16:32:41 +01:00
Merge pull request #29 from cheir-mneme/feature/indexer-management
feat: Sprint 3 - Multi-media indexer support
This commit is contained in:
commit
1bfa716745
32 changed files with 1073 additions and 10 deletions
|
|
@ -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
|
||||
|
|
|
|||
46
README.md
46
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
|
||||
|
||||
|
|
|
|||
48
frontend/src/Components/MediaTypeBadge.tsx
Normal file
48
frontend/src/Components/MediaTypeBadge.tsx
Normal file
|
|
@ -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 (
|
||||
<Label
|
||||
className={className}
|
||||
kind={getKindForMediaType(mediaType)}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{getLabelForMediaType(mediaType)}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaTypeBadge;
|
||||
|
|
@ -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) {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{mediaType && mediaType !== 'movie' ? (
|
||||
<div className={styles.title}>
|
||||
<MediaTypeBadge mediaType={mediaType} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showMonitored ? (
|
||||
<div className={styles.title}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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\"]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<Language>>(new LanguageIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<string>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<MediaType>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ParsedMovieInfo>(new QualityIntConverter(), new LanguageIntConverter()));
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<ReleaseInfo>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<PendingReleaseAdditionalInfo>());
|
||||
|
|
|
|||
|
|
@ -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"}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IndexerRequest> GetRequest(string searchType, string parameters)
|
||||
{
|
||||
if (Settings.Categories.Empty())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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<MediaType> SupportedMediaTypes { get; }
|
||||
|
||||
Task<IList<ReleaseInfo>> FetchRecent();
|
||||
Task<IList<ReleaseInfo>> Fetch(MovieSearchCriteria searchCriteria);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ public interface IIndexerRequestGenerator
|
|||
{
|
||||
IndexerPageableRequestChain GetRecentRequests();
|
||||
IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria);
|
||||
IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria);
|
||||
IndexerPageableRequestChain GetSearchRequests(AudiobookSearchCriteria searchCriteria);
|
||||
Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IndexerRequest> GetRssRequests()
|
||||
{
|
||||
yield return new IndexerRequest(Settings.BaseUrl, HttpAccept.Rss);
|
||||
|
|
|
|||
|
|
@ -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<TSettings> : IIndexer
|
|||
|
||||
public abstract bool SupportsRss { get; }
|
||||
public abstract bool SupportsSearch { get; }
|
||||
public virtual IEnumerable<MediaType> SupportedMediaTypes => new[] { MediaType.Movie };
|
||||
|
||||
public IndexerBase(IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<IndexerDefinitio
|
|||
public IndexerDefinition()
|
||||
{
|
||||
Priority = DefaultPriority;
|
||||
SupportedMediaTypes = new List<MediaType> { 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<MediaType> SupportedMediaTypes { get; set; }
|
||||
|
||||
[MemberwiseEqualityIgnore]
|
||||
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;
|
||||
|
|
|
|||
35
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouse.cs
Normal file
35
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouse.cs
Normal file
|
|
@ -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<MyAnonamouseSettings>
|
||||
{
|
||||
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<MediaType> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseApi.cs
Normal file
59
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseApi.cs
Normal file
|
|
@ -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<MyAnonamouseTorrent> 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; }
|
||||
}
|
||||
}
|
||||
206
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseParser.cs
Normal file
206
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseParser.cs
Normal file
|
|
@ -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<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
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<MyAnonamouseResponse>(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<Dictionary<string, string>>(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<string>();
|
||||
|
||||
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<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string>();
|
||||
|
||||
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<IndexerRequest> 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<string, string> { { "mam_id", Settings.MamId } });
|
||||
}
|
||||
|
||||
yield return new IndexerRequest(requestBuilder.Build());
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
internal static class NameValueCollectionExtensions
|
||||
{
|
||||
public static string ToQueryString(this NameValueCollection nvc)
|
||||
{
|
||||
var items = new List<string>();
|
||||
|
||||
foreach (var key in nvc.AllKeys)
|
||||
{
|
||||
items.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(nvc[key])}");
|
||||
}
|
||||
|
||||
return string.Join("&", items);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseSettings.cs
Normal file
101
src/NzbDrone.Core/Indexers/MyAnonamouse/MyAnonamouseSettings.cs
Normal file
|
|
@ -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<MyAnonamouseSettings>
|
||||
{
|
||||
public MyAnonamouseSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||
RuleFor(c => c.MamId).NotEmpty();
|
||||
RuleFor(c => c.SeedCriteria).SetValidator(_ => new SeedCriteriaSettingsValidator());
|
||||
}
|
||||
}
|
||||
|
||||
public class MyAnonamouseSettings : PropertywiseEquatable<MyAnonamouseSettings>, 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<int>();
|
||||
FailDownloads = Array.Empty<int>();
|
||||
RequiredFlags = Array.Empty<int>();
|
||||
}
|
||||
|
||||
[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<int> MultiLanguages { get; set; }
|
||||
|
||||
[FieldDefinition(10, Type = FieldType.Select, SelectOptions = typeof(FailDownloads), Label = "IndexerSettingsFailDownloads", HelpText = "IndexerSettingsFailDownloadsHelpText", Advanced = true)]
|
||||
public IEnumerable<int> 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<int> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NewznabSettings>
|
|||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
public override int PageSize => GetProviderPageSize();
|
||||
public override IEnumerable<MediaType> 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)
|
||||
|
|
|
|||
|
|
@ -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<int> categories, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var includeTmdbSearch = SupportsTmdbSearch && searchCriteria.Movie.MovieMetadata.Value.TmdbId > 0;
|
||||
|
|
|
|||
128
src/NzbDrone.Core/Indexers/NewznabStandardCategory.cs
Normal file
128
src/NzbDrone.Core/Indexers/NewznabStandardCategory.cs
Normal file
|
|
@ -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<int> 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<int> 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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var baseUrl = $"{Settings.BaseUrl.TrimEnd('/')}/?page=rss{Settings.AdditionalParameters}";
|
||||
|
|
|
|||
|
|
@ -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<IndexerRequest> GetRequest(string searchParameters)
|
||||
{
|
||||
var request =
|
||||
|
|
|
|||
|
|
@ -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<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IndexerRequest> GetRssRequests(string searchParameters)
|
||||
{
|
||||
var request = new IndexerRequest(Settings.BaseUrl.Trim().TrimEnd('/'), HttpAccept.Rss);
|
||||
|
|
|
|||
|
|
@ -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<TorznabSettings>
|
|||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override int PageSize => GetProviderPageSize();
|
||||
public override IEnumerable<MediaType> 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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
14
src/NzbDrone.Core/MediaTypes/MediaType.cs
Normal file
14
src/NzbDrone.Core/MediaTypes/MediaType.cs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue