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);
+ }
+ }
+}