From 910b2588db6fd52b4b77ade2d208f33fe3ff9419 Mon Sep 17 00:00:00 2001 From: Felix Date: Mon, 2 Mar 2026 16:36:08 +0200 Subject: [PATCH] feat(jacktor): add result deduplication and improve release identification Add CollapseNearDuplicates method to filter near-duplicate torrent results based on quality, size, seeders, peers, season, voice, and title. Improve BuildRid to use guid/details as stable identifiers when infohash is unavailable. Add season labels to release display in Controller. Add Guid property to JackettResult model. --- JackTor/Controller.cs | 12 ++++--- JackTor/JackTorInvoke.cs | 62 ++++++++++++++++++++++++++++++--- JackTor/Models/JackTorModels.cs | 2 ++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/JackTor/Controller.cs b/JackTor/Controller.cs index e7d785e..b20ac95 100644 --- a/JackTor/Controller.cs +++ b/JackTor/Controller.cs @@ -118,11 +118,15 @@ namespace JackTor.Controllers var similarTpl = new SimilarTpl(); foreach (var torrent in releases) { - string releaseName = string.IsNullOrWhiteSpace(torrent.Voice) - ? (torrent.Tracker ?? "Без назви") - : torrent.Voice; + string seasonLabel = (torrent.Seasons != null && torrent.Seasons.Length > 0) + ? $"S{string.Join(",", torrent.Seasons.OrderBy(i => i))}" + : $"S{targetSeason}"; - string qualityInfo = $"{torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}"; + string releaseName = string.IsNullOrWhiteSpace(torrent.Voice) + ? $"{seasonLabel} • {(torrent.Tracker ?? "Без назви")}" + : $"{seasonLabel} • {torrent.Voice}"; + + string qualityInfo = $"{torrent.Tracker} / {torrent.QualityLabel} / {torrent.MediaInfo} / ↑{torrent.Seeders}"; string releaseLink = accsArgs($"{host}/jacktor/serial/{torrent.Rid}?rjson={rjson}&title={enTitle}&original_title={enOriginal}&s={targetSeason}"); similarTpl.Append(releaseName, null, qualityInfo, releaseLink); diff --git a/JackTor/JackTorInvoke.cs b/JackTor/JackTorInvoke.cs index 4226f4f..a752809 100644 --- a/JackTor/JackTorInvoke.cs +++ b/JackTor/JackTorInvoke.cs @@ -365,7 +365,7 @@ namespace JackTor if (!string.IsNullOrWhiteSpace(_init.filter_ignore) && IsRegexMatch(regexSource, _init.filter_ignore)) continue; - string rid = BuildRid(raw.InfoHash, source); + string rid = BuildRid(raw.InfoHash, raw.Guid, raw.Details, source); var parsed = new JackTorParsedResult() { @@ -403,7 +403,7 @@ namespace JackTor } } - var result = byRid.Values; + var result = CollapseNearDuplicates(byRid.Values); IOrderedEnumerable ordered = result .OrderByDescending(i => i.AudioRank) @@ -637,6 +637,54 @@ namespace JackTor return $"{mb:0.##} MB"; } + private IEnumerable CollapseNearDuplicates(IEnumerable items) + { + var dedup = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in items) + { + string seasonKey = (item.Seasons == null || item.Seasons.Length == 0) + ? "-" + : string.Join(",", item.Seasons.OrderBy(i => i)); + + string key = $"{item.Quality}|{item.Size}|{item.Seeders}|{item.Peers}|{seasonKey}|{NormalizeVoice(item.Voice)}|{NormalizeTitle(item.Title)}"; + if (dedup.TryGetValue(key, out JackTorParsedResult exists)) + { + if (Score(item) > Score(exists)) + dedup[key] = item; + } + else + { + dedup[key] = item; + } + } + + return dedup.Values; + } + + private string NormalizeVoice(string voice) + { + if (string.IsNullOrWhiteSpace(voice)) + return string.Empty; + + return string.Join(",", + voice.ToLowerInvariant() + .Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .Distinct() + .OrderBy(i => i)); + } + + private string NormalizeTitle(string title) + { + if (string.IsNullOrWhiteSpace(title)) + return string.Empty; + + string lower = title.ToLowerInvariant(); + lower = Regex.Replace(lower, "\\s+", " ").Trim(); + return lower; + } + private int Score(JackTorParsedResult item) { int qualityScore = item.Quality * 100; @@ -648,13 +696,19 @@ namespace JackTor return qualityScore + seedScore + audioScore + gainScore + dateScore; } - private string BuildRid(string infoHash, string source) + private string BuildRid(string infoHash, string guid, string details, string source) { string hash = (infoHash ?? string.Empty).Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(hash)) return hash; - byte[] sourceBytes = Encoding.UTF8.GetBytes(source ?? string.Empty); + string stableId = !string.IsNullOrWhiteSpace(guid) + ? guid.Trim() + : !string.IsNullOrWhiteSpace(details) + ? details.Trim() + : source ?? string.Empty; + + byte[] sourceBytes = Encoding.UTF8.GetBytes(stableId); byte[] digest = SHA1.HashData(sourceBytes); return Convert.ToHexString(digest).ToLowerInvariant(); } diff --git a/JackTor/Models/JackTorModels.cs b/JackTor/Models/JackTorModels.cs index 34aa39b..e6cbd16 100644 --- a/JackTor/Models/JackTorModels.cs +++ b/JackTor/Models/JackTorModels.cs @@ -88,6 +88,8 @@ namespace JackTor.Models public class JackettResult { + public string Guid { get; set; } + public string Tracker { get; set; } public string TrackerId { get; set; }