From a8933a38441975ff8f6b0a01f6d4f7f1d7eaf610 Mon Sep 17 00:00:00 2001 From: Dmitry Chepurovskiy Date: Sun, 18 Oct 2020 23:34:13 +0300 Subject: [PATCH] Added AniMedia public indexer (#9879) --- README.md | 1 + src/Jackett.Common/Indexers/Animedia.cs | 200 ++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/Jackett.Common/Indexers/Animedia.cs diff --git a/README.md b/README.md index 3d81a05cd..d2268605f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/ * ACGsou (36DM) * Anidex * AniLibria + * Animedia * Anime Tosho * AniRena * AniSource diff --git a/src/Jackett.Common/Indexers/Animedia.cs b/src/Jackett.Common/Indexers/Animedia.cs new file mode 100644 index 000000000..bb3d35761 --- /dev/null +++ b/src/Jackett.Common/Indexers/Animedia.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using Newtonsoft.Json.Linq; +using NLog; +using System.Linq; + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + internal class Animedia : BaseWebIndexer + { + private static readonly Regex EpisodesInfoQueryRegex = new Regex(@"серии (\d+)-(\d+) из.*", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ResolutionInfoQueryRegex = new Regex(@"Качество (\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SizeInfoQueryRegex = new Regex(@"Размер:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ReleaseDateInfoQueryRegex = new Regex(@"Добавлен:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public Animedia(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) + : base(id: "Animedia", + name: "Animedia", + description: "Animedia is a public russian tracker and release group for anime.", + link: "https://tt.animedia.tv/", + caps: new TorznabCapabilities(), + configService: configService, + client: wc, + logger: l, + p: ps, + configData: new ConfigurationData()) + { + Encoding = Encoding.UTF8; + Language = "ru-ru"; + Type = "public"; + + // Configure the category mappings + AddCategoryMapping(1, TorznabCatType.TVAnime, "Anime"); + } + + /// + /// https://tt.animedia.tv/ajax/search_result/P0 + /// + private string SearchUrl => SiteLink + "ajax/search_result/P0"; + + public override async Task 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; + } + + // If the search string is empty use the latest releases + protected override async Task> PerformQuery(TorznabQuery query) { + WebResult result; + if (query.IsTest || string.IsNullOrWhiteSpace(query.SearchTerm)) { + result = await RequestWithCookiesAndRetryAsync(SiteLink); + } else { + // Prepare the search query + var queryParameters = new NameValueCollection + { + { "keywords", query.SearchTerm }, + { "limit", "20"}, + { "orderby_sort", "entry_date|desc"} + }; + result = await RequestWithCookiesAndRetryAsync(SearchUrl + "?" + queryParameters.GetQueryString()); + } + + const string ReleaseLinksSelector = "a.ads-list__item__title"; + + var releases = new List(); + + try + { + var parser = new HtmlParser(); + var document = await parser.ParseDocumentAsync(result.ContentString); + + foreach (var linkNode in document.QuerySelectorAll(ReleaseLinksSelector)) + { + var url = linkNode.GetAttribute("href"); + releases.AddRange(await FetchShowReleases(url)); + } + } + catch (Exception ex) + { + OnParseError(result.ContentString, ex); + } + + return releases; + } + + private async Task> FetchShowReleases(string url) + { + var releases = new List(); + var uri = new Uri(url); + //Some URLs in search are broken + if (url.StartsWith("//")) + { + url = "https:" + url; + } + + var result = await RequestWithCookiesAndRetryAsync(url); + + try + { + var parser = new HtmlParser(); + var document = await parser.ParseDocumentAsync(result.ContentString); + + var baseRelease = new ReleaseInfo + { + Title = composeBaseTitle(document), + BannerUrl = new Uri(document.QuerySelector("div.widget__post-info__poster > a").Attributes["href"].Value), + Comments = uri, + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1, + Category = new int[]{ TorznabCatType.TVAnime.ID }, + }; + foreach (var t in document.QuerySelectorAll("ul.media__tabs__nav > li > a")) + { + var release = (ReleaseInfo)baseRelease.Clone(); + var tr_id = t.Attributes["href"].Value; + var tr = document.QuerySelector("div" + tr_id); + release.Title += " - " + composeTitleAdditionalInfo(t, tr); + release.Link = new Uri(document.QuerySelector("div.download_tracker > a.btn__green").Attributes["href"].Value); + release.MagnetUri = new Uri(document.QuerySelector("div.download_tracker > a.btn__d-gray").Attributes["href"].Value); + release.Seeders = long.Parse(document.QuerySelector("div.circle_green_text_top").Text()); + release.Peers = release.Seeders + long.Parse(document.QuerySelector("div.circle_red_text_top").Text()); + release.Grabs = long.Parse(document.QuerySelector("div.circle_grey_text_top").Text()); + release.PublishDate = getReleaseDate(tr); + release.Size = getReleaseSize(tr); + release.Guid = new Uri(uri.ToString() + tr_id); + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(result.ContentString, ex); + } + + return releases; + } + + private string composeBaseTitle(IHtmlDocument r) { + var name_ru = r.QuerySelector("div.media__post__header > h1").Text().Trim(); + var name_en = r.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span").Text().Trim(); + var name_orig = r.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span").Text().Trim(); + + var title = name_ru + " / " + name_en; + if (name_en != name_orig) { + title += " / " + name_orig; + } + return title; + } + + private string composeTitleAdditionalInfo(IElement t, IElement tr) { + var tabName = t.Text(); + tabName = tabName.Replace("Сезон", "Season"); + if (tabName.Contains("Серии")) { + tabName = ""; + } + + var heading = tr.QuerySelector("h3.tracker_info_bold").Text(); + // Transform episodes info if header contains that + heading = EpisodesInfoQueryRegex.Replace( + heading, + match => match.Success ? $"E{int.Parse(match.Groups[1].Value)}-{int.Parse(match.Groups[2].Value)}" : heading + ); + + var resolution = tr.QuerySelector("div.tracker_info_left").Text(); + resolution = ResolutionInfoQueryRegex.Match(resolution).Groups[1].Value; + + return tabName + " " + heading + " [" + resolution + "p]"; + } + + private static long getReleaseSize(IElement tr) + { + var sizeStr = tr.QuerySelector("div.tracker_info_left").Text(); + return ReleaseInfo.GetBytes(SizeInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim()); + } + + private static DateTime getReleaseDate(IElement tr) + { + var sizeStr = tr.QuerySelector("div.tracker_info_left").Text(); + return DateTime.Parse(ReleaseDateInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim()); + } + } +}