using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net; using System.Threading.Tasks; using Jackett.Common.Models; using Jackett.Common.Models.IndexerConfig.Bespoke; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils; using Jackett.Common.Utils.Clients; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; using WebClient = Jackett.Common.Utils.Clients.WebClient; namespace Jackett.Common.Indexers.Abstract { [ExcludeFromCodeCoverage] public abstract class SpeedAppTracker : IndexerBase { public override bool SupportsPagination => true; protected virtual bool UseP2PReleaseName => false; protected virtual int minimumSeedTime => 172800; // 48h private readonly Dictionary _apiHeaders = new Dictionary { {"Accept", "application/json"}, {"Content-Type", "application/json"} }; // API DOC: https://speedapp.io/api/doc private string LoginUrl => SiteLink + "api/login"; private string SearchUrl => SiteLink + "api/torrent"; private string _token; private new ConfigurationDataSpeedAppTracker configData => (ConfigurationDataSpeedAppTracker)base.configData; protected SpeedAppTracker(IIndexerConfigurationService configService, WebClient client, Logger logger, IProtectionService p, ICacheService cs) : base(configService: configService, client: client, logger: logger, p: p, cacheService: cs, configData: new ConfigurationDataSpeedAppTracker()) { } public override async Task ApplyConfiguration(JToken configJson) { LoadValuesFromJson(configJson); await RenewalTokenAsync(); var releases = await PerformQuery(new TorznabQuery()); await ConfigureIfOK(string.Empty, releases.Any(), () => throw new Exception("Could not find releases.")); return IndexerConfigurationStatus.Completed; } private async Task RenewalTokenAsync() { if (configData.Email.Value == null || configData.Password.Value == null) throw new Exception("Please, check the indexer configuration."); var body = new Dictionary { { "username", configData.Email.Value.Trim() }, { "password", configData.Password.Value.Trim() } }; var jsonData = JsonConvert.SerializeObject(body); var result = await RequestWithCookiesAsync( LoginUrl, method: RequestType.POST, headers: _apiHeaders, rawbody: jsonData); var json = JObject.Parse(result.ContentString); _token = json.Value("token"); if (_token == null) throw new Exception(json.Value("message")); } protected override async Task> PerformQuery(TorznabQuery query) { var releases = new List(); //var categoryMapping = MapTorznabCapsToTrackers(query).Distinct().ToList(); var qc = new List> // NameValueCollection don't support cat[]=19&cat[]=6 { { "itemsPerPage", "100" }, { "sort", "torrent.createdAt" }, { "direction", "desc" } }; if (query.Limit > 0 && query.Offset > 0) { var page = query.Offset / query.Limit + 1; qc.Add("page", page.ToString()); } foreach (var cat in MapTorznabCapsToTrackers(query)) qc.Add("categories[]", cat); if (query.IsImdbQuery) qc.Add("imdbId", query.ImdbID); else qc.Add("search", query.GetQueryString()); if (string.IsNullOrWhiteSpace(_token)) // fist time login await RenewalTokenAsync(); var searchUrl = SearchUrl + "?" + qc.GetQueryString(); var response = await RequestWithCookiesAsync(searchUrl, headers: GetSearchHeaders()); if (response.Status == HttpStatusCode.Unauthorized) { await RenewalTokenAsync(); // re-login response = await RequestWithCookiesAsync(searchUrl, headers: GetSearchHeaders()); } else if (response.Status != HttpStatusCode.OK) throw new Exception($"Unknown error in search: {response.ContentString}"); try { var rows = JArray.Parse(response.ContentString); foreach (var row in rows) { var dlVolumeFactor = row.Value("download_volume_factor"); // skip non-freeleech results when freeleech only is set if (configData.FreeleechOnly.Value && dlVolumeFactor != 0) { continue; } var id = row.Value("id"); var link = new Uri($"{SiteLink}api/torrent/{id}/download"); var urlStr = row.Value("url"); var details = new Uri(urlStr); var publishDate = DateTime.Parse(row.Value("created_at"), CultureInfo.InvariantCulture); var cat = row.Value("category").Value("id"); var description = ""; var genres = row.Value("short_description"); char[] delimiters = { ',', ' ', '/', ')', '(', '.', ';', '[', ']', '"', '|', ':' }; var genresSplit = genres.Split(delimiters, System.StringSplitOptions.RemoveEmptyEntries); var genresList = genresSplit.ToList(); genres = string.Join(", ", genresList); if (!string.IsNullOrEmpty(genres)) description = genres; var posterStr = row.Value("poster"); var poster = Uri.TryCreate(posterStr, UriKind.Absolute, out var posterUri) ? posterUri : null; var title = row.Value("name"); // fix for #10883 if (UseP2PReleaseName && !string.IsNullOrWhiteSpace(row.Value("p2p_release_name"))) title = row.Value("p2p_release_name"); if (!query.IsImdbQuery && !query.MatchQueryStringAND(title)) continue; var release = new ReleaseInfo { Title = title, Link = link, Details = details, Guid = details, Category = MapTrackerCatToNewznab(cat), PublishDate = publishDate, Description = description, Poster = poster, Size = row.Value("size"), Grabs = row.Value("times_completed"), Seeders = row.Value("seeders"), Peers = row.Value("leechers") + row.Value("seeders"), DownloadVolumeFactor = dlVolumeFactor, UploadVolumeFactor = row.Value("upload_volume_factor"), MinimumRatio = 1, MinimumSeedTime = minimumSeedTime }; if (release.Genres == null) release.Genres = new List(); release.Genres = release.Genres.Union(genres.Split(',')).ToList(); 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(), headers: GetSearchHeaders()); if (response.Status == HttpStatusCode.Unauthorized) { await RenewalTokenAsync(); response = await RequestWithCookiesAsync(link.ToString(), headers: GetSearchHeaders()); } else if (response.Status != HttpStatusCode.OK) throw new Exception($"Unknown error in download: {response.ContentBytes}"); return response.ContentBytes; } private Dictionary GetSearchHeaders() => new Dictionary { {"Authorization", $"Bearer {_token}"} }; } }