From f9c9d4a0e0ddf5fb677d30df6e88df4d323e51ed Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 2 Jan 2024 22:04:16 +0200 Subject: [PATCH] Fixed: (GGn) Improve title, timezone, MST, min/max size filters Hiding torrents of type link And some minor refactoring around the passkey fetching and parsing. --- .../GazelleGamesTests/GazelleGamesFixture.cs | 15 +- .../Indexers/Definitions/GazelleGames.cs | 302 ++++++++++++------ 2 files changed, 205 insertions(+), 112 deletions(-) diff --git a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs index 5ff5b5ee9..8db0e9ab8 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/GazelleGamesTests/GazelleGamesFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; @@ -21,10 +22,10 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests [SetUp] public void Setup() { - Subject.Definition = new IndexerDefinition() + Subject.Definition = new IndexerDefinition { Name = "GazelleGames", - Settings = new GazelleGamesSettings() { Apikey = "somekey" } + Settings = new GazelleGamesSettings { Apikey = "somekey" } }; } @@ -37,20 +38,20 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); - var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; - releases.Should().HaveCount(1464); + releases.Should().HaveCount(1463); releases.First().Should().BeOfType(); var torrentInfo = releases.First() as TorrentInfo; - torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM"); + torrentInfo.Title.Should().Be("Microsoft_Flight_Simulator-HOODLUM (2020) [Windows / Multi-Language / Full ISO]"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); torrentInfo.DownloadUrl.Should().Be("https://gazellegames.net/torrents.php?action=download&id=303216&authkey=prowlarr&torrent_pass="); torrentInfo.InfoUrl.Should().Be("https://gazellegames.net/torrents.php?id=84781&torrentid=303216"); torrentInfo.CommentUrl.Should().BeNullOrEmpty(); torrentInfo.Indexer.Should().Be(Subject.Definition.Name); - torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 6:39:11").ToUniversalTime()); + torrentInfo.PublishDate.Should().Be(DateTime.Parse("2022-07-25 06:39:11", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); torrentInfo.Size.Should().Be(80077617780); torrentInfo.InfoHash.Should().Be(null); torrentInfo.MagnetUrl.Should().Be(null); @@ -74,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.GazelleGamesTests .Setup(o => o.ExecuteProxiedAsync(It.Is(v => v.Method == HttpMethod.Get), Subject.Definition)) .Returns((r, d) => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), recentFeed))); - var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new int[] { 2000 } })).Releases; + var releases = (await Subject.Fetch(new BasicSearchCriteria { Categories = new[] { 2000 } })).Releases; releases.Should().HaveCount(0); } diff --git a/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs b/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs index 4abc47005..115d871e6 100644 --- a/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs +++ b/src/NzbDrone.Core/Indexers/Definitions/GazelleGames.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using FluentValidation; using FluentValidation.Results; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; @@ -16,6 +19,7 @@ using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Indexers.Settings; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -24,7 +28,7 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGames : TorrentIndexerBase { public override string Name => "GazelleGames"; - public override string[] IndexerUrls => new string[] { "https://gazellegames.net/" }; + public override string[] IndexerUrls => new[] { "https://gazellegames.net/" }; public override string Description => "GazelleGames (GGn) is a Private Torrent Tracker for GAMES"; public override string Language => "en-US"; public override Encoding Encoding => Encoding.UTF8; @@ -38,7 +42,7 @@ namespace NzbDrone.Core.Indexers.Definitions public override IIndexerRequestGenerator GetRequestGenerator() { - return new GazelleGamesRequestGenerator() { Settings = Settings, Capabilities = Capabilities, HttpClient = _httpClient }; + return new GazelleGamesRequestGenerator(Settings, Capabilities); } public override IParseIndexerResponse GetParser() @@ -48,14 +52,13 @@ namespace NzbDrone.Core.Indexers.Definitions private IndexerCapabilities SetCapabilities() { - var caps = new IndexerCapabilities - { - }; + var caps = new IndexerCapabilities(); // Apple caps.Categories.AddCategoryMapping("Mac", NewznabStandardCategory.ConsoleOther, "Mac"); caps.Categories.AddCategoryMapping("iOS", NewznabStandardCategory.PCMobileiOS, "iOS"); caps.Categories.AddCategoryMapping("Apple Bandai Pippin", NewznabStandardCategory.ConsoleOther, "Apple Bandai Pippin"); + caps.Categories.AddCategoryMapping("Apple II", NewznabStandardCategory.ConsoleOther, "Apple II"); // Google caps.Categories.AddCategoryMapping("Android", NewznabStandardCategory.PCMobileAndroid, "Android"); @@ -78,6 +81,7 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping("Nintendo GameCube", NewznabStandardCategory.ConsoleOther, "Nintendo GameCube"); caps.Categories.AddCategoryMapping("Pokemon Mini", NewznabStandardCategory.ConsoleOther, "Pokemon Mini"); caps.Categories.AddCategoryMapping("SNES", NewznabStandardCategory.ConsoleOther, "SNES"); + caps.Categories.AddCategoryMapping("Switch", NewznabStandardCategory.ConsoleOther, "Switch"); caps.Categories.AddCategoryMapping("Virtual Boy", NewznabStandardCategory.ConsoleOther, "Virtual Boy"); caps.Categories.AddCategoryMapping("Wii", NewznabStandardCategory.ConsoleWii, "Wii"); caps.Categories.AddCategoryMapping("Wii U", NewznabStandardCategory.ConsoleWiiU, "Wii U"); @@ -178,31 +182,62 @@ namespace NzbDrone.Core.Indexers.Definitions caps.Categories.AddCategoryMapping("Retro - Other", NewznabStandardCategory.ConsoleOther, "Retro - Other"); // special categories (real categories/not platforms) - caps.Categories.AddCategoryMapping("OST", NewznabStandardCategory.AudioOther, "OST"); - caps.Categories.AddCategoryMapping("Applications", NewznabStandardCategory.PC0day, "Applications"); - caps.Categories.AddCategoryMapping("E-Books", NewznabStandardCategory.BooksEBook, "E-Books"); + caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.PCGames, "Games"); + caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.PC0day, "Applications"); + caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.BooksEBook, "E-Books"); + caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.AudioOther, "OST"); return caps; } protected override async Task Test(List failures) { - ((GazelleGamesRequestGenerator)GetRequestGenerator()).FetchPasskey(); - await base.Test(failures); + await FetchPasskey().ConfigureAwait(false); + + await base.Test(failures).ConfigureAwait(false); + } + + private async Task FetchPasskey() + { + var request = new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}/api.php") + .Accept(HttpAccept.Json) + .SetHeader("X-API-Key", Settings.Apikey) + .AddQueryParam("request", "quick_user") + .Build(); + + var indexResponse = await _httpClient.ExecuteAsync(request).ConfigureAwait(false); + + var index = Json.Deserialize(indexResponse.Content); + + if (index == null || + string.IsNullOrWhiteSpace(index.Status) || + index.Status != "success" || + string.IsNullOrWhiteSpace(index.Response.PassKey)) + { + throw new IndexerAuthException("Failed to authenticate with GazelleGames."); + } + + // Set passkey on settings so it can be used to generate the download URL + Settings.Passkey = index.Response.PassKey; } } public class GazelleGamesRequestGenerator : IIndexerRequestGenerator { - public GazelleGamesSettings Settings { get; set; } - public IndexerCapabilities Capabilities { get; set; } - public IIndexerHttpClient HttpClient { get; set; } + private readonly GazelleGamesSettings _settings; + private readonly IndexerCapabilities _capabilities; + + public GazelleGamesRequestGenerator(GazelleGamesSettings settings, IndexerCapabilities capabilities) + { + _settings = settings; + _capabilities = capabilities; + } public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria) { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -211,7 +246,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -220,7 +255,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -229,7 +264,7 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } @@ -238,61 +273,72 @@ namespace NzbDrone.Core.Indexers.Definitions { var pageableRequests = new IndexerPageableRequestChain(); - pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria.Categories))); + pageableRequests.Add(GetRequest(GetBasicSearchParameters(searchCriteria.SanitizedSearchTerm, searchCriteria))); return pageableRequests; } - public void FetchPasskey() + private IEnumerable GetRequest(List> parameters) { - // GET on index for the passkey - var request = RequestBuilder().Resource("api.php?request=quick_user").Build(); - var indexResponse = HttpClient.Execute(request); - var index = Json.Deserialize(indexResponse.Content); - if (index == null || - string.IsNullOrWhiteSpace(index.Status) || - index.Status != "success" || - string.IsNullOrWhiteSpace(index.Response.PassKey)) - { - throw new Exception("Failed to authenticate with GazelleGames."); - } - - // Set passkey on settings so it can be used to generate the download URL - Settings.Passkey = index.Response.PassKey; - } - - private IEnumerable GetRequest(string parameters) - { - var req = RequestBuilder() - .Resource($"api.php?{parameters}") + var request = RequestBuilder() + .Resource($"/api.php?{parameters.GetQueryString()}") .Build(); - yield return new IndexerRequest(req); + yield return new IndexerRequest(request); } private HttpRequestBuilder RequestBuilder() { - return new HttpRequestBuilder($"{Settings.BaseUrl.Trim().TrimEnd('/')}") + return new HttpRequestBuilder($"{_settings.BaseUrl.Trim().TrimEnd('/')}") + .Resource("/api.php") .Accept(HttpAccept.Json) - .SetHeader("X-API-Key", Settings.Apikey); + .SetHeader("X-API-Key", _settings.Apikey); } - private string GetBasicSearchParameters(string searchTerm, int[] categories) + private List> GetBasicSearchParameters(string searchTerm, SearchCriteriaBase searchCriteria) { - var parameters = "request=search&search_type=torrents&empty_groups=filled&order_by=time&order_way=desc"; - - if (!string.IsNullOrWhiteSpace(searchTerm)) + var parameters = new List> { - var searchType = Settings.SearchGroupNames ? "groupname" : "searchstr"; + { "request", "search" }, + { "search_type", "torrents" }, + { "empty_groups", "filled" }, + { "order_by", "time" }, + { "order_way", "desc" } + }; - parameters += string.Format("&{1}={0}", searchTerm.Replace(".", " "), searchType); + if (searchTerm.IsNotNullOrWhiteSpace()) + { + parameters.Add( + _settings.SearchGroupNames ? "groupname" : "searchstr", + searchTerm.Replace(".", " ")); } - if (categories != null) + if (searchCriteria.Categories != null) { - foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories)) + var categoryMappings = _capabilities.Categories + .MapTorznabCapsToTrackers(searchCriteria.Categories) + .Distinct() + .Where(x => !x.IsAllDigits()) + .ToList(); + + categoryMappings.ForEach(category => parameters.Add("artistcheck[]", category)); + } + + if (searchCriteria.MinSize is > 0) + { + var minSize = searchCriteria.MinSize.Value / 1024L / 1024L; + if (minSize > 0) { - parameters += string.Format("&artistcheck[]={0}", cat); + parameters.Add("sizesmall", minSize.ToString()); + } + } + + if (searchCriteria.MaxSize is > 0) + { + var maxSize = searchCriteria.MaxSize.Value / 1024L / 1024L; + if (maxSize > 0) + { + parameters.Add("sizeslarge", maxSize.ToString()); } } @@ -329,66 +375,63 @@ namespace NzbDrone.Core.Indexers.Definitions } var jsonResponse = new HttpResponse(indexerResponse.HttpResponse); + if (jsonResponse.Resource.Status != "success" || string.IsNullOrWhiteSpace(jsonResponse.Resource.Status) || - jsonResponse.Resource.Response == null) + jsonResponse.Resource.Response is not JObject response) { return torrentInfos; } - Dictionary response; + var groups = response.ToObject>(JsonSerializer.Create(Json.GetSerializerSettings())); - try + foreach (var group in groups) { - response = ((JObject)jsonResponse.Resource.Response).ToObject>(); - } - catch - { - return torrentInfos; - } - - foreach (var result in response) - { - Dictionary torrents; - - try - { - torrents = ((JObject)result.Value.Torrents).ToObject>(); - } - catch + if (group.Value.Torrents is not JObject groupTorrents) { continue; } - if (result.Value.Torrents != null) + var torrents = groupTorrents + .ToObject>(JsonSerializer.Create(Json.GetSerializerSettings())) + .Where(t => t.Value.TorrentType != "Link") + .ToList(); + + var categories = group.Value.Artists + .SelectMany(a => _categories.MapTrackerCatDescToNewznab(a.Name)) + .Distinct() + .ToArray(); + + foreach (var torrent in torrents) { - var categories = result.Value.Artists.Select(a => a.Name); + var torrentId = torrent.Key; + var infoUrl = GetInfoUrl(group.Key, torrentId); - foreach (var torrent in torrents) + if (categories.Length == 0) { - var id = int.Parse(torrent.Key); - - var infoUrl = GetInfoUrl(result.Key, id); - - var release = new TorrentInfo() - { - Guid = infoUrl, - Title = torrent.Value.ReleaseTitle, - Files = torrent.Value.FileCount, - Grabs = torrent.Value.Snatched, - Size = long.Parse(torrent.Value.Size), - DownloadUrl = GetDownloadUrl(id), - InfoUrl = infoUrl, - Seeders = torrent.Value.Seeders, - Categories = _categories.MapTrackerCatDescToNewznab(categories.FirstOrDefault()), - Peers = torrent.Value.Leechers + torrent.Value.Seeders, - PublishDate = torrent.Value.Time.ToUniversalTime(), - DownloadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.FreeLeech || torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1, - UploadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1 - }; - - torrentInfos.Add(release); + categories = _categories.MapTrackerCatToNewznab(torrent.Value.CategoryId.ToString()).ToArray(); } + + var release = new TorrentInfo + { + Guid = infoUrl, + InfoUrl = infoUrl, + DownloadUrl = GetDownloadUrl(torrentId), + Title = GetTitle(group.Value, torrent.Value), + Categories = categories, + Files = torrent.Value.FileCount, + Size = long.Parse(torrent.Value.Size), + Grabs = torrent.Value.Snatched, + Seeders = torrent.Value.Seeders, + Peers = torrent.Value.Leechers + torrent.Value.Seeders, + PublishDate = torrent.Value.Time.ToUniversalTime(), + Scene = torrent.Value.Scene == 1, + DownloadVolumeFactor = torrent.Value.FreeTorrent is GazelleGamesFreeTorrent.FreeLeech or GazelleGamesFreeTorrent.Neutral || torrent.Value.LowSeedFL ? 0 : 1, + UploadVolumeFactor = torrent.Value.FreeTorrent == GazelleGamesFreeTorrent.Neutral ? 0 : 1, + MinimumSeedTime = 288000 // Minimum of 3 days and 8 hours (80 hours in total) + }; + + torrentInfos.Add(release); } } @@ -399,6 +442,44 @@ namespace NzbDrone.Core.Indexers.Definitions .ToArray(); } + private static string GetTitle(GazelleGamesGroup group, GazelleGamesTorrent torrent) + { + var title = WebUtility.HtmlDecode(torrent.ReleaseTitle); + + if (group.Year is > 0 && !title.Contains(group.Year.ToString())) + { + title += $" ({group.Year})"; + } + + if (torrent.RemasterTitle.IsNotNullOrWhiteSpace()) + { + title += $" [{$"{torrent.RemasterTitle} {torrent.RemasterYear}".Trim()}]"; + } + + var flags = new List + { + $"{torrent.Format} {torrent.Encoding}".Trim() + }; + + if (group.Artists is { Count: > 0 }) + { + flags.AddIfNotNull(group.Artists.Select(a => a.Name).Join(", ")); + } + + flags.AddIfNotNull(torrent.Language); + flags.AddIfNotNull(torrent.Region); + flags.AddIfNotNull(torrent.Miscellaneous); + + flags = flags.Where(x => x.IsNotNullOrWhiteSpace()).ToList(); + + if (flags.Any()) + { + title += $" [{string.Join(" / ", flags)}]"; + } + + return title; + } + private string GetDownloadUrl(int torrentId) { // AuthKey is required but not checked, just pass in a dummy variable @@ -413,7 +494,7 @@ namespace NzbDrone.Core.Indexers.Definitions return url.FullUri; } - private string GetInfoUrl(string groupId, int torrentId) + private string GetInfoUrl(int groupId, int torrentId) { var url = new HttpUri(_settings.BaseUrl) .CombinePath("/torrents.php") @@ -444,7 +525,7 @@ namespace NzbDrone.Core.Indexers.Definitions Passkey = ""; } - [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings), Must have User Permissions", Privacy = PrivacyLevel.ApiKey)] + [FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in Settings => Access Settings)", HelpTextWarning = "Must have User and Torrents permissions", Privacy = PrivacyLevel.ApiKey)] public string Apikey { get; set; } [FieldDefinition(3, Label = "Search Group Names", Type = FieldType.Checkbox, HelpText = "Search Group Names Only")] @@ -466,8 +547,9 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGamesGroup { - public List Artists { get; set; } + public ReadOnlyCollection Artists { get; set; } public object Torrents { get; set; } + public int? Year { get; set; } } public class GazelleGamesArtist @@ -478,13 +560,23 @@ namespace NzbDrone.Core.Indexers.Definitions public class GazelleGamesTorrent { + public int CategoryId { get; set; } + public string Format { get; set; } + public string Encoding { get; set; } + public string Language { get; set; } + public string Region { get; set; } + public string RemasterYear { get; set; } + public string RemasterTitle { get; set; } + public string ReleaseTitle { get; set; } + public string Miscellaneous { get; set; } + public int Scene { get; set; } + public DateTime Time { get; set; } + public string TorrentType { get; set; } + public int FileCount { get; set; } public string Size { get; set; } public int? Snatched { get; set; } public int Seeders { get; set; } public int Leechers { get; set; } - public string ReleaseTitle { get; set; } - public DateTime Time { get; set; } - public int FileCount { get; set; } public GazelleGamesFreeTorrent FreeTorrent { get; set; } public bool PersonalFL { get; set; } public bool LowSeedFL { get; set; } @@ -503,9 +595,9 @@ namespace NzbDrone.Core.Indexers.Definitions public enum GazelleGamesFreeTorrent { - Normal, - FreeLeech, - Neutral, - Either + Normal = 0, + FreeLeech = 1, + Neutral = 2, + Either = 3 } }