using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; 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; using Jackett.Common.Utils.Clients; using Newtonsoft.Json.Linq; using NLog; namespace Jackett.Common.Indexers { [ExcludeFromCodeCoverage] internal class ShizaProject : BaseWebIndexer { public override string[] LegacySiteLinks { get; protected set; } = { "http://shiza-project.com/" // site is forcing https }; public ShizaProject(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps, ICacheService cs) : base(id: "ShizaProject", name: "ShizaProject", description: "ShizaProject Tracker is a semi-private russian tracker and release group for anime", link: "https://shiza-project.com/", caps: new TorznabCapabilities { TvSearchParams = new List { TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep } }, configService: configService, client: wc, logger: l, p: ps, cacheService: cs, configData: new ConfigurationDataBasicLoginWithEmail()) { Encoding = Encoding.UTF8; Language = "ru-ru"; Type = "semi-private"; AddCategoryMapping(1, TorznabCatType.TVAnime, "Anime"); } private ConfigurationDataBasicLoginWithEmail Configuration => (ConfigurationDataBasicLoginWithEmail)configData; /// /// http://shiza-project.com/accounts/login /// private string LoginUrl => SiteLink + "accounts/login"; /// /// http://shiza-project.com/releases/search /// private string SearchUrl => SiteLink + "releases/search"; public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); var data = new Dictionary { { "field-email", Configuration.Email.Value }, { "field-password", Configuration.Password.Value } }; var result = await RequestLoginAndFollowRedirect( LoginUrl, data, null, returnCookiesFromFirstCall: true ); var parser = new HtmlParser(); var document = await parser.ParseDocumentAsync(result.ContentString); await ConfigureIfOK(result.Cookies, IsAuthorized(result), () => { var errorMessage = document.QuerySelector("div.alert-error").Text().Trim(); throw new ExceptionWithConfigData(errorMessage, Configuration); }); return IndexerConfigurationStatus.Completed; } public override async Task Download(Uri link) { await EnsureAuthorized(); return await base.Download(link); } // If the search string is empty use the latest releases protected override async Task> PerformQuery(TorznabQuery query) { await EnsureAuthorized(); WebResult result; if (query.IsTest || string.IsNullOrWhiteSpace(query.SearchTerm)) { result = await RequestWithCookiesAndRetryAsync(SiteLink); } else { // Prepare the search query var queryParameters = new NameValueCollection { { "q", query.SearchTerm} }; result = await RequestWithCookiesAndRetryAsync(SearchUrl + "?" + queryParameters.GetQueryString()); } const string ReleaseLinksSelector = "article.grid-card > a.card-box"; 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 EnsureAuthorized() { var result = await RequestWithCookiesAndRetryAsync(SiteLink); if (!IsAuthorized(result)) { await ApplyConfiguration(null); } } private async Task> FetchShowReleases(string url) { var releases = new List(); var uri = new Uri(url); var result = await RequestWithCookiesAndRetryAsync(url); try { var parser = new HtmlParser(); var document = await parser.ParseDocumentAsync(result.ContentString); var r = document.QuerySelector("div.release > div.wrapper-release"); var baseRelease = new ReleaseInfo { Title = composeBaseTitle(r), Poster = new Uri(SiteLink + r.QuerySelector("a[data-fancybox]").Attributes["href"].Value), Details = uri, DownloadVolumeFactor = 0, UploadVolumeFactor = 1, Category = new[] { TorznabCatType.TVAnime.ID } }; foreach (var t in r.QuerySelectorAll("a[data-toggle]")) { var release = (ReleaseInfo)baseRelease.Clone(); release.Title += " " + t.Text().Trim(); var tr_id = t.Attributes["href"].Value; var tr = r.QuerySelector("div" + tr_id); release.Link = new Uri(tr.QuerySelector("a.button--success").Attributes["href"].Value); release.Seeders = long.Parse(tr.QuerySelector("div.torrent-counter > div:nth-of-type(1)").Text().Trim().Split(' ')[0]); release.Peers = release.Seeders + long.Parse(tr.QuerySelector("div.torrent-counter > div:nth-of-type(2)").Text().Trim().Split(' ')[0]); release.Grabs = long.Parse(tr.QuerySelector("div.torrent-counter > div:nth-of-type(3)").Text().Trim().Split(' ')[0]); release.PublishDate = DateTime.Parse(tr.QuerySelector("time.torrent-time").Text()); 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(IElement release) { var titleDiv = release.QuerySelector("section:nth-of-type(2) > div.card > article:nth-of-type(1) > div.card-header"); return titleDiv.QuerySelector("h3").Text() + " " + titleDiv.QuerySelector("p").Text(); } // Appending id to differentiate between different quality versions private bool IsAuthorized(WebResult result) { return result.ContentString.Contains("/logout"); } private static long getReleaseSize(IElement tr) { var size = tr.QuerySelector("a.torrent-size").Text().Trim(); size = size.Replace("КБ", "KB"); size = size.Replace("МБ", "MB"); size = size.Replace("ГБ", "GB"); size = size.Replace("ТБ", "TB"); return ReleaseInfo.GetBytes(size); } } }