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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="AsyncKeyedLock" Version="8.0.2" />
<PackageVersion Include="Backport.System.Threading.Lock" Version="3.1.6" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.24" />
Expand Down
4 changes: 2 additions & 2 deletions fe/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion fe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "listenarr-fe",
"version": "0.2.55",
"version": "0.2.56",
"private": true,
"type": "module",
"engines": {
Expand Down
16 changes: 8 additions & 8 deletions listenarr.api/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,8 +1023,8 @@ public async Task<ActionResult<List<MetadataSearchResult>>> IntelligentSearch(
}

_logger.LogInformation("IntelligentSearch called for query: {Query}", LogRedaction.SanitizeText(query));
var region = Request.Query.ContainsKey("region") ? Request.Query["region"].ToString() ?? "us" : "us";
var language = Request.Query.ContainsKey("language") ? Request.Query["language"].ToString() : null;
var region = Request.Query.TryGetValue("region", out var regionValue) ? regionValue.ToString() ?? "us" : "us";
var language = Request.Query.TryGetValue("language", out var languageValue) ? languageValue.ToString() : null;
var results = await _searchService.IntelligentSearchAsync(query, candidateLimit, returnLimit, containmentMode, requireAuthorAndPublisher, fuzzyThreshold, region, language, HttpContext.RequestAborted);
// Normalize images for metadata results so the SPA receives local /api/v{version}/images/{asin} when possible
if (_imageCacheService != null && results != null)
Expand Down Expand Up @@ -1135,13 +1135,13 @@ public async Task<ActionResult<List<SearchResult>>> IndexersSearch(

// Support MyAnonamouse query string toggles (mamFilter, mamSearchInDescription, mamSearchInSeries, mamSearchInFilenames, mamLanguage, mamFreeleechWedge)
var mamOptions = new Listenarr.Api.Models.MyAnonamouseOptions();
if (Request.Query.ContainsKey("mamFilter") && Enum.TryParse<Listenarr.Api.Models.MamTorrentFilter>(Request.Query["mamFilter"].ToString() ?? string.Empty, true, out var mamFilter))
if (Request.Query.TryGetValue("mamFilter", out var queryMamFilter) && Enum.TryParse<Listenarr.Api.Models.MamTorrentFilter>(queryMamFilter.ToString() ?? string.Empty, true, out var mamFilter))
mamOptions.Filter = mamFilter;
if (Request.Query.ContainsKey("mamSearchInDescription") && bool.TryParse(Request.Query["mamSearchInDescription"], out var sd)) mamOptions.SearchInDescription = sd;
if (Request.Query.ContainsKey("mamSearchInSeries") && bool.TryParse(Request.Query["mamSearchInSeries"], out var ss)) mamOptions.SearchInSeries = ss;
if (Request.Query.ContainsKey("mamSearchInFilenames") && bool.TryParse(Request.Query["mamSearchInFilenames"], out var sf)) mamOptions.SearchInFilenames = sf;
if (Request.Query.ContainsKey("mamLanguage")) mamOptions.SearchLanguage = Request.Query["mamLanguage"].ToString();
if (Request.Query.ContainsKey("mamFreeleechWedge") && Enum.TryParse<Listenarr.Api.Models.MamFreeleechWedge>(Request.Query["mamFreeleechWedge"].ToString() ?? string.Empty, true, out var mw)) mamOptions.FreeleechWedge = mw;
if (Request.Query.TryGetValue("mamSearchInDescription", out var queryMamSearchInDescription) && bool.TryParse(queryMamSearchInDescription, out var sd)) mamOptions.SearchInDescription = sd;
if (Request.Query.TryGetValue("mamSearchInSeries", out var queryMamSearchInSeries) && bool.TryParse(queryMamSearchInSeries, out var ss)) mamOptions.SearchInSeries = ss;
if (Request.Query.TryGetValue("mamSearchInFilenames", out var queryMamSearchInFilenames) && bool.TryParse(queryMamSearchInFilenames, out var sf)) mamOptions.SearchInFilenames = sf;
if (Request.Query.TryGetValue("mamLanguage", out var queryMamLanguage)) mamOptions.SearchLanguage = queryMamLanguage.ToString();
if (Request.Query.TryGetValue("mamFreeleechWedge", out var queryMamFreeleechWedge) && Enum.TryParse<Listenarr.Api.Models.MamFreeleechWedge>(queryMamFreeleechWedge.ToString() ?? string.Empty, true, out var mw)) mamOptions.FreeleechWedge = mw;

var req = new Listenarr.Api.Models.SearchRequest { MyAnonamouse = mamOptions };
var results = await _searchService.SearchIndexersAsync(query, category, sortBy, sortDirection, isAutomaticSearch, req);
Expand Down
1 change: 1 addition & 0 deletions listenarr.api/Listenarr.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Asp.Versioning.Mvc" VersionOverride="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" VersionOverride="8.1.0" />
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))" Include="Backport.System.Threading.Lock" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="SixLabors.ImageSharp" />
Expand Down
26 changes: 17 additions & 9 deletions listenarr.api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:4545",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"LISTENARR_CONTENT_ROOT": "../../listenarr.api"
}
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:4545"
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7172;http://localhost:4545",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"LISTENARR_CONTENT_ROOT": "../../listenarr.api"
}
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7172;http://localhost:4545"
},
"IIS Express": {
"commandName": "IISExpress",
Expand All @@ -31,5 +30,14 @@
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:36264/",
"sslPort": 44320
}
}
}
}
65 changes: 32 additions & 33 deletions listenarr.api/Services/AudioFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
// Ensure candidate is the same directory or a subdirectory of the existing dir
var isInExistingDir = candidateDir.Equals(existingDir, StringComparison.OrdinalIgnoreCase) ||
candidateDir.StartsWith(existingDir + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);

// Also allow if file is within the audiobook's BasePath (multi-file migration)
var isInBasePath = !string.IsNullOrWhiteSpace(audiobook.BasePath) &&
candidateFull.StartsWith(Path.GetFullPath(audiobook.BasePath) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
Expand Down Expand Up @@ -93,11 +93,13 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
await toastSvc.PublishToastAsync("warning", "File not associated", $"Refused to associate {Path.GetFileName(filePath)} to {audiobookTitle}");
}
}
catch (Exception thx) when (thx is not OperationCanceledException && thx is not OutOfMemoryException && thx is not StackOverflowException) {
catch (Exception thx) when (thx is not OperationCanceledException && thx is not OutOfMemoryException && thx is not StackOverflowException)
{
_logger.LogDebug(thx, "Failed to publish toast for refused file association");
}
}
catch (Exception hx) when (hx is not OperationCanceledException && hx is not OutOfMemoryException && hx is not StackOverflowException) {
catch (Exception hx) when (hx is not OperationCanceledException && hx is not OutOfMemoryException && hx is not StackOverflowException)
{
_logger.LogDebug(hx, "Failed to persist history for refused file association (AudiobookId={AudiobookId}, File={File})", audiobookId, filePath);
}

Expand All @@ -106,7 +108,8 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
}
}
}
catch (Exception exDir) when (exDir is not OperationCanceledException && exDir is not OutOfMemoryException && exDir is not StackOverflowException) {
catch (Exception exDir) when (exDir is not OperationCanceledException && exDir is not OutOfMemoryException && exDir is not StackOverflowException)
{
_logger.LogDebug(exDir, "Failed to verify audiobook folder containment for AudiobookId={AudiobookId} File={File}", audiobookId, filePath);
}

Expand All @@ -119,24 +122,18 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
var cacheKey = $"meta::{filePath}::{ticks}";
if (!_memoryCache.TryGetValue(cacheKey, out var cachedObj) || !(cachedObj is AudioMetadata cachedMeta))
{
await _limiter.Sem.WaitAsync();
try
{
meta = await metadataService.ExtractFileMetadataAsync(filePath);
// Cache for 5 minutes
_memoryCache.Set(cacheKey, meta, TimeSpan.FromMinutes(5));
}
finally
{
_limiter.Sem.Release();
}
using var _ = await _limiter.Sem.LockAsync();
meta = await metadataService.ExtractFileMetadataAsync(filePath);
// Cache for 5 minutes
_memoryCache.Set(cacheKey, meta, TimeSpan.FromMinutes(5));
}
else
{
meta = cachedMeta;
}
}
catch (Exception mEx) when (mEx is not OperationCanceledException && mEx is not OutOfMemoryException && mEx is not StackOverflowException) {
catch (Exception mEx) when (mEx is not OperationCanceledException && mEx is not OutOfMemoryException && mEx is not StackOverflowException)
{
_logger.LogInformation(mEx, "Metadata extraction failed for {Path}", filePath);
}
// If metadata extraction produced minimal results, attempt to ensure ffprobe is installed
Expand All @@ -162,27 +159,25 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
if (!string.IsNullOrEmpty(ffpath))
{
// Retry metadata extraction once under limiter
await _limiter.Sem.WaitAsync();
try
{
meta = await metadataService.ExtractFileMetadataAsync(filePath);
// Update cache
var fileInfoForCache2 = new FileInfo(filePath);
var ticks2 = fileInfoForCache2.Exists ? fileInfoForCache2.LastWriteTimeUtc.Ticks : 0L;
var cacheKey2 = $"meta::{filePath}::{ticks2}";
_memoryCache.Set(cacheKey2, meta, TimeSpan.FromMinutes(5));
}
finally { _limiter.Sem.Release(); }
using var _ = await _limiter.Sem.LockAsync();
meta = await metadataService.ExtractFileMetadataAsync(filePath);
// Update cache
var fileInfoForCache2 = new FileInfo(filePath);
var ticks2 = fileInfoForCache2.Exists ? fileInfoForCache2.LastWriteTimeUtc.Ticks : 0L;
var cacheKey2 = $"meta::{filePath}::{ticks2}";
_memoryCache.Set(cacheKey2, meta, TimeSpan.FromMinutes(5));
}
}
catch (Exception rex) when (rex is not OperationCanceledException && rex is not OutOfMemoryException && rex is not StackOverflowException) {
catch (Exception rex) when (rex is not OperationCanceledException && rex is not OutOfMemoryException && rex is not StackOverflowException)
{
_logger.LogInformation(rex, "Retry metadata extraction failed for {Path}", filePath);
}
}
}
}
}
catch (Exception exRetry) when (exRetry is not OperationCanceledException && exRetry is not OutOfMemoryException && exRetry is not StackOverflowException) {
catch (Exception exRetry) when (exRetry is not OperationCanceledException && exRetry is not OutOfMemoryException && exRetry is not StackOverflowException)
{
_logger.LogDebug(exRetry, "Non-fatal error while attempting ffprobe install/retry for {Path}", filePath);
}
var fi = new FileInfo(filePath);
Expand Down Expand Up @@ -215,7 +210,8 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
var conn = db.Database.GetDbConnection();
_logger.LogInformation("Created AudiobookFile for audiobook {AudiobookId}: {Path} (Db: {Db}) Id={Id}", audiobookId, filePath, conn?.ConnectionString, fileRecord.Id);
}
catch (Exception logEx) when (logEx is not OperationCanceledException && logEx is not OutOfMemoryException && logEx is not StackOverflowException) {
catch (Exception logEx) when (logEx is not OperationCanceledException && logEx is not OutOfMemoryException && logEx is not StackOverflowException)
{
_logger.LogInformation("Created AudiobookFile for audiobook {AudiobookId}: {Path} (Db: unknown) Id={Id}", audiobookId, filePath, fileRecord.Id);
_logger.LogDebug(logEx, "Failed to log DB connection string for AudiobookFile creation");
}
Expand Down Expand Up @@ -268,11 +264,13 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
await db.SaveChangesAsync();
}
}
catch (Exception aubEx) when (aubEx is not OperationCanceledException && aubEx is not OutOfMemoryException && aubEx is not StackOverflowException) {
catch (Exception aubEx) when (aubEx is not OperationCanceledException && aubEx is not OutOfMemoryException && aubEx is not StackOverflowException)
{
_logger.LogDebug(aubEx, "Failed to update Audiobook file summary fields for AudiobookId {AudiobookId}", audiobookId);
}
}
catch (Exception hx) when (hx is not OperationCanceledException && hx is not OutOfMemoryException && hx is not StackOverflowException) {
catch (Exception hx) when (hx is not OperationCanceledException && hx is not OutOfMemoryException && hx is not StackOverflowException)
{
_logger.LogDebug(hx, "Failed to create history entry for added audiobook file {Path}", filePath);
}

Expand All @@ -298,7 +296,8 @@ public async Task<bool> EnsureAudiobookFileAsync(int audiobookId, string filePat
}
}
}
catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) {
catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException)
{
_logger.LogWarning(ex, "Failed to create AudiobookFile record for audiobook {AudiobookId} at {Path}", audiobookId, filePath);
return false;
}
Expand Down
11 changes: 3 additions & 8 deletions listenarr.api/Services/CompletedDownloadHandlingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Listenarr.Domain.Models;
using Listenarr.Infrastructure.Models;
using Microsoft.EntityFrameworkCore;

namespace Listenarr.Api.Services
{
Expand All @@ -44,7 +40,7 @@ public class CompletedDownloadHandlingService : BackgroundService

// Track downloads that are in the completion pipeline to avoid duplicate processing
private readonly Dictionary<string, DateTime> _processingDownloads = new();
private readonly object _processingLock = new();
private readonly Lock _processingLock = new();

public CompletedDownloadHandlingService(
IServiceScopeFactory serviceScopeFactory,
Expand Down Expand Up @@ -200,9 +196,8 @@ private async Task ProcessCompletedDownloadsAsync(CancellationToken cancellation
// Skip if already being processed
lock (_processingLock)
{
if (_processingDownloads.ContainsKey(download.Id))
if (_processingDownloads.TryGetValue(download.Id, out var firstSeen))
{
var firstSeen = _processingDownloads[download.Id];
if (DateTime.UtcNow - firstSeen > TimeSpan.FromMinutes(5))
{
// Been processing for too long, reset and retry
Expand Down
Loading