2020-02-09 02:43:32 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Collections.Specialized;
|
2020-05-03 23:35:52 +00:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
2020-02-09 02:43:32 +00:00
|
|
|
using System.Globalization;
|
|
|
|
using System.IO;
|
|
|
|
using System.Linq;
|
2020-04-11 02:46:15 +00:00
|
|
|
using System.Net;
|
2020-02-09 02:43:32 +00:00
|
|
|
using System.Text;
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
using AngleSharp.Dom;
|
|
|
|
using AngleSharp.Html.Parser;
|
|
|
|
using Jackett.Common.Models;
|
|
|
|
using Jackett.Common.Models.IndexerConfig;
|
|
|
|
using Jackett.Common.Services.Interfaces;
|
|
|
|
using Jackett.Common.Utils;
|
2020-06-11 15:09:27 +00:00
|
|
|
using Jackett.Common.Utils.Clients;
|
2020-02-09 02:43:32 +00:00
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using NLog;
|
|
|
|
using static Jackett.Common.Models.IndexerConfig.ConfigurationData;
|
|
|
|
|
|
|
|
namespace Jackett.Common.Indexers
|
|
|
|
{
|
2020-05-03 23:35:52 +00:00
|
|
|
[ExcludeFromCodeCoverage]
|
2020-02-09 02:43:32 +00:00
|
|
|
public class Anidex : BaseWebIndexer
|
|
|
|
{
|
|
|
|
public Anidex(IIndexerConfigurationService configService, Utils.Clients.WebClient wc, Logger l, IProtectionService ps)
|
2020-05-11 19:59:28 +00:00
|
|
|
: base(id: "anidex",
|
|
|
|
name: "Anidex",
|
2020-04-11 02:46:15 +00:00
|
|
|
description: "Anidex is a Public torrent tracker and indexer, primarily for English fansub groups of anime",
|
|
|
|
link: "https://anidex.info/",
|
|
|
|
caps: new TorznabCapabilities(),
|
|
|
|
configService: configService,
|
|
|
|
client: wc,
|
|
|
|
logger: l,
|
|
|
|
p: ps,
|
|
|
|
configData: new ConfigurationData())
|
2020-02-09 02:43:32 +00:00
|
|
|
{
|
|
|
|
Encoding = Encoding.UTF8;
|
|
|
|
Language = "en-us";
|
|
|
|
Type = "public";
|
|
|
|
|
|
|
|
// Configure the category mappings
|
|
|
|
AddCategoryMapping(1, TorznabCatType.TVAnime, "Anime - Sub");
|
|
|
|
AddCategoryMapping(2, TorznabCatType.TVAnime, "Anime - Raw");
|
|
|
|
AddCategoryMapping(3, TorznabCatType.TVAnime, "Anime - Dub");
|
|
|
|
AddCategoryMapping(4, TorznabCatType.TVAnime, "LA - Sub");
|
|
|
|
AddCategoryMapping(5, TorznabCatType.TVAnime, "LA - Raw");
|
|
|
|
AddCategoryMapping(6, TorznabCatType.TVAnime, "Light Novel");
|
|
|
|
AddCategoryMapping(7, TorznabCatType.TVAnime, "Manga - TLed");
|
|
|
|
AddCategoryMapping(8, TorznabCatType.TVAnime, "Manga - Raw");
|
|
|
|
AddCategoryMapping(9, TorznabCatType.TVAnime, "♫ - Lossy");
|
|
|
|
AddCategoryMapping(10, TorznabCatType.TVAnime, "♫ - Lossless");
|
|
|
|
AddCategoryMapping(11, TorznabCatType.TVAnime, "♫ - Video");
|
|
|
|
AddCategoryMapping(12, TorznabCatType.TVAnime, "Games");
|
|
|
|
AddCategoryMapping(13, TorznabCatType.TVAnime, "Applications");
|
|
|
|
AddCategoryMapping(14, TorznabCatType.TVAnime, "Pictures");
|
|
|
|
AddCategoryMapping(15, TorznabCatType.TVAnime, "Adult Video");
|
|
|
|
AddCategoryMapping(16, TorznabCatType.TVAnime, "Other");
|
|
|
|
|
|
|
|
// Configure the language select option
|
|
|
|
var languageSelect = new SelectItem(new Dictionary<string, string>()
|
|
|
|
{
|
|
|
|
{"1", "English"},
|
|
|
|
{"2", "Japanese"},
|
|
|
|
{"3", "Polish"},
|
|
|
|
{"4", "Serbo-Croatian" },
|
|
|
|
{"5", "Dutch"},
|
|
|
|
{"6", "Italian"},
|
|
|
|
{"7", "Russian"},
|
|
|
|
{"8", "German"},
|
|
|
|
{"9", "Hungarian"},
|
|
|
|
{"10", "French"},
|
|
|
|
{"11", "Finnish"},
|
|
|
|
{"12", "Vietnamese"},
|
|
|
|
{"13", "Greek"},
|
|
|
|
{"14", "Bulgarian"},
|
|
|
|
{"15", "Spanish (Spain)" },
|
|
|
|
{"16", "Portuguese (Brazil)" },
|
|
|
|
{"17", "Portuguese (Portugal)" },
|
|
|
|
{"18", "Swedish"},
|
|
|
|
{"19", "Arabic"},
|
|
|
|
{"20", "Danish"},
|
|
|
|
{"21", "Chinese (Simplified)" },
|
|
|
|
{"22", "Bengali"},
|
|
|
|
{"23", "Romanian"},
|
|
|
|
{"24", "Czech"},
|
|
|
|
{"25", "Mongolian"},
|
|
|
|
{"26", "Turkish"},
|
|
|
|
{"27", "Indonesian"},
|
|
|
|
{"28", "Korean"},
|
|
|
|
{"29", "Spanish (LATAM)" },
|
|
|
|
{"30", "Persian"},
|
|
|
|
{"31", "Malaysian"}
|
2020-02-10 22:16:19 +00:00
|
|
|
})
|
|
|
|
{ Name = "Language", Value = "1" };
|
2020-02-09 02:43:32 +00:00
|
|
|
configData.AddDynamic("languageid", languageSelect);
|
|
|
|
|
|
|
|
// Configure the sort selects
|
|
|
|
var sortBySelect = new SelectItem(new Dictionary<string, string>()
|
|
|
|
{
|
|
|
|
{"upload_timestamp", "created"},
|
|
|
|
{"seeders", "seeders"},
|
|
|
|
{"size", "size"},
|
|
|
|
{"filename", "title"}
|
2020-02-10 22:16:19 +00:00
|
|
|
})
|
|
|
|
{ Name = "Sort by", Value = "upload_timestamp" };
|
2020-02-09 02:43:32 +00:00
|
|
|
configData.AddDynamic("sortrequestedfromsite", sortBySelect);
|
|
|
|
|
|
|
|
var orderSelect = new SelectItem(new Dictionary<string, string>()
|
|
|
|
{
|
|
|
|
{"desc", "Descending"},
|
|
|
|
{"asc", "Ascending"}
|
|
|
|
})
|
|
|
|
{ Name = "Order", Value = "desc" };
|
|
|
|
configData.AddDynamic("orderrequestedfromsite", orderSelect);
|
|
|
|
}
|
|
|
|
|
|
|
|
private string GetSortBy => ((SelectItem)configData.GetDynamic("sortrequestedfromsite")).Value;
|
|
|
|
|
|
|
|
private string GetOrder => ((SelectItem)configData.GetDynamic("orderrequestedfromsite")).Value;
|
|
|
|
|
|
|
|
private Uri GetAbsoluteUrl(string relativeUrl) => new Uri(SiteLink + relativeUrl.TrimStart('/'));
|
|
|
|
|
|
|
|
public override async Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson)
|
|
|
|
{
|
|
|
|
LoadValuesFromJson(configJson);
|
|
|
|
var releases = await PerformQuery(new TorznabQuery());
|
|
|
|
|
|
|
|
await ConfigureIfOK(string.Empty, releases.Any(), () =>
|
|
|
|
throw new Exception("Could not find releases from this URL"));
|
|
|
|
|
|
|
|
return IndexerConfigurationStatus.Completed;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override async Task<IEnumerable<ReleaseInfo>> PerformQuery(TorznabQuery query)
|
|
|
|
{
|
|
|
|
// Prepare the search query
|
|
|
|
var queryParameters = new NameValueCollection
|
|
|
|
{
|
|
|
|
{ "q", query.SearchTerm ?? string.Empty },
|
|
|
|
{ "s", GetSortBy },
|
2020-07-21 23:25:56 +00:00
|
|
|
{ "o", GetOrder },
|
|
|
|
{ "group", "0" }, // No group
|
2020-02-09 02:43:32 +00:00
|
|
|
};
|
|
|
|
|
2020-07-21 23:25:56 +00:00
|
|
|
// Get specified categories
|
|
|
|
var searchCategories = MapTorznabCapsToTrackers(query);
|
|
|
|
if (searchCategories.Count > 0)
|
|
|
|
queryParameters.Add("id", string.Join(",", searchCategories));
|
|
|
|
|
2020-02-09 02:43:32 +00:00
|
|
|
// Make search request
|
|
|
|
var searchUri = GetAbsoluteUrl("?" + queryParameters.GetQueryString());
|
2020-06-11 15:09:27 +00:00
|
|
|
var response = await RequestWithCookiesAndRetryAsync(searchUri.AbsoluteUri);
|
2020-02-09 02:43:32 +00:00
|
|
|
|
2020-04-11 02:46:15 +00:00
|
|
|
// Check for DDOS Guard
|
2020-02-09 02:43:32 +00:00
|
|
|
if (response.Status == System.Net.HttpStatusCode.Forbidden)
|
2020-04-11 02:46:15 +00:00
|
|
|
{
|
|
|
|
await ConfigureDDoSGuardCookie();
|
2020-06-11 15:09:27 +00:00
|
|
|
response = await RequestWithCookiesAndRetryAsync(searchUri.AbsoluteUri);
|
2020-04-11 02:46:15 +00:00
|
|
|
}
|
2020-02-09 02:43:32 +00:00
|
|
|
|
|
|
|
if (response.Status != System.Net.HttpStatusCode.OK)
|
2020-04-11 02:46:15 +00:00
|
|
|
throw new WebException($"Anidex search returned unexpected result. Expected 200 OK but got {response.Status}.", WebExceptionStatus.ProtocolError);
|
2020-02-09 02:43:32 +00:00
|
|
|
|
|
|
|
// Search seems to have been a success so parse it
|
2020-06-09 17:36:57 +00:00
|
|
|
return ParseResult(response.ContentString);
|
2020-02-09 02:43:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private IEnumerable<ReleaseInfo> ParseResult(string response)
|
|
|
|
{
|
|
|
|
const string rowSelector = "div#content table > tbody > tr";
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var resultParser = new HtmlParser();
|
|
|
|
var resultDocument = resultParser.ParseDocument(response);
|
|
|
|
IEnumerable<IElement> rows = resultDocument.QuerySelectorAll(rowSelector);
|
|
|
|
|
|
|
|
var releases = new List<ReleaseInfo>();
|
|
|
|
foreach (var r in rows)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var release = new ReleaseInfo();
|
|
|
|
|
|
|
|
release.Category = ParseValueFromRow(r, nameof(release.Category), "td:nth-child(1) a", (e) => MapTrackerCatToNewznab(e.Attributes["href"].Value.Substring(5)));
|
|
|
|
release.Title = ParseStringValueFromRow(r, nameof(release.Title), "td:nth-child(3) span");
|
|
|
|
release.Link = ParseValueFromRow(r, nameof(release.Link), "a[href^=\"/dl/\"]", (e) => GetAbsoluteUrl(e.Attributes["href"].Value));
|
|
|
|
release.MagnetUri = ParseValueFromRow(r, nameof(release.MagnetUri), "a[href^=\"magnet:?\"]", (e) => new Uri(e.Attributes["href"].Value));
|
|
|
|
release.Size = ParseValueFromRow(r, nameof(release.Size), "td:nth-child(7)", (e) => ReleaseInfo.GetBytes(e.Text()));
|
|
|
|
release.PublishDate = ParseValueFromRow(r, nameof(release.PublishDate), "td:nth-child(8)", (e) => DateTime.ParseExact(e.Attributes["title"].Value, "yyyy-MM-dd HH:mm:ss UTC", CultureInfo.InvariantCulture));
|
|
|
|
release.Seeders = ParseIntValueFromRow(r, nameof(release.Seeders), "td:nth-child(9)");
|
|
|
|
release.Peers = ParseIntValueFromRow(r, nameof(release.Peers), "td:nth-child(10)") + release.Seeders;
|
|
|
|
release.Grabs = ParseIntValueFromRow(r, nameof(release.Grabs), "td:nth-child(11)");
|
|
|
|
release.Comments = ParseValueFromRow(r, nameof(release.Comments), "td:nth-child(3) a", (e) => GetAbsoluteUrl(e.Attributes["href"].Value));
|
|
|
|
release.Guid = release.Comments;
|
|
|
|
release.MinimumRatio = 1;
|
|
|
|
release.MinimumSeedTime = 172800; // 48 hours
|
|
|
|
release.DownloadVolumeFactor = 0;
|
|
|
|
release.UploadVolumeFactor = 1;
|
|
|
|
|
|
|
|
releases.Add(release);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
logger.Error($"Anidex: Error parsing search result row '{r.ToHtmlPretty()}':\n\n{ex}");
|
|
|
|
}
|
|
|
|
|
|
|
|
return releases;
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
throw new IOException($"Error parsing search result page: {ex}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async Task ConfigureDDoSGuardCookie()
|
|
|
|
{
|
2020-04-11 02:46:15 +00:00
|
|
|
const string ddosPostUrl = "https://check.ddos-guard.net/check.js";
|
2020-06-11 15:09:27 +00:00
|
|
|
var response = await WebRequestWithCookiesAsync(ddosPostUrl, string.Empty);
|
2020-04-11 02:46:15 +00:00
|
|
|
if (response.Status != System.Net.HttpStatusCode.OK)
|
|
|
|
throw new WebException($"Unexpected DDOS Guard response: Status: {response.Status}", WebExceptionStatus.ProtocolError);
|
|
|
|
if (response.IsRedirect)
|
|
|
|
throw new WebException($"Unexpected DDOS Guard response: Redirect: {response.RedirectingTo}", WebExceptionStatus.UnknownError);
|
|
|
|
if (string.IsNullOrWhiteSpace(response.Cookies))
|
|
|
|
throw new WebException("Unexpected DDOS Guard response: Empty cookie", WebExceptionStatus.ReceiveFailure);
|
2020-02-09 02:43:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private static TResult ParseValueFromRow<TResult>(IElement row, string propertyName, string selector,
|
|
|
|
Func<IElement, TResult> parseFunction)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var selectedElement = row.QuerySelector(selector);
|
|
|
|
if (selectedElement == null)
|
|
|
|
throw new IOException($"Unable to find '{selector}'.");
|
|
|
|
|
|
|
|
return parseFunction(selectedElement);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
throw new IOException($"Error parsing for property '{propertyName}': {ex.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static string ParseStringValueFromRow(IElement row, string propertyName, string selector)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var selectedElement = row.QuerySelector(selector);
|
|
|
|
if (selectedElement == null)
|
|
|
|
throw new IOException($"Unable to find '{selector}'.");
|
|
|
|
|
|
|
|
return selectedElement.Text();
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
throw new IOException($"Error parsing for property '{propertyName}': {ex.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int ParseIntValueFromRow(IElement row, string propertyName, string selector)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var text = ParseStringValueFromRow(row, propertyName, selector);
|
|
|
|
if (!int.TryParse(text, out var value))
|
|
|
|
throw new IOException($"Could not convert '{text}' to int.");
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
throw new IOException($"Error parsing for property '{propertyName}': {ex.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|