diff --git a/README.md b/README.md index 3bd86e435..a50075d7b 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ A third-party Golang SDK for Jackett is available from [webtor-io/go-jackett](ht * Torrents-Local * TribalMixes * Union Fansub + * UniOtaku * vTorrent * xTorrenty * YggTorrent (YGG) diff --git a/src/Jackett.Common/Indexers/Uniotaku.cs b/src/Jackett.Common/Indexers/Uniotaku.cs new file mode 100644 index 000000000..06d02a087 --- /dev/null +++ b/src/Jackett.Common/Indexers/Uniotaku.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AngleSharp.Html.Parser; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig.Bespoke; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Newtonsoft.Json.Linq; +using NLog; + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + public class Uniotaku : BaseWebIndexer + { + public Uniotaku(IIndexerConfigurationService configService, Utils.Clients.WebClient wc, Logger l, IProtectionService ps, ICacheService cs) + : base(id: "uniotaku", + name: "UniOtaku", + description: "UniOtaku is a BRAZILIAN Semi-Private Torrent Tracker for ANIME", + link: "https://tracker.uniotaku.com/", + caps: new TorznabCapabilities + { + TvSearchParams = new List + { + TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep + }, + MovieSearchParams = new List + { + MovieSearchParam.Q + }, + MusicSearchParams = new List + { + MusicSearchParam.Q + }, + BookSearchParams = new List + { + BookSearchParam.Q + } + }, + configService: configService, + client: wc, + logger: l, + p: ps, + cacheService: cs, + configData: new ConfigurationDataUniotaku()) + { + Encoding = Encoding.UTF8; + Language = "pt-BR"; + Type = "semi-private"; + + AddCategoryMapping(28, TorznabCatType.TVAnime, "Anime"); + AddCategoryMapping(47, TorznabCatType.MoviesOther, "Filme"); + AddCategoryMapping(48, TorznabCatType.TVAnime, "OVA"); + AddCategoryMapping(49, TorznabCatType.BooksComics, "Mangá"); + AddCategoryMapping(50, TorznabCatType.TVOther, "Dorama"); + AddCategoryMapping(51, TorznabCatType.Audio, "OST"); + AddCategoryMapping(52, TorznabCatType.TVAnime, "Anime Completo"); + AddCategoryMapping(53, TorznabCatType.BooksComics, "Mangá Completo"); + AddCategoryMapping(54, TorznabCatType.TVOther, "Dorama Completo"); + AddCategoryMapping(55, TorznabCatType.XXX, "Hentai"); + AddCategoryMapping(56, TorznabCatType.XXXOther, "H Doujinshi"); + AddCategoryMapping(57, TorznabCatType.TVOther, "Tokusatsu"); + } + + private new ConfigurationDataUniotaku configData => (ConfigurationDataUniotaku)base.configData; + + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + + var loginUrl = SiteLink + "account-login.php"; + + var postData = new Dictionary + { + { "username", configData.Username.Value }, + { "password", configData.Password.Value }, + { "manter", "1" } + }; + + var response = await RequestLoginAndFollowRedirect(loginUrl, postData, CookieHeader, true, null, SiteLink); + + await ConfigureIfOK(response.Cookies, response.Cookies != null && response.Cookies.Contains("uid=") && response.Cookies.Contains("pass="), () => + { + var parser = new HtmlParser(); + var dom = parser.ParseDocument(response.ContentString); + var errorMessage = dom.QuerySelector(".login-content span.text-red")?.TextContent.Trim(); + + throw new ExceptionWithConfigData(errorMessage ?? "Unknown error message, please report.", configData); + }); + + return IndexerConfigurationStatus.RequiresTesting; + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + var searchString = query.GetQueryString(); + + if (!string.IsNullOrWhiteSpace(searchString)) + searchString = "%" + Regex.Replace(searchString, @"[ -._]+", "%").Trim() + "%"; + + var categoryMapping = MapTorznabCapsToTrackers(query); + + var parameters = new NameValueCollection + { + { "categoria", categoryMapping.FirstIfSingleOrDefault("0") }, + { "grupo", "0" }, + { "status", configData.Freeleech.Value ? "1" : "0" }, + { "ordenar", configData.SortBy.Value }, + { "start", "0" }, + { "length", "100" }, + { "search[value]", searchString ?? string.Empty }, + { "search[regex]", "false" }, + }; + + var searchUrl = $"{SiteLink}torrents_.php?{parameters.GetQueryString()}"; + var response = await RequestWithCookiesAsync(searchUrl); + + var releases = new List(); + var parser = new HtmlParser(); + + try + { + var jsonContent = JObject.Parse(response.ContentString); + + var publishDate = DateTime.Now; + foreach (var item in jsonContent.Value("data")) + { + var detailsDom = parser.ParseDocument(item.SelectToken("[0]").Value()); + var categoryDom = parser.ParseDocument(item.SelectToken("[1]").Value()); + var groupDom = parser.ParseDocument(item.SelectToken("[7]").Value()); + + var qTitleLink = detailsDom.QuerySelector("a[href^=\"torrents-details.php?id=\"]"); + var title = qTitleLink?.TextContent.Trim(); + var details = new Uri(SiteLink + qTitleLink?.GetAttribute("href")); + + var category = categoryDom.QuerySelector("img[alt]")?.GetAttribute("alt")?.Trim() ?? "Anime"; + + var releaseGroup = groupDom.QuerySelector("a[href*=\"teams-view.php?id=\"]")?.TextContent.Trim(); + if (!string.IsNullOrWhiteSpace(releaseGroup)) + title += $" [{releaseGroup}]"; + + var seeders = item.SelectToken("[3]")?.Value(); + var leechers = item.SelectToken("[4]")?.Value(); + + publishDate = publishDate.AddMinutes(-1); + + var release = new ReleaseInfo + { + Guid = details, + Details = details, + Link = details, + Title = title, + Category = MapTrackerCatDescToNewznab(category), + Size = ReleaseInfo.GetBytes(item.SelectToken("[6]")?.Value()), + Grabs = item.SelectToken("[5]")?.Value(), + Seeders = seeders, + Peers = seeders + leechers, + PublishDate = publishDate, + DownloadVolumeFactor = + detailsDom.QuerySelector("img[src*=\"images/free.gif\"]") != null ? 0 : + detailsDom.QuerySelector("img[src*=\"images/silverdownload.gif\"]") != null ? 0.5 : 1, + UploadVolumeFactor = 1, + MinimumRatio = 0.7 + }; + + releases.Add(release); + } + } + catch (Exception ex) + { + OnParseError(response.ContentString, ex); + } + + return releases; + } + + public override async Task Download(Uri link) + { + var response = await RequestWithCookiesAsync(link.ToString()); + + var parser = new HtmlParser(); + var dom = parser.ParseDocument(response.ContentString); + var downloadLink = dom.QuerySelector("a[href^=\"download.php?id=\"]")?.GetAttribute("href")?.Trim(); + + if (downloadLink == null) + throw new Exception($"Failed to fetch download link from {link}"); + + return await base.Download(new Uri(SiteLink + downloadLink)); + } + } +} diff --git a/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataUniotaku.cs b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataUniotaku.cs new file mode 100644 index 000000000..319ab3668 --- /dev/null +++ b/src/Jackett.Common/Models/IndexerConfig/Bespoke/ConfigurationDataUniotaku.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Jackett.Common.Models.IndexerConfig.Bespoke +{ + [ExcludeFromCodeCoverage] + internal class ConfigurationDataUniotaku : ConfigurationDataBasicLogin + { + public BoolConfigurationItem Freeleech { get; private set; } + public SingleSelectConfigurationItem SortBy { get; private set; } + + public ConfigurationDataUniotaku() + { + Freeleech = new BoolConfigurationItem("Search freeleech only") { Value = false }; + + SortBy = new SingleSelectConfigurationItem("Sort By", new Dictionary + { + {"0", "created"}, + {"3", "seeders"}, + {"9", "size"}, + {"1", "title"} + }) + { Value = "0" }; + } + + } +}