mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-25 22:59:10 -04:00
926 lines
39 KiB
C#
926 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using FluentValidation;
|
|
using FluentValidation.Results;
|
|
using Newtonsoft.Json;
|
|
using NLog;
|
|
using NzbDrone.Common.Cache;
|
|
using NzbDrone.Common.Extensions;
|
|
using NzbDrone.Common.Http;
|
|
using NzbDrone.Common.Serializer;
|
|
using NzbDrone.Core.Annotations;
|
|
using NzbDrone.Core.Configuration;
|
|
using NzbDrone.Core.Exceptions;
|
|
using NzbDrone.Core.Indexers.Exceptions;
|
|
using NzbDrone.Core.Indexers.Settings;
|
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
|
using NzbDrone.Core.Messaging.Events;
|
|
using NzbDrone.Core.Parser;
|
|
using NzbDrone.Core.Parser.Model;
|
|
using NzbDrone.Core.ThingiProvider;
|
|
using NzbDrone.Core.Validation;
|
|
|
|
namespace NzbDrone.Core.Indexers.Definitions
|
|
{
|
|
public class MyAnonamouse : TorrentIndexerBase<MyAnonamouseSettings>
|
|
{
|
|
public override string Name => "MyAnonamouse";
|
|
public override string[] IndexerUrls => new[] { "https://www.myanonamouse.net/" };
|
|
public override string Description => "MyAnonaMouse (MAM) is a large ebook and audiobook tracker.";
|
|
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
|
public override bool SupportsPagination => true;
|
|
public override int PageSize => 100;
|
|
public override IndexerCapabilities Capabilities => SetCapabilities();
|
|
|
|
private readonly ICacheManager _cacheManager;
|
|
|
|
public MyAnonamouse(IIndexerHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger, ICacheManager cacheManager)
|
|
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
|
{
|
|
_cacheManager = cacheManager;
|
|
}
|
|
|
|
public override IIndexerRequestGenerator GetRequestGenerator()
|
|
{
|
|
return new MyAnonamouseRequestGenerator(Settings, Capabilities, _logger);
|
|
}
|
|
|
|
public override IParseIndexerResponse GetParser()
|
|
{
|
|
return new MyAnonamouseParser(Definition, Settings, Capabilities.Categories, _httpClient, _cacheManager, _logger);
|
|
}
|
|
|
|
public override async Task<IndexerDownloadResponse> Download(Uri link)
|
|
{
|
|
var downloadLink = link.RemoveQueryParam("canUseToken");
|
|
|
|
if (Settings.UseFreeleechWedge is (int)MyAnonamouseFreeleechWedgeAction.Preferred or (int)MyAnonamouseFreeleechWedgeAction.Required &&
|
|
bool.TryParse(link.GetQueryParam("canUseToken"), out var canUseToken) && canUseToken)
|
|
{
|
|
_logger.Debug("Attempting to use freeleech wedge for {0}", downloadLink.AbsoluteUri);
|
|
|
|
if (int.TryParse(link.GetQueryParam("tid"), out var torrentId) && torrentId > 0)
|
|
{
|
|
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
|
|
var freeleechUrl = Settings.BaseUrl + $"json/bonusBuy.php/{timestamp}";
|
|
|
|
var freeleechRequestBuilder = new HttpRequestBuilder(freeleechUrl)
|
|
.Accept(HttpAccept.Json)
|
|
.AddQueryParam("spendtype", "personalFL")
|
|
.AddQueryParam("torrentid", torrentId)
|
|
.AddQueryParam("timestamp", timestamp.ToString());
|
|
|
|
freeleechRequestBuilder.LogResponseContent = true;
|
|
|
|
var cookies = GetCookies();
|
|
|
|
if (cookies != null && cookies.Any())
|
|
{
|
|
freeleechRequestBuilder.SetCookies(cookies);
|
|
}
|
|
|
|
var freeleechRequest = freeleechRequestBuilder.Build();
|
|
|
|
var freeleechResponse = await _httpClient.ExecuteProxiedAsync(freeleechRequest, Definition).ConfigureAwait(false);
|
|
|
|
var resource = Json.Deserialize<MyAnonamouseBuyPersonalFreeleechResponse>(freeleechResponse.Content);
|
|
|
|
if (resource.Success)
|
|
{
|
|
_logger.Debug("Successfully used freeleech wedge for torrentid {0}.", torrentId);
|
|
}
|
|
else if (resource.Error.IsNotNullOrWhiteSpace() && resource.Error.ContainsIgnoreCase("This Torrent is VIP"))
|
|
{
|
|
_logger.Debug("{0} is already VIP, continuing downloading: {1}", torrentId, resource.Error);
|
|
}
|
|
else if (resource.Error.IsNotNullOrWhiteSpace() && resource.Error.ContainsIgnoreCase("This is already a personal freeleech"))
|
|
{
|
|
_logger.Debug("{0} is already a personal freeleech, continuing downloading: {1}", torrentId, resource.Error);
|
|
}
|
|
else
|
|
{
|
|
_logger.Warn("Failed to purchase freeleech wedge for {0}: {1}", torrentId, resource.Error);
|
|
|
|
if (Settings.UseFreeleechWedge == (int)MyAnonamouseFreeleechWedgeAction.Preferred)
|
|
{
|
|
_logger.Debug("'Use Freeleech Wedge' option set to preferred, continuing downloading: '{0}'", downloadLink.AbsoluteUri);
|
|
}
|
|
else
|
|
{
|
|
throw new ReleaseUnavailableException($"Failed to buy freeleech wedge and 'Use Freeleech Wedge' is set to required, aborting download: '{downloadLink.AbsoluteUri}'");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.Warn("Could not get torrent id from link {0}, skipping use of freeleech wedge.", downloadLink.AbsoluteUri);
|
|
}
|
|
}
|
|
|
|
return await base.Download(downloadLink).ConfigureAwait(false);
|
|
}
|
|
|
|
protected override IDictionary<string, string> GetCookies()
|
|
{
|
|
var cookies = base.GetCookies();
|
|
|
|
if (cookies is { Count: > 0 } && cookies.TryGetValue("mam_id", out var mamId) && mamId.IsNotNullOrWhiteSpace())
|
|
{
|
|
return cookies;
|
|
}
|
|
|
|
return CookieUtil.CookieHeaderToDictionary($"mam_id={Settings.MamId}");
|
|
}
|
|
|
|
protected override async Task Test(List<ValidationFailure> failures)
|
|
{
|
|
UpdateCookies(null, null);
|
|
|
|
_logger.Debug("Cookies cleared.");
|
|
|
|
await base.Test(failures).ConfigureAwait(false);
|
|
}
|
|
|
|
private IndexerCapabilities SetCapabilities()
|
|
{
|
|
var caps = new IndexerCapabilities
|
|
{
|
|
BookSearchParams = new List<BookSearchParam>
|
|
{
|
|
BookSearchParam.Q
|
|
}
|
|
};
|
|
|
|
caps.Categories.AddCategoryMapping("13", NewznabStandardCategory.AudioAudiobook, "AudioBooks");
|
|
caps.Categories.AddCategoryMapping("14", NewznabStandardCategory.BooksEBook, "E-Books");
|
|
caps.Categories.AddCategoryMapping("15", NewznabStandardCategory.AudioAudiobook, "Musicology");
|
|
caps.Categories.AddCategoryMapping("16", NewznabStandardCategory.AudioAudiobook, "Radio");
|
|
caps.Categories.AddCategoryMapping("39", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Action/Adventure");
|
|
caps.Categories.AddCategoryMapping("49", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Art");
|
|
caps.Categories.AddCategoryMapping("50", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Biographical");
|
|
caps.Categories.AddCategoryMapping("83", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Business");
|
|
caps.Categories.AddCategoryMapping("51", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Computer/Internet");
|
|
caps.Categories.AddCategoryMapping("97", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Crafts");
|
|
caps.Categories.AddCategoryMapping("40", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Crime/Thriller");
|
|
caps.Categories.AddCategoryMapping("41", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Fantasy");
|
|
caps.Categories.AddCategoryMapping("106", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Food");
|
|
caps.Categories.AddCategoryMapping("42", NewznabStandardCategory.AudioAudiobook, "Audiobooks - General Fiction");
|
|
caps.Categories.AddCategoryMapping("52", NewznabStandardCategory.AudioAudiobook, "Audiobooks - General Non-Fic");
|
|
caps.Categories.AddCategoryMapping("98", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Historical Fiction");
|
|
caps.Categories.AddCategoryMapping("54", NewznabStandardCategory.AudioAudiobook, "Audiobooks - History");
|
|
caps.Categories.AddCategoryMapping("55", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Home/Garden");
|
|
caps.Categories.AddCategoryMapping("43", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Horror");
|
|
caps.Categories.AddCategoryMapping("99", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Humor");
|
|
caps.Categories.AddCategoryMapping("84", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Instructional");
|
|
caps.Categories.AddCategoryMapping("44", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Juvenile");
|
|
caps.Categories.AddCategoryMapping("56", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Language");
|
|
caps.Categories.AddCategoryMapping("45", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Literary Classics");
|
|
caps.Categories.AddCategoryMapping("57", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Math/Science/Tech");
|
|
caps.Categories.AddCategoryMapping("85", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Medical");
|
|
caps.Categories.AddCategoryMapping("87", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Mystery");
|
|
caps.Categories.AddCategoryMapping("119", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Nature");
|
|
caps.Categories.AddCategoryMapping("88", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Philosophy");
|
|
caps.Categories.AddCategoryMapping("58", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Pol/Soc/Relig");
|
|
caps.Categories.AddCategoryMapping("59", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Recreation");
|
|
caps.Categories.AddCategoryMapping("46", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Romance");
|
|
caps.Categories.AddCategoryMapping("47", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Science Fiction");
|
|
caps.Categories.AddCategoryMapping("53", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Self-Help");
|
|
caps.Categories.AddCategoryMapping("89", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Travel/Adventure");
|
|
caps.Categories.AddCategoryMapping("100", NewznabStandardCategory.AudioAudiobook, "Audiobooks - True Crime");
|
|
caps.Categories.AddCategoryMapping("108", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Urban Fantasy");
|
|
caps.Categories.AddCategoryMapping("48", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Western");
|
|
caps.Categories.AddCategoryMapping("111", NewznabStandardCategory.AudioAudiobook, "Audiobooks - Young Adult");
|
|
caps.Categories.AddCategoryMapping("60", NewznabStandardCategory.BooksEBook, "Ebooks - Action/Adventure");
|
|
caps.Categories.AddCategoryMapping("71", NewznabStandardCategory.BooksEBook, "Ebooks - Art");
|
|
caps.Categories.AddCategoryMapping("72", NewznabStandardCategory.BooksEBook, "Ebooks - Biographical");
|
|
caps.Categories.AddCategoryMapping("90", NewznabStandardCategory.BooksEBook, "Ebooks - Business");
|
|
caps.Categories.AddCategoryMapping("61", NewznabStandardCategory.BooksComics, "Ebooks - Comics/Graphic novels");
|
|
caps.Categories.AddCategoryMapping("73", NewznabStandardCategory.BooksEBook, "Ebooks - Computer/Internet");
|
|
caps.Categories.AddCategoryMapping("101", NewznabStandardCategory.BooksEBook, "Ebooks - Crafts");
|
|
caps.Categories.AddCategoryMapping("62", NewznabStandardCategory.BooksEBook, "Ebooks - Crime/Thriller");
|
|
caps.Categories.AddCategoryMapping("63", NewznabStandardCategory.BooksEBook, "Ebooks - Fantasy");
|
|
caps.Categories.AddCategoryMapping("107", NewznabStandardCategory.BooksEBook, "Ebooks - Food");
|
|
caps.Categories.AddCategoryMapping("64", NewznabStandardCategory.BooksEBook, "Ebooks - General Fiction");
|
|
caps.Categories.AddCategoryMapping("74", NewznabStandardCategory.BooksEBook, "Ebooks - General Non-Fiction");
|
|
caps.Categories.AddCategoryMapping("102", NewznabStandardCategory.BooksEBook, "Ebooks - Historical Fiction");
|
|
caps.Categories.AddCategoryMapping("76", NewznabStandardCategory.BooksEBook, "Ebooks - History");
|
|
caps.Categories.AddCategoryMapping("77", NewznabStandardCategory.BooksEBook, "Ebooks - Home/Garden");
|
|
caps.Categories.AddCategoryMapping("65", NewznabStandardCategory.BooksEBook, "Ebooks - Horror");
|
|
caps.Categories.AddCategoryMapping("103", NewznabStandardCategory.BooksEBook, "Ebooks - Humor");
|
|
caps.Categories.AddCategoryMapping("115", NewznabStandardCategory.BooksEBook, "Ebooks - Illusion/Magic");
|
|
caps.Categories.AddCategoryMapping("91", NewznabStandardCategory.BooksEBook, "Ebooks - Instructional");
|
|
caps.Categories.AddCategoryMapping("66", NewznabStandardCategory.BooksEBook, "Ebooks - Juvenile");
|
|
caps.Categories.AddCategoryMapping("78", NewznabStandardCategory.BooksEBook, "Ebooks - Language");
|
|
caps.Categories.AddCategoryMapping("67", NewznabStandardCategory.BooksEBook, "Ebooks - Literary Classics");
|
|
caps.Categories.AddCategoryMapping("79", NewznabStandardCategory.BooksMags, "Ebooks - Magazines/Newspapers");
|
|
caps.Categories.AddCategoryMapping("80", NewznabStandardCategory.BooksTechnical, "Ebooks - Math/Science/Tech");
|
|
caps.Categories.AddCategoryMapping("92", NewznabStandardCategory.BooksEBook, "Ebooks - Medical");
|
|
caps.Categories.AddCategoryMapping("118", NewznabStandardCategory.BooksEBook, "Ebooks - Mixed Collections");
|
|
caps.Categories.AddCategoryMapping("94", NewznabStandardCategory.BooksEBook, "Ebooks - Mystery");
|
|
caps.Categories.AddCategoryMapping("120", NewznabStandardCategory.BooksEBook, "Ebooks - Nature");
|
|
caps.Categories.AddCategoryMapping("95", NewznabStandardCategory.BooksEBook, "Ebooks - Philosophy");
|
|
caps.Categories.AddCategoryMapping("81", NewznabStandardCategory.BooksEBook, "Ebooks - Pol/Soc/Relig");
|
|
caps.Categories.AddCategoryMapping("82", NewznabStandardCategory.BooksEBook, "Ebooks - Recreation");
|
|
caps.Categories.AddCategoryMapping("68", NewznabStandardCategory.BooksEBook, "Ebooks - Romance");
|
|
caps.Categories.AddCategoryMapping("69", NewznabStandardCategory.BooksEBook, "Ebooks - Science Fiction");
|
|
caps.Categories.AddCategoryMapping("75", NewznabStandardCategory.BooksEBook, "Ebooks - Self-Help");
|
|
caps.Categories.AddCategoryMapping("96", NewznabStandardCategory.BooksEBook, "Ebooks - Travel/Adventure");
|
|
caps.Categories.AddCategoryMapping("104", NewznabStandardCategory.BooksEBook, "Ebooks - True Crime");
|
|
caps.Categories.AddCategoryMapping("109", NewznabStandardCategory.BooksEBook, "Ebooks - Urban Fantasy");
|
|
caps.Categories.AddCategoryMapping("70", NewznabStandardCategory.BooksEBook, "Ebooks - Western");
|
|
caps.Categories.AddCategoryMapping("112", NewznabStandardCategory.BooksEBook, "Ebooks - Young Adult");
|
|
caps.Categories.AddCategoryMapping("19", NewznabStandardCategory.AudioAudiobook, "Guitar/Bass Tabs");
|
|
caps.Categories.AddCategoryMapping("20", NewznabStandardCategory.AudioAudiobook, "Individual Sheet");
|
|
caps.Categories.AddCategoryMapping("24", NewznabStandardCategory.AudioAudiobook, "Individual Sheet MP3");
|
|
caps.Categories.AddCategoryMapping("126", NewznabStandardCategory.AudioAudiobook, "Instructional Book with Video");
|
|
caps.Categories.AddCategoryMapping("22", NewznabStandardCategory.AudioAudiobook, "Instructional Media - Music");
|
|
caps.Categories.AddCategoryMapping("113", NewznabStandardCategory.AudioAudiobook, "Lick Library - LTP/Jam With");
|
|
caps.Categories.AddCategoryMapping("114", NewznabStandardCategory.AudioAudiobook, "Lick Library - Techniques/QL");
|
|
caps.Categories.AddCategoryMapping("17", NewznabStandardCategory.AudioAudiobook, "Music - Complete Editions");
|
|
caps.Categories.AddCategoryMapping("26", NewznabStandardCategory.AudioAudiobook, "Music Book");
|
|
caps.Categories.AddCategoryMapping("27", NewznabStandardCategory.AudioAudiobook, "Music Book MP3");
|
|
caps.Categories.AddCategoryMapping("30", NewznabStandardCategory.AudioAudiobook, "Sheet Collection");
|
|
caps.Categories.AddCategoryMapping("31", NewznabStandardCategory.AudioAudiobook, "Sheet Collection MP3");
|
|
caps.Categories.AddCategoryMapping("127", NewznabStandardCategory.AudioAudiobook, "Radio - Comedy");
|
|
caps.Categories.AddCategoryMapping("130", NewznabStandardCategory.AudioAudiobook, "Radio - Drama");
|
|
caps.Categories.AddCategoryMapping("128", NewznabStandardCategory.AudioAudiobook, "Radio - Factual/Documentary");
|
|
caps.Categories.AddCategoryMapping("132", NewznabStandardCategory.AudioAudiobook, "Radio - Reading");
|
|
|
|
return caps;
|
|
}
|
|
}
|
|
|
|
public class MyAnonamouseRequestGenerator : IIndexerRequestGenerator
|
|
{
|
|
private static readonly Regex SanitizeSearchQueryRegex = new("[^\\w]+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
|
|
private readonly MyAnonamouseSettings _settings;
|
|
private readonly IndexerCapabilities _capabilities;
|
|
private readonly Logger _logger;
|
|
|
|
public MyAnonamouseRequestGenerator(MyAnonamouseSettings settings, IndexerCapabilities capabilities, Logger logger)
|
|
{
|
|
_settings = settings;
|
|
_capabilities = capabilities;
|
|
_logger = logger;
|
|
}
|
|
|
|
private IEnumerable<IndexerRequest> GetPagedRequests(SearchCriteriaBase searchCriteria)
|
|
{
|
|
var term = SanitizeSearchQueryRegex.Replace(searchCriteria.SanitizedSearchTerm, " ").Trim();
|
|
|
|
if (searchCriteria.SearchTerm.IsNotNullOrWhiteSpace() && term.IsNullOrWhiteSpace())
|
|
{
|
|
_logger.Debug("Search term is empty after being sanitized, stopping search. Initial search term: '{0}'", searchCriteria.SearchTerm);
|
|
|
|
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]", term },
|
|
{ "tor[searchType]", searchType },
|
|
{ "tor[srchIn][title]", "true" },
|
|
{ "tor[srchIn][author]", "true" },
|
|
{ "tor[srchIn][narrator]", "true" },
|
|
{ "tor[searchIn]", "torrents" },
|
|
{ "tor[sortType]", "default" },
|
|
{ "tor[perpage]", searchCriteria.Limit?.ToString() ?? "100" },
|
|
{ "tor[startNumber]", searchCriteria.Offset?.ToString() ?? "0" },
|
|
{ "thumbnails", "1" }, // gives links for thumbnail sized versions of their posters
|
|
{ "description", "1" } // include the description
|
|
};
|
|
|
|
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");
|
|
}
|
|
|
|
if (_settings.SearchLanguages.Any())
|
|
{
|
|
foreach (var (language, index) in _settings.SearchLanguages.Select((value, index) => (value, index)))
|
|
{
|
|
parameters.Set($"tor[browse_lang][{index}]", language.ToString());
|
|
}
|
|
}
|
|
|
|
var catList = _capabilities.Categories.MapTorznabCapsToTrackers(searchCriteria.Categories).Distinct().ToList();
|
|
|
|
if (catList.Any())
|
|
{
|
|
foreach (var (category, index) in catList.Select((value, index) => (value, index)))
|
|
{
|
|
parameters.Set($"tor[cat][{index}]", category);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
parameters.Set("tor[cat][]", "0");
|
|
}
|
|
|
|
if (searchCriteria.MinSize is > 0)
|
|
{
|
|
parameters.Set("tor[minSize]", searchCriteria.MinSize.Value.ToString());
|
|
}
|
|
|
|
if (searchCriteria.MaxSize is > 0)
|
|
{
|
|
parameters.Set("tor[maxSize]", searchCriteria.MaxSize.Value.ToString());
|
|
}
|
|
|
|
if (searchCriteria.MinSize is > 0 || searchCriteria.MaxSize is > 0)
|
|
{
|
|
parameters.Set("tor[unit]", "1");
|
|
}
|
|
|
|
var searchUrl = _settings.BaseUrl + "tor/js/loadSearchJSONbasic.php";
|
|
|
|
if (parameters.Count > 0)
|
|
{
|
|
searchUrl += $"?{parameters.GetQueryString()}";
|
|
}
|
|
|
|
var request = new IndexerRequest(searchUrl, HttpAccept.Json);
|
|
|
|
yield return request;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
|
{
|
|
return new IndexerPageableRequestChain();
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
|
{
|
|
return new IndexerPageableRequestChain();
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
|
{
|
|
return new IndexerPageableRequestChain();
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests(searchCriteria));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
|
{
|
|
var pageableRequests = new IndexerPageableRequestChain();
|
|
|
|
pageableRequests.Add(GetPagedRequests(searchCriteria));
|
|
|
|
return pageableRequests;
|
|
}
|
|
|
|
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
}
|
|
|
|
public class MyAnonamouseParser : IParseIndexerResponse
|
|
{
|
|
private readonly ProviderDefinition _definition;
|
|
private readonly MyAnonamouseSettings _settings;
|
|
private readonly IndexerCapabilitiesCategories _categories;
|
|
private readonly IIndexerHttpClient _httpClient;
|
|
private readonly Logger _logger;
|
|
|
|
private readonly ICached<string> _userClassCache;
|
|
private readonly HashSet<string> _vipFreeleechUserClasses = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"VIP",
|
|
"Elite VIP"
|
|
};
|
|
|
|
public MyAnonamouseParser(ProviderDefinition definition,
|
|
MyAnonamouseSettings settings,
|
|
IndexerCapabilitiesCategories categories,
|
|
IIndexerHttpClient httpClient,
|
|
ICacheManager cacheManager,
|
|
Logger logger)
|
|
{
|
|
_definition = definition;
|
|
_settings = settings;
|
|
_categories = categories;
|
|
_httpClient = httpClient;
|
|
_logger = logger;
|
|
|
|
_userClassCache = cacheManager.GetCache<string>(GetType());
|
|
}
|
|
|
|
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
|
{
|
|
var httpResponse = indexerResponse.HttpResponse;
|
|
|
|
// Throw auth errors here before we try to parse
|
|
if (httpResponse.StatusCode == HttpStatusCode.Forbidden)
|
|
{
|
|
throw new IndexerAuthException("[403 Forbidden] - mam_id expired or invalid");
|
|
}
|
|
|
|
// Throw common http errors here before we try to parse
|
|
if (httpResponse.StatusCode != HttpStatusCode.OK)
|
|
{
|
|
// Remove cookie cache
|
|
CookiesUpdater(null, null);
|
|
|
|
throw new IndexerException(indexerResponse, $"Unexpected response status {httpResponse.StatusCode} code from indexer request");
|
|
}
|
|
|
|
if (!httpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
|
{
|
|
// Remove cookie cache
|
|
CookiesUpdater(null, null);
|
|
|
|
throw new IndexerException(indexerResponse, $"Unexpected response header {httpResponse.Headers.ContentType} from indexer request, expected {HttpAccept.Json.Value}");
|
|
}
|
|
|
|
var releaseInfos = new List<ReleaseInfo>();
|
|
|
|
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseResponse>(indexerResponse.Content);
|
|
|
|
var error = jsonResponse.Error;
|
|
if (error.IsNotNullOrWhiteSpace() && error.StartsWithIgnoreCase("Nothing returned, out of"))
|
|
{
|
|
return releaseInfos.ToArray();
|
|
}
|
|
|
|
if (jsonResponse.Data == null)
|
|
{
|
|
throw new IndexerException(indexerResponse, "Unexpected response content from indexer request: {0}", jsonResponse.Message ?? "Check the logs for more information.");
|
|
}
|
|
|
|
var hasUserVip = HasUserVip(httpResponse.GetCookies());
|
|
|
|
foreach (var item in jsonResponse.Data)
|
|
{
|
|
//TODO shift to ReleaseInfo object initializer for consistency
|
|
var release = new TorrentInfo();
|
|
|
|
var id = item.Id;
|
|
|
|
release.Title = item.Title;
|
|
release.Description = item.Description;
|
|
|
|
release.BookTitle = item.Title;
|
|
|
|
if (item.AuthorInfo != null)
|
|
{
|
|
var authorInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.AuthorInfo);
|
|
var author = authorInfo?.Take(5).Select(v => v.Value).Join(", ");
|
|
|
|
if (author.IsNotNullOrWhiteSpace())
|
|
{
|
|
release.Title += " by " + author;
|
|
release.Author = author;
|
|
}
|
|
}
|
|
|
|
var flags = new List<string>();
|
|
|
|
var languageCode = item.LanguageCode;
|
|
if (!string.IsNullOrEmpty(languageCode))
|
|
{
|
|
flags.Add(languageCode);
|
|
}
|
|
|
|
var filetype = item.Filetype;
|
|
if (!string.IsNullOrEmpty(filetype))
|
|
{
|
|
flags.Add(filetype.ToUpper());
|
|
}
|
|
|
|
if (flags.Count > 0)
|
|
{
|
|
release.Title += " [" + flags.Join(" / ") + "]";
|
|
}
|
|
|
|
if (item.Vip)
|
|
{
|
|
release.Title += " [VIP]";
|
|
}
|
|
|
|
var isFreeLeech = item.Free || item.PersonalFreeLeech || (hasUserVip && item.FreeVip);
|
|
|
|
release.DownloadUrl = GetDownloadUrl(id, !isFreeLeech);
|
|
release.InfoUrl = $"{_settings.BaseUrl}t/{id}";
|
|
release.Guid = release.InfoUrl;
|
|
release.Categories = _categories.MapTrackerCatToNewznab(item.Category);
|
|
release.PublishDate = DateTime.ParseExact(item.Added, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime();
|
|
release.Grabs = item.Grabs;
|
|
release.Files = item.NumFiles;
|
|
release.Seeders = item.Seeders;
|
|
release.Peers = item.Leechers + release.Seeders;
|
|
release.Size = ParseUtil.GetBytes(item.Size);
|
|
release.DownloadVolumeFactor = isFreeLeech ? 0 : 1;
|
|
release.UploadVolumeFactor = 1;
|
|
release.MinimumRatio = 1;
|
|
release.MinimumSeedTime = 259200; // 72 hours
|
|
|
|
releaseInfos.Add(release);
|
|
}
|
|
|
|
// Update cookies with the updated mam_id value received in the response
|
|
CookiesUpdater(httpResponse.GetCookies(), DateTime.Now.AddDays(30));
|
|
|
|
return releaseInfos.ToArray();
|
|
}
|
|
|
|
private string GetDownloadUrl(int torrentId, bool canUseToken)
|
|
{
|
|
var url = new HttpUri(_settings.BaseUrl)
|
|
.CombinePath("/tor/download.php")
|
|
.AddQueryParam("tid", torrentId);
|
|
|
|
if (_settings.UseFreeleechWedge is (int)MyAnonamouseFreeleechWedgeAction.Preferred or (int)MyAnonamouseFreeleechWedgeAction.Required && canUseToken)
|
|
{
|
|
url = url.AddQueryParam("canUseToken", "true");
|
|
}
|
|
|
|
return url.FullUri;
|
|
}
|
|
|
|
private bool HasUserVip(Dictionary<string, string> cookies)
|
|
{
|
|
var cacheKey = "myanonamouse_user_class_" + _settings.ToJson().SHA256Hash();
|
|
|
|
var userClass = _userClassCache.Get(
|
|
cacheKey,
|
|
() =>
|
|
{
|
|
var request = new HttpRequestBuilder(_settings.BaseUrl.Trim('/'))
|
|
.Resource("/jsonLoad.php")
|
|
.Accept(HttpAccept.Json)
|
|
.SetCookies(cookies)
|
|
.Build();
|
|
|
|
_logger.Debug("Fetching user data: {0}", request.Url.FullUri);
|
|
|
|
var response = _httpClient.ExecuteProxied(request, _definition);
|
|
|
|
var jsonResponse = JsonConvert.DeserializeObject<MyAnonamouseUserDataResponse>(response.Content);
|
|
|
|
_logger.Trace("Current user class: '{0}'", jsonResponse.UserClass);
|
|
|
|
return jsonResponse.UserClass?.Trim();
|
|
},
|
|
TimeSpan.FromHours(1));
|
|
|
|
return _vipFreeleechUserClasses.Contains(userClass);
|
|
}
|
|
|
|
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
|
}
|
|
|
|
public class MyAnonamouseSettingsValidator : NoAuthSettingsValidator<MyAnonamouseSettings>
|
|
{
|
|
public MyAnonamouseSettingsValidator()
|
|
{
|
|
RuleFor(c => c.MamId).NotEmpty();
|
|
}
|
|
}
|
|
|
|
public class MyAnonamouseSettings : NoAuthTorrentBaseSettings
|
|
{
|
|
private static readonly MyAnonamouseSettingsValidator Validator = new();
|
|
|
|
public MyAnonamouseSettings()
|
|
{
|
|
MamId = "";
|
|
SearchType = (int)MyAnonamouseSearchType.All;
|
|
SearchInDescription = false;
|
|
SearchInSeries = false;
|
|
SearchInFilenames = false;
|
|
SearchLanguages = Array.Empty<int>();
|
|
UseFreeleechWedge = (int)MyAnonamouseFreeleechWedgeAction.Never;
|
|
}
|
|
|
|
[FieldDefinition(2, Type = FieldType.Textbox, Label = "Mam Id", HelpText = "Mam Session Id (Created Under Preferences -> Security)")]
|
|
public string MamId { get; set; }
|
|
|
|
[FieldDefinition(3, Type = FieldType.Select, Label = "Search Type", SelectOptions = typeof(MyAnonamouseSearchType), HelpText = "Specify the desired search type")]
|
|
public int SearchType { get; set; }
|
|
|
|
[FieldDefinition(4, Type = FieldType.Checkbox, Label = "Search in description", HelpText = "Search text in the description")]
|
|
public bool SearchInDescription { get; set; }
|
|
|
|
[FieldDefinition(5, Type = FieldType.Checkbox, Label = "Search in series", HelpText = "Search text in the series")]
|
|
public bool SearchInSeries { get; set; }
|
|
|
|
[FieldDefinition(6, Type = FieldType.Checkbox, Label = "Search in filenames", HelpText = "Search text in the filenames")]
|
|
public bool SearchInFilenames { get; set; }
|
|
|
|
[FieldDefinition(7, Type = FieldType.Select, Label = "Search Languages", SelectOptions = typeof(MyAnonamouseSearchLanguages), HelpText = "Specify the desired languages. If unspecified, all options are used.")]
|
|
public IEnumerable<int> SearchLanguages { get; set; }
|
|
|
|
[FieldDefinition(8, Type = FieldType.Select, Label = "Use Freeleech Wedges", SelectOptions = typeof(MyAnonamouseFreeleechWedgeAction), HelpText = "Use freeleech wedges to make grabbed torrents personal freeleech")]
|
|
public int UseFreeleechWedge { get; set; }
|
|
|
|
public override NzbDroneValidationResult Validate()
|
|
{
|
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
|
}
|
|
}
|
|
|
|
public enum MyAnonamouseSearchType
|
|
{
|
|
[FieldOption(Label="All torrents", Hint = "Search everything")]
|
|
All = 0,
|
|
|
|
[FieldOption(Label="Only active", Hint = "Last update had 1+ seeders")]
|
|
Active = 1,
|
|
|
|
[FieldOption(Label="Freeleech", Hint = "Freeleech torrents")]
|
|
Freeleech = 2,
|
|
|
|
[FieldOption(Label="Freeleech or VIP", Hint = "Freeleech or VIP torrents")]
|
|
FreeleechOrVip = 3,
|
|
|
|
[FieldOption(Label="VIP", Hint = "VIP torrents")]
|
|
Vip = 4,
|
|
|
|
[FieldOption(Label="Not VIP", Hint = "Torrents not VIP")]
|
|
NotVip = 5,
|
|
}
|
|
|
|
public enum MyAnonamouseSearchLanguages
|
|
{
|
|
[FieldOption(Label="English")]
|
|
English = 1,
|
|
|
|
[FieldOption(Label="Afrikaans")]
|
|
Afrikaans = 17,
|
|
|
|
[FieldOption(Label="Arabic")]
|
|
Arabic = 32,
|
|
|
|
[FieldOption(Label="Bengali")]
|
|
Bengali = 35,
|
|
|
|
[FieldOption(Label="Bosnian")]
|
|
Bosnian = 51,
|
|
|
|
[FieldOption(Label="Bulgarian")]
|
|
Bulgarian = 18,
|
|
|
|
[FieldOption(Label="Burmese")]
|
|
Burmese = 6,
|
|
|
|
[FieldOption(Label="Cantonese")]
|
|
Cantonese = 44,
|
|
|
|
[FieldOption(Label="Catalan")]
|
|
Catalan = 19,
|
|
|
|
[FieldOption(Label="Chinese")]
|
|
Chinese = 2,
|
|
|
|
[FieldOption(Label="Croatian")]
|
|
Croatian = 49,
|
|
|
|
[FieldOption(Label="Czech")]
|
|
Czech = 20,
|
|
|
|
[FieldOption(Label="Danish")]
|
|
Danish = 21,
|
|
|
|
[FieldOption(Label="Dutch")]
|
|
Dutch = 22,
|
|
|
|
[FieldOption(Label="Estonian")]
|
|
Estonian = 61,
|
|
|
|
[FieldOption(Label="Farsi")]
|
|
Farsi = 39,
|
|
|
|
[FieldOption(Label="Finnish")]
|
|
Finnish = 23,
|
|
|
|
[FieldOption(Label="French")]
|
|
French = 36,
|
|
|
|
[FieldOption(Label="German")]
|
|
German = 37,
|
|
|
|
[FieldOption(Label="Greek")]
|
|
Greek = 26,
|
|
|
|
[FieldOption(Label="Greek, Ancient")]
|
|
GreekAncient = 59,
|
|
|
|
[FieldOption(Label="Gujarati")]
|
|
Gujarati = 3,
|
|
|
|
[FieldOption(Label="Hebrew")]
|
|
Hebrew = 27,
|
|
|
|
[FieldOption(Label="Hindi")]
|
|
Hindi = 8,
|
|
|
|
[FieldOption(Label="Hungarian")]
|
|
Hungarian = 28,
|
|
|
|
[FieldOption(Label="Icelandic")]
|
|
Icelandic = 63,
|
|
|
|
[FieldOption(Label="Indonesian")]
|
|
Indonesian = 53,
|
|
|
|
[FieldOption(Label="Irish")]
|
|
Irish = 56,
|
|
|
|
[FieldOption(Label="Italian")]
|
|
Italian = 43,
|
|
|
|
[FieldOption(Label="Japanese")]
|
|
Japanese = 38,
|
|
|
|
[FieldOption(Label="Javanese")]
|
|
Javanese = 12,
|
|
|
|
[FieldOption(Label="Kannada")]
|
|
Kannada = 5,
|
|
|
|
[FieldOption(Label="Korean")]
|
|
Korean = 41,
|
|
|
|
[FieldOption(Label="Lithuanian")]
|
|
Lithuanian = 50,
|
|
|
|
[FieldOption(Label="Latin")]
|
|
Latin = 46,
|
|
|
|
[FieldOption(Label="Latvian")]
|
|
Latvian = 62,
|
|
|
|
[FieldOption(Label="Malay")]
|
|
Malay = 33,
|
|
|
|
[FieldOption(Label="Malayalam")]
|
|
Malayalam = 58,
|
|
|
|
[FieldOption(Label="Manx")]
|
|
Manx = 57,
|
|
|
|
[FieldOption(Label="Marathi")]
|
|
Marathi = 9,
|
|
|
|
[FieldOption(Label="Norwegian")]
|
|
Norwegian = 48,
|
|
|
|
[FieldOption(Label="Polish")]
|
|
Polish = 45,
|
|
|
|
[FieldOption(Label="Portuguese")]
|
|
Portuguese = 34,
|
|
|
|
[FieldOption(Label="Brazilian Portuguese (BP)")]
|
|
BrazilianPortuguese = 52,
|
|
|
|
[FieldOption(Label="Punjabi")]
|
|
Punjabi = 14,
|
|
|
|
[FieldOption(Label="Romanian")]
|
|
Romanian = 30,
|
|
|
|
[FieldOption(Label="Russian")]
|
|
Russian = 16,
|
|
|
|
[FieldOption(Label="Scottish Gaelic")]
|
|
ScottishGaelic = 24,
|
|
|
|
[FieldOption(Label="Sanskrit")]
|
|
Sanskrit = 60,
|
|
|
|
[FieldOption(Label="Serbian")]
|
|
Serbian = 31,
|
|
|
|
[FieldOption(Label="Slovenian")]
|
|
Slovenian = 54,
|
|
|
|
[FieldOption(Label="Spanish")]
|
|
Spanish = 4,
|
|
|
|
[FieldOption(Label="Castilian Spanish")]
|
|
CastilianSpanish = 55,
|
|
|
|
[FieldOption(Label="Swedish")]
|
|
Swedish = 40,
|
|
|
|
[FieldOption(Label="Tagalog")]
|
|
Tagalog = 29,
|
|
|
|
[FieldOption(Label="Tamil")]
|
|
Tamil = 11,
|
|
|
|
[FieldOption(Label="Telugu")]
|
|
Telugu = 10,
|
|
|
|
[FieldOption(Label="Thai")]
|
|
Thai = 7,
|
|
|
|
[FieldOption(Label="Turkish")]
|
|
Turkish = 42,
|
|
|
|
[FieldOption(Label="Ukrainian")]
|
|
Ukrainian = 25,
|
|
|
|
[FieldOption(Label="Urdu")]
|
|
Urdu = 15,
|
|
|
|
[FieldOption(Label="Vietnamese")]
|
|
Vietnamese = 13,
|
|
|
|
[FieldOption(Label="Other")]
|
|
Other = 47,
|
|
}
|
|
|
|
public enum MyAnonamouseFreeleechWedgeAction
|
|
{
|
|
[FieldOption(Label = "Never", Hint = "Do not buy as freeleech")]
|
|
Never = 0,
|
|
|
|
[FieldOption(Label = "Preferred", Hint = "Buy and use wedge if possible")]
|
|
Preferred = 1,
|
|
|
|
[FieldOption(Label = "Required", Hint = "Abort download if unable to buy wedge")]
|
|
Required = 2,
|
|
}
|
|
|
|
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 MyAnonamouseResponse
|
|
{
|
|
public string Error { get; set; }
|
|
public IReadOnlyCollection<MyAnonamouseTorrent> Data { get; set; }
|
|
public string Message { get; set; }
|
|
}
|
|
|
|
public class MyAnonamouseBuyPersonalFreeleechResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Error { get; set; }
|
|
}
|
|
|
|
public class MyAnonamouseUserDataResponse
|
|
{
|
|
[JsonProperty(PropertyName = "classname")]
|
|
public string UserClass { get; set; }
|
|
}
|
|
}
|