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.
pull/1970/head
Bogdan 5 months ago
parent bf47380f7b
commit f9c9d4a0e0

@ -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<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((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<TorrentInfo>();
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<HttpRequest>(v => v.Method == HttpMethod.Get), Subject.Definition))
.Returns<HttpRequest, IndexerDefinition>((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);
}

@ -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<GazelleGamesSettings>
{
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<ValidationFailure> 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<GazelleGamesUserResponse>(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<IndexerRequest> GetRequest(List<KeyValuePair<string, string>> 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<GazelleGamesUserResponse>(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<IndexerRequest> 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<KeyValuePair<string, string>> GetBasicSearchParameters(string searchTerm, SearchCriteriaBase searchCriteria)
{
var parameters = "request=search&search_type=torrents&empty_groups=filled&order_by=time&order_way=desc";
var parameters = new List<KeyValuePair<string, string>>
{
{ "request", "search" },
{ "search_type", "torrents" },
{ "empty_groups", "filled" },
{ "order_by", "time" },
{ "order_way", "desc" }
};
if (!string.IsNullOrWhiteSpace(searchTerm))
if (searchTerm.IsNotNullOrWhiteSpace())
{
var searchType = Settings.SearchGroupNames ? "groupname" : "searchstr";
parameters.Add(
_settings.SearchGroupNames ? "groupname" : "searchstr",
searchTerm.Replace(".", " "));
}
if (searchCriteria.Categories != null)
{
var categoryMappings = _capabilities.Categories
.MapTorznabCapsToTrackers(searchCriteria.Categories)
.Distinct()
.Where(x => !x.IsAllDigits())
.ToList();
parameters += string.Format("&{1}={0}", searchTerm.Replace(".", " "), searchType);
categoryMappings.ForEach(category => parameters.Add("artistcheck[]", category));
}
if (categories != null)
if (searchCriteria.MinSize is > 0)
{
foreach (var cat in Capabilities.Categories.MapTorznabCapsToTrackers(categories))
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<GazelleGamesResponse>(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<string, GazelleGamesGroup> response;
try
{
response = ((JObject)jsonResponse.Resource.Response).ToObject<Dictionary<string, GazelleGamesGroup>>();
}
catch
{
return torrentInfos;
}
var groups = response.ToObject<Dictionary<int, GazelleGamesGroup>>(JsonSerializer.Create(Json.GetSerializerSettings()));
foreach (var result in response)
foreach (var group in groups)
{
Dictionary<string, GazelleGamesTorrent> torrents;
try
{
torrents = ((JObject)result.Value.Torrents).ToObject<Dictionary<string, GazelleGamesTorrent>>();
}
catch
if (group.Value.Torrents is not JObject groupTorrents)
{
continue;
}
if (result.Value.Torrents != null)
var torrents = groupTorrents
.ToObject<Dictionary<int, GazelleGamesTorrent>>(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<string>
{
$"{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<GazelleGamesArtist> Artists { get; set; }
public ReadOnlyCollection<GazelleGamesArtist> 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
}
}

Loading…
Cancel
Save