Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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))
{
Expand All @@ -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<MamFreeleechWedge>(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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,8 @@ public static List<IndexerSearchResult> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
}