From 557d3c12c912f8b4930d57f62d59ea6ad4a1ce40 Mon Sep 17 00:00:00 2001 From: Matthew Sloan Date: Thu, 6 Aug 2026 13:42:18 -0500 Subject: [PATCH] Add freeleech wedge support for MyAnonamouse Append fl=1 to download URLs when the freeleech wedge is configured (Preferred/Required) and the torrent is not already site or personal freeleech. Includes JSON parsing of mam_options and full test coverage. --- .../MyAnonamouseDownloadUrlBuilder.cs | 91 +++++++++- .../MyAnonamouseResponseParser.cs | 3 +- .../Providers/MyAnonamouseFreeleechTests.cs | 165 ++++++++++++++++++ 3 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 tests/Features/Api/Services/Search/Providers/MyAnonamouseFreeleechTests.cs diff --git a/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseDownloadUrlBuilder.cs b/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseDownloadUrlBuilder.cs index 0a0d1bb36..9c32f2e08 100644 --- a/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseDownloadUrlBuilder.cs +++ b/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseDownloadUrlBuilder.cs @@ -15,13 +15,15 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +using System.Text.Json; using Listenarr.Application.Common; +using Listenarr.Application.Search.Core; namespace Listenarr.Application.Search.Indexers.MyAnonamouse { internal static class MyAnonamouseDownloadUrlBuilder { - public static string Build(string dlHash, string torrentId, Indexer indexer) + public static string Build(string dlHash, string torrentId, Indexer indexer, bool isAlreadyFreeleech = false) { if (string.IsNullOrWhiteSpace(dlHash) && string.IsNullOrWhiteSpace(torrentId)) { @@ -48,7 +50,94 @@ public static string Build(string dlHash, string torrentId, Indexer indexer) downloadUrl += $"{separator}mam_id={Uri.EscapeDataString(mamIdLocal)}"; } + // MAM spends a freeleech wedge when fl=1 is present on the download URL + // (same approach as Prowlarr). Only apply when configured and the torrent + // is not already freeleech / personal freeleech. + if (ShouldApplyFreeleechWedge(indexer) && !isAlreadyFreeleech) + { + var separator = downloadUrl.Contains('?') ? '&' : '?'; + downloadUrl += $"{separator}fl=1"; + } + return downloadUrl; } + + internal static bool ShouldApplyFreeleechWedge(Indexer indexer) + { + var wedge = TryGetFreeleechWedge(indexer.AdditionalSettings); + return wedge is MamFreeleechWedge.Preferred or MamFreeleechWedge.Required; + } + + internal static bool IsAlreadyFreeleech(JsonElement item) + { + // Match Prowlarr: site freeleech or personal freeleech already applied. + // VIP freeleech (fl_vip) still needs a wedge for non-VIP users, so we do + // not treat it as already freeleech without knowing the account class. + return GetJsonBoolean(item, "free") || GetJsonBoolean(item, "personal_freeleech"); + } + + private static MamFreeleechWedge? TryGetFreeleechWedge(string? additionalSettings) + { + if (string.IsNullOrWhiteSpace(additionalSettings)) + { + return null; + } + + try + { + using var doc = JsonDocument.Parse(additionalSettings); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) + { + return null; + } + + if (root.TryGetProperty("mam_options", out var mamOptions) && mamOptions.ValueKind == JsonValueKind.Object) + { + return ParseWedgeProperty(mamOptions); + } + + return ParseWedgeProperty(root); + } + catch (JsonException) + { + return null; + } + } + + private static MamFreeleechWedge? ParseWedgeProperty(JsonElement root) + { + if (root.TryGetProperty("freeleechWedge", out var wedge) + && wedge.ValueKind == JsonValueKind.String + && Enum.TryParse(wedge.GetString() ?? string.Empty, true, out var parsed)) + { + return parsed; + } + + return null; + } + + private static bool GetJsonBoolean(JsonElement item, string propertyName) + { + foreach (var prop in item.EnumerateObject()) + { + if (!string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + return prop.Value.ValueKind switch + { + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Number => prop.Value.TryGetInt32(out var n) && n != 0, + JsonValueKind.String => bool.TryParse(prop.Value.GetString(), out var b) && b + || prop.Value.GetString() is "1", + _ => false + }; + } + + return false; + } } } diff --git a/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.cs b/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.cs index 9ff93f1ed..924d1c2cb 100644 --- a/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.cs +++ b/listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.cs @@ -331,7 +331,8 @@ public static List Parse(string jsonResponse, Indexer index } } - var downloadUrl = MyAnonamouseDownloadUrlBuilder.Build(dlHash, id, indexer); + var isAlreadyFreeleech = MyAnonamouseDownloadUrlBuilder.IsAlreadyFreeleech(item); + var downloadUrl = MyAnonamouseDownloadUrlBuilder.Build(dlHash, id, indexer, isAlreadyFreeleech); // Preserve raw language code for later flagging/flags list string rawLangCode = string.Empty; diff --git a/tests/Features/Api/Services/Search/Providers/MyAnonamouseFreeleechTests.cs b/tests/Features/Api/Services/Search/Providers/MyAnonamouseFreeleechTests.cs new file mode 100644 index 000000000..08f2a9e3e --- /dev/null +++ b/tests/Features/Api/Services/Search/Providers/MyAnonamouseFreeleechTests.cs @@ -0,0 +1,165 @@ +/* + * Listenarr - Audiobook Management System + * Copyright (C) 2024-2026 Listenarr Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +using Microsoft.Extensions.Logging.Abstractions; + +namespace Listenarr.Tests.Features.Api.Services.Search.Providers +{ + [Trait("Name", "MyAnonamouseFreeleechTests")] + [Trait("Category", "IndexerSearchProvider")] + public class MyAnonamouseFreeleechTests + { + [Theory] + [InlineData("Preferred")] + [InlineData("Required")] + public void ParseMyAnonamouse_Appends_Fl_When_FreeleechWedge_Enabled_And_Not_Already_Freeleech(string wedge) + { + var json = @"[ + { + ""guid"": ""https://www.myanonamouse.net/t/123"", + ""dl"": ""abc123"", + ""title"": ""Test"", + ""size"": 12345, + ""free"": false, + ""personal_freeleech"": false + } +]"; + var indexer = new Indexer + { + Name = "MyAnonamouse", + Url = "https://www.myanonamouse.net", + Type = "Torrent", + Implementation = "MyAnonamouse", + AdditionalSettings = $"{{\"mam_id\":\"test_mam\",\"mam_options\":{{\"freeleechWedge\":\"{wedge}\"}}}}" + }; + var results = MyAnonamouseResponseParser.Parse(json, indexer, NullLogger.Instance); + + var result = Assert.Single(results); + Assert.Equal("https://www.myanonamouse.net/tor/download.php/abc123?mam_id=test_mam&fl=1", result.TorrentUrl); + } + + [Fact] + public void ParseMyAnonamouse_Does_Not_Append_Fl_When_Torrent_Is_Already_Freeleech() + { + var json = @"[ + { + ""guid"": ""https://www.myanonamouse.net/t/123"", + ""dl"": ""abc123"", + ""title"": ""Test"", + ""size"": 12345, + ""free"": true + } +]"; + var indexer = new Indexer + { + Name = "MyAnonamouse", + Url = "https://www.myanonamouse.net", + Type = "Torrent", + Implementation = "MyAnonamouse", + AdditionalSettings = """{"mam_id":"test_mam","mam_options":{"freeleechWedge":"Preferred"}}""" + }; + var results = MyAnonamouseResponseParser.Parse(json, indexer, NullLogger.Instance); + + var result = Assert.Single(results); + Assert.Equal("https://www.myanonamouse.net/tor/download.php/abc123?mam_id=test_mam", result.TorrentUrl); + } + + [Fact] + public void ParseMyAnonamouse_Does_Not_Append_Fl_When_Personal_Freeleech() + { + var json = @"[ + { + ""guid"": ""https://www.myanonamouse.net/t/123"", + ""dl"": ""abc123"", + ""title"": ""Test"", + ""size"": 12345, + ""personal_freeleech"": true + } +]"; + var indexer = new Indexer + { + Name = "MyAnonamouse", + Url = "https://www.myanonamouse.net", + Type = "Torrent", + Implementation = "MyAnonamouse", + AdditionalSettings = """{"mam_id":"test_mam","mam_options":{"freeleechWedge":"Required"}}""" + }; + var results = MyAnonamouseResponseParser.Parse(json, indexer, NullLogger.Instance); + + var result = Assert.Single(results); + Assert.Equal("https://www.myanonamouse.net/tor/download.php/abc123?mam_id=test_mam", result.TorrentUrl); + } + + [Fact] + public void ParseMyAnonamouse_Does_Not_Append_Fl_When_FreeleechWedge_Is_Never() + { + var json = @"[ + { + ""guid"": ""https://www.myanonamouse.net/t/123"", + ""dl"": ""abc123"", + ""title"": ""Test"", + ""size"": 12345, + ""free"": false + } +]"; + var indexer = new Indexer + { + Name = "MyAnonamouse", + Url = "https://www.myanonamouse.net", + Type = "Torrent", + Implementation = "MyAnonamouse", + AdditionalSettings = """{"mam_id":"test_mam","mam_options":{"freeleechWedge":"Never"}}""" + }; + var results = MyAnonamouseResponseParser.Parse(json, indexer, NullLogger.Instance); + + var result = Assert.Single(results); + Assert.Equal("https://www.myanonamouse.net/tor/download.php/abc123?mam_id=test_mam", result.TorrentUrl); + } + + [Fact] + public void ParseMyAnonamouse_Appends_Fl_To_Tid_DownloadUrl_When_Wedge_Enabled() + { + var json = """ + [ + { + "id": "1246262", + "title": "A Parade of Horribles", + "size": "1.1 GiB", + "seeders": 2196, + "leechers": 6, + "free": false + } + ] + """; + var indexer = new Indexer + { + Name = "MyAnonamouse", + Url = "https://www.myanonamouse.net", + Type = "Torrent", + Implementation = "MyAnonamouse", + AdditionalSettings = """{"mam_id":"test_mam","mam_options":{"freeleechWedge":"Preferred"}}""" + }; + + var results = MyAnonamouseResponseParser.Parse(json, indexer, NullLogger.Instance); + + var result = Assert.Single(results); + Assert.Equal( + "https://www.myanonamouse.net/tor/download.php?tid=1246262&mam_id=test_mam&fl=1", + result.TorrentUrl); + } + } +}