diff --git a/.github/.cursorrules b/.github/.cursorrules index 7e78bb44d..0899f2135 100644 --- a/.github/.cursorrules +++ b/.github/.cursorrules @@ -1,5 +1,17 @@ # Listenarr Copilot Rules +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow: + +- `CONTRIBUTING.md` +- `BACKEND_ARCHITECTURE.md` +- `.github/RULES.md` +- `.github/AGENTS.md` +- `.github/copilot-instructions.md` + +Repository-specific guidance takes precedence over general examples in this file. Keep infrastructure-shaped dependencies out of `listenarr.application`; define application-owned ports there and implement adapters in infrastructure/API. + ## Project Overview Listenarr is a C# .NET Core Web API backend with Vue.js frontend for automated audiobook downloading and processing. The backend uses ASP.NET Core with Entity Framework Core and SQLite, while the frontend uses Vue.js 3 with TypeScript, Pinia, and Vite. @@ -306,4 +318,4 @@ var audiobooks = await _db.Audiobooks ``` Remember: This project follows established patterns. When in doubt, look at existing code for examples of how similar functionality is implemented. -c:\Users\Robbie\Documents\GitHub\Listenarr\.cursorrules \ No newline at end of file +c:\Users\Robbie\Documents\GitHub\Listenarr\.cursorrules diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 3b85ce64c..36bb6517a 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -1,6 +1,18 @@ ````markdown # Secure .NET Code Generation Codex +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow: + +- `CONTRIBUTING.md` +- `BACKEND_ARCHITECTURE.md` +- `.github/RULES.md` +- `.github/copilot-instructions.md` +- `.github/.cursorrules` + +Repository-specific guidance takes precedence over general examples in this file. Keep infrastructure-shaped dependencies out of `listenarr.application`; define application-owned ports there and implement adapters in infrastructure/API. + As a security-aware developer, generate secure .NET code using ASP.NET Core that inherently prevents top security weaknesses. Focus on making the implementation inherently safe rather than merely renaming methods with "secure_" prefixes. Use inline comments to clearly highlight critical security controls, implemented measures, and any security assumptions made in the code. diff --git a/.github/ANTHROPIC.md b/.github/ANTHROPIC.md index 41c214c93..4d6428d71 100644 --- a/.github/ANTHROPIC.md +++ b/.github/ANTHROPIC.md @@ -1,5 +1,9 @@ # Anthropic/Claude Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/AZURE_OPENAI.md b/.github/AZURE_OPENAI.md index 7ea372d13..362ee0667 100644 --- a/.github/AZURE_OPENAI.md +++ b/.github/AZURE_OPENAI.md @@ -1,5 +1,9 @@ # Azure OpenAI Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend for automated audiobook downloads. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/BARD.md b/.github/BARD.md index 27d89b763..1b308c2d2 100644 --- a/.github/BARD.md +++ b/.github/BARD.md @@ -1,5 +1,9 @@ # Google Bard/Gemini Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/BEDROCK.md b/.github/BEDROCK.md index fb3efc8b2..c3f660c6c 100644 --- a/.github/BEDROCK.md +++ b/.github/BEDROCK.md @@ -1,5 +1,9 @@ # Amazon Bedrock Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 71f8f610f..15417d14f 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -1,6 +1,18 @@ ````markdown # Secure Code Generation Rules for .NET/ASP.NET Core +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow: + +- `CONTRIBUTING.md` +- `BACKEND_ARCHITECTURE.md` +- `.github/RULES.md` +- `.github/copilot-instructions.md` +- `.github/.cursorrules` + +Repository-specific guidance takes precedence over general examples in this file. Keep infrastructure-shaped dependencies out of `listenarr.application`; define application-owned ports there and implement adapters in infrastructure/API. + As a security-aware developer, generate secure .NET code using ASP.NET Core that inherently prevents top security weaknesses. Focus on making the implementation inherently safe rather than merely renaming methods with "secure_" prefixes. Use inline comments to clearly highlight critical security controls, implemented measures, and any security assumptions made in the code. Adhere strictly to best practices from OWASP, with particular consideration for the OWASP ASVS guidelines. **Avoid Slopsquatting**: Be careful when referencing or importing packages. Do not guess if a package exists. Comment on any low reputation or uncommon packages you have included. --- diff --git a/.github/CLAUDE_LISTENARR.md b/.github/CLAUDE_LISTENARR.md index 2ec0822b6..41050e1b3 100644 --- a/.github/CLAUDE_LISTENARR.md +++ b/.github/CLAUDE_LISTENARR.md @@ -1,5 +1,9 @@ # Claude AI Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Quick Reference This file contains Claude-specific guidance for the Listenarr audiobook management system. For comprehensive secure coding practices, see [AGENTS.md](AGENTS.md) and [CLAUDE.md](CLAUDE.md). For complete project details, see [copilot-instructions.md](copilot-instructions.md). diff --git a/.github/COHERE.md b/.github/COHERE.md index 57638df4c..9a7c244d7 100644 --- a/.github/COHERE.md +++ b/.github/COHERE.md @@ -1,5 +1,9 @@ # Cohere Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/HUGGINGFACE.md b/.github/HUGGINGFACE.md index 0d530530b..8ed45fff3 100644 --- a/.github/HUGGINGFACE.md +++ b/.github/HUGGINGFACE.md @@ -1,5 +1,9 @@ # Hugging Face Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic provider guidance. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/OpenAI.md b/.github/OpenAI.md index 20a1acce9..f234466cd 100644 --- a/.github/OpenAI.md +++ b/.github/OpenAI.md @@ -1,5 +1,18 @@ # OpenAI Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow: + +- `CONTRIBUTING.md` +- `BACKEND_ARCHITECTURE.md` +- `.github/RULES.md` +- `.github/copilot-instructions.md` +- `.github/AGENTS.md` +- `.github/.cursorrules` + +Repository-specific guidance takes precedence over general examples in this file. Keep infrastructure-shaped dependencies out of `listenarr.application`; define application-owned ports there and implement adapters in infrastructure/API. + ## Overview Listenarr is a C# .NET 10.0 audiobook management system with Vue.js 3 frontend. See [copilot-instructions.md](copilot-instructions.md) for complete details. diff --git a/.github/RULES.md b/.github/RULES.md index adf7ad502..9ed2f281e 100644 --- a/.github/RULES.md +++ b/.github/RULES.md @@ -2,6 +2,16 @@ This folder contains comprehensive instructions for AI assistants working with the Listenarr audiobook management system. +## Mandatory First Step + +Before making code, dependency, workflow, or documentation changes, AI agents must review and follow: + +- Repository contribution rules: [`../CONTRIBUTING.md`](../CONTRIBUTING.md) +- Backend architecture boundaries: [`../BACKEND_ARCHITECTURE.md`](../BACKEND_ARCHITECTURE.md) +- The primary AI guidance files listed below, especially [`copilot-instructions.md`](copilot-instructions.md), [`AGENTS.md`](AGENTS.md), and [`.cursorrules`](.cursorrules) + +If these documents conflict, follow the more specific repository guidance first. In particular, keep infrastructure-shaped dependencies out of `listenarr.application`; add application-owned ports and implement adapters in infrastructure/API. + ## Primary Reference Files ### [copilot-instructions.md](copilot-instructions.md) - **MOST COMPREHENSIVE** @@ -61,10 +71,11 @@ These files provide quick-start guidance tailored to specific AI providers, with ## Quick Start -1. **For comprehensive project understanding**: Read [copilot-instructions.md](copilot-instructions.md) -2. **For security compliance**: Read [AGENTS.md](AGENTS.md) -3. **For coding standards**: Read [.cursorrules](.cursorrules) -4. **For provider-specific guidance**: Choose your AI provider file above +1. **Before changing anything**: Read [`../CONTRIBUTING.md`](../CONTRIBUTING.md) and [`../BACKEND_ARCHITECTURE.md`](../BACKEND_ARCHITECTURE.md) +2. **For comprehensive project understanding**: Read [copilot-instructions.md](copilot-instructions.md) +3. **For security compliance**: Read [AGENTS.md](AGENTS.md) +4. **For coding standards**: Read [.cursorrules](.cursorrules) +5. **For provider-specific guidance**: Choose your AI provider file above ## Project Overview (Quick Reference) diff --git a/.github/WARP.md b/.github/WARP.md index cf4c476df..bc68a6e9c 100644 --- a/.github/WARP.md +++ b/.github/WARP.md @@ -2,6 +2,10 @@ This file provides guidance to WARP (warp.dev) when working with code in this repository. +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic tool guidance. + ## Project Overview Listenarr is an automated audiobook collection management system built as a full-stack application with a C# .NET 10 backend API and Vue.js 3 frontend. The project follows a monorepo structure with integrated build processes. diff --git a/.github/clinerules b/.github/clinerules index 2f3300fa1..1630545c3 100644 --- a/.github/clinerules +++ b/.github/clinerules @@ -1,5 +1,9 @@ # Cline AI Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic tool guidance. + As a security-aware developer, generate secure .NET code using ASP.NET Core that inherently prevents top security weaknesses. Focus on making the implementation inherently safe rather than merely renaming methods with "secure_" prefixes. Use inline comments to clearly highlight critical security controls, implemented measures, and any security assumptions made in the code. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1d006335d..00f859f03 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,6 +2,18 @@ This is a complete C# .NET Web API backend with Vue.js frontend for automated audiobook downloading and processing. +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow: + +- `CONTRIBUTING.md` +- `BACKEND_ARCHITECTURE.md` +- `.github/RULES.md` +- `.github/AGENTS.md` +- `.github/.cursorrules` + +Repository-specific guidance takes precedence over general examples in this file. Keep infrastructure-shaped dependencies out of `listenarr.application`; define application-owned ports there and implement adapters in infrastructure/API. + ## Project Overview - **Backend**: ASP.NET Core Web API (.NET 10.0+ / net10.0) with modular service architecture - **Frontend**: Vue.js 3 + TypeScript + Pinia + Vue Router + Vite diff --git a/.github/windsurfrules b/.github/windsurfrules index d00fa1bfd..b75fd2ef4 100644 --- a/.github/windsurfrules +++ b/.github/windsurfrules @@ -6,6 +6,10 @@ globs: **/*.cs, **/*.csproj, **/*.json, **/*.xml, **/*.vue, **/*.ts # Windsurf AI Instructions for Listenarr +## Required Repository Context + +Before making code, dependency, workflow, or documentation changes, review and follow `CONTRIBUTING.md`, `BACKEND_ARCHITECTURE.md`, `.github/RULES.md`, and the primary AI guidance files. Repository-specific guidance takes precedence over generic tool guidance. + As a security-aware developer, generate secure .NET code using ASP.NET Core that inherently prevents top security weaknesses. Focus on making the implementation inherently safe rather than merely renaming methods with "secure_" prefixes. Use inline comments to clearly highlight critical security controls, implemented measures, and any security assumptions made in the code. diff --git a/BACKEND_ARCHITECTURE.md b/BACKEND_ARCHITECTURE.md new file mode 100644 index 000000000..be912dea8 --- /dev/null +++ b/BACKEND_ARCHITECTURE.md @@ -0,0 +1,47 @@ +# Backend Architecture Boundaries + +Listenarr is moving toward a layered backend where each project has a clear job: + +- `listenarr.domain` owns the domain model, value objects, domain exceptions, and business rules that do not need hosting, persistence, files, or network access. +- `listenarr.application` owns use-case orchestration, application services, DTOs, mapping, and contracts that other layers implement. It can coordinate work, but it should avoid owning persistence, file, network, parsing, or image-processing implementations. +- `listenarr.infrastructure` owns concrete adapters for technical concerns: EF Core and SQLite persistence, filesystem work, external HTTP clients, metadata/tagging libraries, HTML scraping/parsing, image inspection, cache implementations, SignalR infrastructure, and downloader integrations. +- `listenarr.api` is the composition and hosting layer. It wires dependency injection, controllers, middleware, Swagger/OpenAPI, auth policy, and request pipeline behavior. + +## Current Decision + +The diagram describes the intended boundary: application is business/use-case logic and infrastructure is persistence, files, and external adapters. The codebase is still in transition, but implementation-specific packages should be kept out of `listenarr.application` unless there is a documented reason to do otherwise. + +New implementation-specific dependencies should go in `listenarr.infrastructure`. The application layer should define contracts and coordinate use cases; infrastructure should implement those contracts with EF Core, filesystem, HTTP, parsing, image, tagging, and other adapter libraries. + +The application project should not reference SQLite providers, EF Core implementation packages, Swagger/OpenAPI packages, HTML parsers, image libraries, audio tagging libraries, ASP.NET Core hosting types, SignalR hubs, HTTP context, or data-protection implementations directly. SQLite and EF Core belong to infrastructure, Swagger/OpenAPI belongs to API, hosted adapters and SignalR delivery belong to infrastructure/API, and parsing/tagging/image inspection belong behind application ports implemented by infrastructure. + +## Boundary Cleanup + +The application layer now delegates these infrastructure-shaped concerns through interfaces: + +- EF Core update failures are translated by infrastructure into application-owned `PersistenceException` types before they leave persistence. +- TagLibSharp ASIN writing is behind `IAudioTagWriter`, implemented by infrastructure. +- ImageSharp cover probing is behind `ICoverImageProbe`, implemented by infrastructure. +- HtmlAgilityPack text extraction and Audible author-page parsing are behind `IHtmlTextExtractor` and `IAudibleAuthorPageParser`, implemented by infrastructure. +- Hosted services and SignalR hubs live in infrastructure. Application code publishes client events through `IHubBroadcaster` instead of referencing hubs or `IHubContext`. +- HTTP request details are exposed to application services through `IRequestContextAccessor`, with ASP.NET Core adaptation handled outside application. +- Secret protection is exposed through `ISecretProtector`, with Data Protection implemented in infrastructure. +- `listenarr.application` no longer has an ASP.NET Core framework reference. It may reference general `Microsoft.Extensions.*` abstractions for logging, options, caching, dependency-factory access, and HTTP client factories, but it should not reference host/web implementation packages. + +## Migration Direction + +Use this pattern when moving a concern out of application: + +1. Keep the application-level interface, DTOs, and result models in `listenarr.application` or `listenarr.domain`. +2. Move the concrete implementation to the appropriate `listenarr.infrastructure` feature or technology folder. +3. Register the implementation in `listenarr.infrastructure/Extensions/InfrastructureServiceRegistrationExtensions.cs`. +4. Keep `listenarr.api` responsible for calling the registration extension and composing the host. +5. Add or update focused tests before deleting the old implementation. + +Recommended follow-up slices: + +- Revisit background workers that combine orchestration with persistence or filesystem details and split the use case from the hosted adapter. +- Continue replacing direct service-locator patterns with narrower application ports where a worker or service only needs one operation from another layer. +- Keep new host-specific concerns in API or infrastructure and expose them to application through small application-owned contracts. + +Until those slices are complete, reviewers should treat any new infrastructure-shaped application dependency as a boundary regression unless it is explicitly documented. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 114a0a08e..0796262d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -400,6 +400,7 @@ If you have any questions about contributing, please: --- ## Layering rules & migration steps (practical) +- Backend project boundaries are documented in `BACKEND_ARCHITECTURE.md`. Keep infrastructure-shaped dependencies out of `listenarr.application`; add an application-owned port and implement the adapter in infrastructure/API instead. - Keep contracts (interfaces, DTOs, domain models) in `listenarr.application` or `listenarr.domain`. - Keep framework-dependent implementations (EF Core, HttpClients, filesystem) in `listenarr.infrastructure`. - `listenarr.api` should only compose services, host controllers, and register DI; do not add new interfaces that duplicate application/infrastructure contracts. diff --git a/Directory.Packages.props b/Directory.Packages.props index e66d165b7..fb7d3f150 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,7 +20,12 @@ + + + + + diff --git a/listenarr.api/Attributes/LocalOrAdminAttribute.cs b/listenarr.api/Attributes/LocalOrAdminAttribute.cs index 71f8903d5..8b1e87141 100644 --- a/listenarr.api/Attributes/LocalOrAdminAttribute.cs +++ b/listenarr.api/Attributes/LocalOrAdminAttribute.cs @@ -1,4 +1,3 @@ -using Listenarr.Application.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -9,8 +8,8 @@ public class LocalOrAdminAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { - var isLoopback = SecurityRequestUtils.IsLoopbackRequest(context.HttpContext); - var isAuth = SecurityRequestUtils.IsAuthenticatedAdminOrApiKey(context.HttpContext); + var isLoopback = HttpSecurityRequestUtils.IsLoopbackRequest(context.HttpContext); + var isAuth = HttpSecurityRequestUtils.IsAuthenticatedAdminOrApiKey(context.HttpContext); if (!isLoopback && !isAuth) { diff --git a/listenarr.api/Attributes/RequireAdminOrApiKeyAttribute.cs b/listenarr.api/Attributes/RequireAdminOrApiKeyAttribute.cs index 51415d3ae..9d447e63e 100644 --- a/listenarr.api/Attributes/RequireAdminOrApiKeyAttribute.cs +++ b/listenarr.api/Attributes/RequireAdminOrApiKeyAttribute.cs @@ -17,7 +17,6 @@ */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -59,7 +58,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE } if (!_startupConfigService.IsAuthenticationRequired() || - SecurityRequestUtils.IsAuthenticatedAdminOrApiKey(context.HttpContext)) + HttpSecurityRequestUtils.IsAuthenticatedAdminOrApiKey(context.HttpContext)) { await next(); return; diff --git a/listenarr.api/Attributes/RequireAdministratorSessionAttribute.cs b/listenarr.api/Attributes/RequireAdministratorSessionAttribute.cs index 10ff53e2a..e888f4cc8 100644 --- a/listenarr.api/Attributes/RequireAdministratorSessionAttribute.cs +++ b/listenarr.api/Attributes/RequireAdministratorSessionAttribute.cs @@ -17,7 +17,6 @@ */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -60,7 +59,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE var user = context.HttpContext.User; if (user?.Identity?.IsAuthenticated == true && user.IsInRole("Administrator") && - !SecurityRequestUtils.IsApiKeyAuthenticated(context.HttpContext)) + !HttpSecurityRequestUtils.IsApiKeyAuthenticated(context.HttpContext)) { await next(); return; diff --git a/listenarr.api/Attributes/RequireApiKeyAttribute.cs b/listenarr.api/Attributes/RequireApiKeyAttribute.cs index 3106b9c8b..0a395b5a0 100644 --- a/listenarr.api/Attributes/RequireApiKeyAttribute.cs +++ b/listenarr.api/Attributes/RequireApiKeyAttribute.cs @@ -17,7 +17,6 @@ */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -56,7 +55,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE return; } - if (SecurityRequestUtils.IsApiKeyAuthenticated(context.HttpContext)) + if (HttpSecurityRequestUtils.IsApiKeyAuthenticated(context.HttpContext)) { await next(); return; diff --git a/listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs b/listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs index f96865812..f6e248e25 100644 --- a/listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs +++ b/listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs @@ -17,7 +17,6 @@ */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -56,7 +55,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE var user = httpContext.User; if (user?.Identity?.IsAuthenticated == true && user.IsInRole("Administrator") && - !SecurityRequestUtils.IsApiKeyAuthenticated(httpContext)) + !HttpSecurityRequestUtils.IsApiKeyAuthenticated(httpContext)) { await next(); return; @@ -68,7 +67,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE return; } - if (SecurityRequestUtils.IsLocalOrPrivateRequest(httpContext)) + if (HttpSecurityRequestUtils.IsLocalOrPrivateRequest(httpContext)) { await next(); return; diff --git a/listenarr.api/Common/ApiVersionHttpContextExtensions.cs b/listenarr.api/Common/ApiVersionHttpContextExtensions.cs new file mode 100644 index 000000000..72ec520cc --- /dev/null +++ b/listenarr.api/Common/ApiVersionHttpContextExtensions.cs @@ -0,0 +1,46 @@ +/* + * 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. + */ +using Listenarr.Domain.Common; + +namespace Listenarr.Api.Common +{ + public static class HttpApiVersionUtils + { + public static string ResolveApiVersion(HttpContext? context, string? fallbackVersion = null, ILogger? logger = null) + { + try + { + if (context?.Request?.RouteValues?.TryGetValue("version", out var routeVersionObj) is true) + { + var routeVersion = routeVersionObj?.ToString(); + if (!string.IsNullOrWhiteSpace(routeVersion)) + { + return ApiVersionNormalizer.NormalizeOrDefault(routeVersion); + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger?.LogWarning(ex, "API version route parse failed."); + } + + return Listenarr.Application.Common.ApiVersionUtils.ResolveApiVersion(context?.Request?.Path.Value, fallbackVersion, logger); + } + + public static string GetApiVersionSegment(HttpContext? context, string? fallbackVersion = null) + => $"v{ResolveApiVersion(context, fallbackVersion)}"; + + public static string BuildApiPath(string endpoint, HttpContext? context, string? fallbackVersion = null) + => Listenarr.Application.Common.ApiVersionUtils.BuildApiPath(endpoint, context?.Request?.Path.Value, fallbackVersion); + + public static string BuildImagePath(string identifier, HttpContext? context, string? fallbackVersion = null, string? sourceUrl = null) + => Listenarr.Application.Common.ApiVersionUtils.BuildImagePath(identifier, context?.Request?.Path.Value, fallbackVersion, sourceUrl); + } +} diff --git a/listenarr.api/Controllers/AntiforgeryController.cs b/listenarr.api/Controllers/AntiforgeryController.cs index 3f51854e9..657c9d500 100644 --- a/listenarr.api/Controllers/AntiforgeryController.cs +++ b/listenarr.api/Controllers/AntiforgeryController.cs @@ -75,4 +75,3 @@ public IActionResult GetToken() } } } - diff --git a/listenarr.api/Controllers/Configurations/ApiSourcesController.cs b/listenarr.api/Controllers/Configurations/ApiSourcesController.cs index afe2541ec..f4f9215aa 100644 --- a/listenarr.api/Controllers/Configurations/ApiSourcesController.cs +++ b/listenarr.api/Controllers/Configurations/ApiSourcesController.cs @@ -52,7 +52,7 @@ public async Task>> GetApiConfigurations() try { var configs = await _configurationService.GetApiConfigurationsAsync(); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { configs = configs.Select(ApiResponseRedactor.RedactApiConfiguration).ToList(); } @@ -83,7 +83,7 @@ public async Task> GetApiConfiguration(string id) { return NotFound(); } - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { return Ok(ApiResponseRedactor.RedactApiConfiguration(config)); } diff --git a/listenarr.api/Controllers/Configurations/SettingsController.cs b/listenarr.api/Controllers/Configurations/SettingsController.cs index 42a036795..0d00f0633 100644 --- a/listenarr.api/Controllers/Configurations/SettingsController.cs +++ b/listenarr.api/Controllers/Configurations/SettingsController.cs @@ -18,12 +18,10 @@ using Listenarr.Api.Attributes; using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; using Listenarr.Application.Security; using Listenarr.Domain.Models; using Listenarr.Domain.Models.Configurations; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using System.Text.Json; namespace Listenarr.Api.Controllers.Configurations @@ -35,16 +33,16 @@ public class SettingsController : ControllerBase { private readonly IConfigurationService _configurationService; private readonly ILogger _logger; - private readonly IHubContext _settingsHub; + private readonly IHubBroadcaster _hubBroadcaster; public SettingsController( IConfigurationService configurationService, ILogger logger, - IHubContext settingsHub) + IHubBroadcaster hubBroadcaster) { _configurationService = configurationService; _logger = logger; - _settingsHub = settingsHub; + _hubBroadcaster = hubBroadcaster; } /// @@ -57,7 +55,7 @@ public async Task> GetApplicationSettings() try { var settings = PrepareApplicationSettingsResponse(await _configurationService.GetApplicationSettingsAsync()); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { return Ok(ApiResponseRedactor.RedactApplicationSettings(settings)); } @@ -72,7 +70,7 @@ public async Task> GetApplicationSettings() } /// - /// Save application settings. Broadcasts the update to all connected clients via SignalR. + /// Save application settings. Broadcasts the update to all connected realtime clients. /// /// Updated application settings. [Tags("Settings")] @@ -88,10 +86,13 @@ public async Task> SaveApplicationSettings([Fr savedSettings.AdminUsername = null; savedSettings.AdminPassword = null; - await _settingsHub.Clients.All.SendAsync("SettingsUpdated", ApiResponseRedactor.RedactApplicationSettings(savedSettings)); + await _hubBroadcaster.BroadcastAsync( + RealtimeHubTarget.Settings, + "SettingsUpdated", + ApiResponseRedactor.RedactApplicationSettings(savedSettings)); - _logger.LogDebug("Application settings saved successfully and broadcasted via SignalR"); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + _logger.LogDebug("Application settings saved successfully and broadcasted to realtime clients"); + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { return Ok(ApiResponseRedactor.RedactApplicationSettings(savedSettings)); } diff --git a/listenarr.api/Controllers/Configurations/StartupConfigurationController.cs b/listenarr.api/Controllers/Configurations/StartupConfigurationController.cs index 393482b57..7a9b3be46 100644 --- a/listenarr.api/Controllers/Configurations/StartupConfigurationController.cs +++ b/listenarr.api/Controllers/Configurations/StartupConfigurationController.cs @@ -75,7 +75,7 @@ public async Task> GetStartupConfig() { var config = await _configurationService.GetStartupConfigAsync() ?? new StartupConfig(); config.ApiVersion = NormalizeStartupApiVersion(config.ApiVersion); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { config = ApiResponseRedactor.RedactStartupConfig(config); } @@ -105,7 +105,7 @@ public async Task> SaveStartupConfig([FromBody] Star } savedConfig.ApiVersion = NormalizeStartupApiVersion(savedConfig.ApiVersion); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { return Ok(ApiResponseRedactor.RedactStartupConfig(savedConfig)); } diff --git a/listenarr.api/Controllers/DownloadClientController.cs b/listenarr.api/Controllers/DownloadClientController.cs index 75e7d6d5c..d814b6252 100644 --- a/listenarr.api/Controllers/DownloadClientController.cs +++ b/listenarr.api/Controllers/DownloadClientController.cs @@ -55,7 +55,7 @@ public async Task>> GetDownloadCl try { var configs = await _configurationService.GetDownloadClientConfigurationsAsync(); - var redactSecrets = SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext); + var redactSecrets = HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext); var response = configs .Select(c => redactSecrets ? ApiResponseRedactor.RedactDownloadClientConfiguration(c) : c) .Select(ApiResponseRedactor.ToDownloadClientSummaryResponse) @@ -88,7 +88,7 @@ public async Task> GetDownloadClientCo return NotFound(); } - var responseConfig = SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext) + var responseConfig = HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext) ? ApiResponseRedactor.RedactDownloadClientConfiguration(config) : config; var response = ApiResponseRedactor.ToDownloadClientDetailResponse(responseConfig); @@ -248,7 +248,7 @@ public async Task> TestDownloadClientConfiguration([FromBod var (success, message) = await _downloadClientGateway.TestConnectionAsync(config); var clientResponse = config; - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { clientResponse = ApiResponseRedactor.RedactDownloadClientConfiguration(clientResponse); } diff --git a/listenarr.api/Controllers/DownloadController.cs b/listenarr.api/Controllers/DownloadController.cs index 969d3913e..2c7eadffe 100644 --- a/listenarr.api/Controllers/DownloadController.cs +++ b/listenarr.api/Controllers/DownloadController.cs @@ -347,5 +347,3 @@ public class ReprocessAllRequest public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30); } } - - diff --git a/listenarr.api/Controllers/DownloadsController.cs b/listenarr.api/Controllers/DownloadsController.cs index a966b807c..ec73ae552 100644 --- a/listenarr.api/Controllers/DownloadsController.cs +++ b/listenarr.api/Controllers/DownloadsController.cs @@ -377,5 +377,3 @@ private async Task> EnhanceDownloadsWithClientNames(List }).Cast().ToList(); } } - - diff --git a/listenarr.api/Controllers/FfmpegController.cs b/listenarr.api/Controllers/FfmpegController.cs index 5fe35ab8c..89537e643 100644 --- a/listenarr.api/Controllers/FfmpegController.cs +++ b/listenarr.api/Controllers/FfmpegController.cs @@ -119,5 +119,3 @@ public async Task RunFfprobe([FromBody] FfprobeScanRequest req) } } } - - diff --git a/listenarr.api/Controllers/FileSystemController.cs b/listenarr.api/Controllers/FileSystemController.cs index 3827cec44..3571dfa0b 100644 --- a/listenarr.api/Controllers/FileSystemController.cs +++ b/listenarr.api/Controllers/FileSystemController.cs @@ -312,5 +312,3 @@ public class VolumeCheckResponse public string? DestVolume { get; set; } public string? Message { get; set; } } - - diff --git a/listenarr.api/Controllers/IndexersController.cs b/listenarr.api/Controllers/IndexersController.cs index 1d5059a71..533f0a9b2 100644 --- a/listenarr.api/Controllers/IndexersController.cs +++ b/listenarr.api/Controllers/IndexersController.cs @@ -18,7 +18,6 @@ using Listenarr.Api.Attributes; using Listenarr.Api.Dtos; -using Listenarr.Application.Common; using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Security; @@ -50,7 +49,7 @@ public IndexersController(IIndexerRepository indexerRepository, ILogger SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext); + => HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext); private Indexer RedactIndexerForCaller(Indexer indexer) => ShouldRedactIndexerSecretsForCaller() ? ApiResponseRedactor.RedactIndexer(indexer) : indexer; @@ -1048,7 +1047,7 @@ public async Task DebugMyAnonamouseSearch(int id, [FromBody] Json { var scheme = Request.Scheme; var hostVal = Request.Host.Value; - var localSearchUrl = $"{scheme}://{hostVal}{ApiVersionUtils.BuildApiPath($"/search/{id}", HttpContext)}?query={Uri.EscapeDataString(query)}"; + var localSearchUrl = $"{scheme}://{hostVal}{HttpApiVersionUtils.BuildApiPath($"/search/{id}", HttpContext)}?query={Uri.EscapeDataString(query)}"; using var localResp = await _httpClient.GetAsync(localSearchUrl); if (localResp.IsSuccessStatusCode) { diff --git a/listenarr.api/Controllers/LibraryController.cs b/listenarr.api/Controllers/LibraryController.cs index ed1d665b1..b9ab588b4 100644 --- a/listenarr.api/Controllers/LibraryController.cs +++ b/listenarr.api/Controllers/LibraryController.cs @@ -32,7 +32,6 @@ using Listenarr.Application.Security; using Listenarr.Application.Metadata; using Listenarr.Application.Search; -using Microsoft.AspNetCore.SignalR; using Listenarr.Application.Audiobooks; using Listenarr.Api.Attributes; @@ -2314,13 +2313,13 @@ public async Task ScanAudiobookFiles(int id, [FromBody] ScanReque var jobId = await _scanQueueService.EnqueueScanAsync(audiobook, request?.Path); _logger.LogInformation("Enqueued scan job {JobId} for audiobook {AudiobookId}", jobId, id); - // Broadcast initial job status via SignalR so clients can show queued state + // Broadcast initial job status so realtime clients can show queued state try { using var scope = _scopeFactory.CreateScope(); - var hub = scope.ServiceProvider.GetRequiredService>(); + var hub = scope.ServiceProvider.GetRequiredService(); var job = new { jobId = jobId.ToString(), audiobookId = id, status = "Queued", enqueuedAt = DateTime.UtcNow }; - await hub.Clients.All.SendAsync("ScanJobUpdate", job); + await hub.BroadcastAsync(RealtimeHubTarget.Downloads, "ScanJobUpdate", job); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -2907,9 +2906,9 @@ normalizeEx is ArgumentException try { using var hubScope = _scopeFactory.CreateScope(); - var hub = hubScope.ServiceProvider.GetRequiredService>(); + var hub = hubScope.ServiceProvider.GetRequiredService(); var job = new { jobId = jobId.ToString(), audiobookId = id, status = "Queued", enqueuedAt = DateTime.UtcNow }; - await hub.Clients.All.SendAsync("MoveJobUpdate", job); + await hub.BroadcastAsync(RealtimeHubTarget.Downloads, "MoveJobUpdate", job); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -2962,9 +2961,9 @@ public async Task RequeueMoveJob(string jobId) try { using var scope = _scopeFactory.CreateScope(); - var hub = scope.ServiceProvider.GetRequiredService>(); + var hub = scope.ServiceProvider.GetRequiredService(); var job = new { jobId = newJobId.ToString(), status = "Queued", enqueuedAt = DateTime.UtcNow }; - await hub.Clients.All.SendAsync("MoveJobUpdate", job); + await hub.BroadcastAsync(RealtimeHubTarget.Downloads, "MoveJobUpdate", job); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -2994,9 +2993,9 @@ public async Task RequeueScanJob(string jobId) try { using var scope = _scopeFactory.CreateScope(); - var hub = scope.ServiceProvider.GetRequiredService>(); + var hub = scope.ServiceProvider.GetRequiredService(); var job = new { jobId = newJobId.ToString(), status = "Queued", enqueuedAt = DateTime.UtcNow }; - await hub.Clients.All.SendAsync("ScanJobUpdate", job); + await hub.BroadcastAsync(RealtimeHubTarget.Downloads, "ScanJobUpdate", job); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -3044,9 +3043,9 @@ private async Task ProcessAudiobookForSearchAsync( }).ToList(); using var scope = _scopeFactory.CreateScope(); - var hub = scope.ServiceProvider.GetRequiredService>(); + var hub = scope.ServiceProvider.GetRequiredService(); // Include a structured payload so clients can distinguish manual vs automatic searches - await hub.Clients.All.SendCoreAsync("SearchProgress", new object[] { new { message = $"Manual search query: {searchQuery}", details = new { rawCount = searchResults.Count, rawSamples = rawSummaries }, type = "interactive", audiobookId = audiobook.Id } }); + await hub.BroadcastAsync(RealtimeHubTarget.Downloads, "SearchProgress", new { message = $"Manual search query: {searchQuery}", details = new { rawCount = searchResults.Count, rawSamples = rawSummaries }, type = "interactive", audiobookId = audiobook.Id }); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { diff --git a/listenarr.api/Controllers/ProwlarrCompatController.cs b/listenarr.api/Controllers/ProwlarrCompatController.cs index 43d333856..0596e10aa 100644 --- a/listenarr.api/Controllers/ProwlarrCompatController.cs +++ b/listenarr.api/Controllers/ProwlarrCompatController.cs @@ -20,8 +20,6 @@ using Listenarr.Application.Interfaces; using Listenarr.Domain.Models; using Listenarr.Application.Interfaces.Repositories; -using Microsoft.AspNetCore.SignalR; -using Listenarr.Application.Notification; using Listenarr.Application.Security; using Listenarr.Api.Attributes; @@ -51,7 +49,8 @@ private StartupConfig GetStartupConfig() private readonly ILogger _logger; private readonly IIndexerRepository _indexerRepository; - private readonly IHubContext _settingsHub; + private readonly IHubBroadcaster _hubBroadcaster; + private readonly IRealtimeClientRegistry _realtimeClientRegistry; private readonly IToastService _toastService; private readonly IStartupConfigService _startupConfigService; private readonly IApplicationVersionService _applicationVersionService; @@ -109,14 +108,16 @@ private static bool ShouldSendToastForMessage(string message) public ProwlarrCompatController( ILogger logger, IIndexerRepository indexerRepository, - IHubContext settingsHub, + IHubBroadcaster hubBroadcaster, + IRealtimeClientRegistry realtimeClientRegistry, IToastService toastService, IStartupConfigService startupConfigService, IApplicationVersionService applicationVersionService) { _logger = logger; _indexerRepository = indexerRepository; - _settingsHub = settingsHub; + _hubBroadcaster = hubBroadcaster; + _realtimeClientRegistry = realtimeClientRegistry; _toastService = toastService; _startupConfigService = startupConfigService; _applicationVersionService = applicationVersionService; @@ -347,7 +348,7 @@ public async Task GetIndexersList() .ThenBy(i => i.Name) .ToList(); - if (SecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) + if (HttpSecurityRequestUtils.ShouldRedactSecretsForCaller(HttpContext)) { indexers = indexers.Select(ApiResponseRedactor.RedactIndexer).ToList(); } @@ -383,7 +384,7 @@ public async Task DeleteIndexer(int id) _logger?.LogInformation("Prowlarr: Deleted indexer {Id} (name={Name})", i.Id, i.Name); try { - await _settingsHub.Clients.All.SendAsync("IndexersUpdated", new { created = 0, skipped = 0, indexers = new[] { new { id = i.Id, name = i.Name, baseUrl = i.Url } } }); + await _hubBroadcaster.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", new { created = 0, skipped = 0, indexers = new[] { new { id = i.Id, name = i.Name, baseUrl = i.Url } } }); var deleteMessage = $"Removed indexer: {i.Name}"; if (ShouldSendToastForIndexer(i.Id, deleteMessage) && ShouldSendToastForMessage(deleteMessage)) { @@ -651,7 +652,7 @@ public async Task PutIndexer(int id, [FromBody] System.Text.Json. { var stillExists = (await _indexerRepository.GetByIdAsync(indexer.Id)) != null; var createdForBroadcast = (created && stillExists) ? 1 : 0; - await _settingsHub.Clients.All.SendAsync("IndexersUpdated", new { created = createdForBroadcast, skipped = 0, indexers = new[] { new { id = indexer.Id, name = indexer.Name, baseUrl = indexer.Url } } }); + await _hubBroadcaster.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", new { created = createdForBroadcast, skipped = 0, indexers = new[] { new { id = indexer.Id, name = indexer.Name, baseUrl = indexer.Url } } }); // Determine toast message. If the indexer was created very recently (by a prior POST or PUT), // suppress an additional 'Updated' toast to avoid duplicate notifications for rapid import/update flows. @@ -980,7 +981,7 @@ string getString(System.Text.Json.JsonElement el, string prop1, string? prop2 = _logger?.LogInformation("Broadcasting IndexersUpdated to clients: created={Created}, skipped={Skipped}, indexerCount={Count}", created, skipped, createdInfo.Length); - await _settingsHub.Clients.All.SendAsync("IndexersUpdated", new { created, skipped, indexers = createdInfo }); + await _hubBroadcaster.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", new { created, skipped, indexers = createdInfo }); _logger?.LogInformation("IndexersUpdated broadcast complete"); @@ -1005,7 +1006,7 @@ string getString(System.Text.Json.JsonElement el, string prop1, string? prop2 = } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { - _logger?.LogWarning(ex, "Failed to broadcast IndexersUpdated via SignalR"); + _logger?.LogWarning(ex, "Failed to broadcast IndexersUpdated to realtime clients"); } } @@ -1044,7 +1045,7 @@ string getString(System.Text.Json.JsonElement el, string prop1, string? prop2 = /// /// DEBUG: POST /api/v1/debug/indexers/publish - /// Manually trigger an IndexersUpdated SignalR broadcast for testing client connectivity. + /// Manually trigger an IndexersUpdated realtime broadcast for testing client connectivity. /// [HttpPost("debug/indexers/publish")] [AllowAnonymous] @@ -1088,7 +1089,7 @@ public async Task DebugPublishIndexers([FromBody] System.Text.Jso _logger?.LogInformation("DEBUG: Broadcasting IndexersUpdated (manual test): created={Created}", created); - await _settingsHub.Clients.All.SendAsync("IndexersUpdated", new { created, skipped = 0, indexers }); + await _hubBroadcaster.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", new { created, skipped = 0, indexers }); _logger?.LogInformation("DEBUG: IndexersUpdated broadcast sent"); @@ -1109,22 +1110,22 @@ public async Task DebugPublishIndexers([FromBody] System.Text.Jso /// /// DEBUG: GET /api/v1/debug/settings/clients - /// Returns the list and count of currently connected SettingsHub clients. + /// Returns the list and count of currently connected settings realtime clients. /// [HttpGet("debug/settings/clients")] [AllowAnonymous] [LocalOrAdmin] [ApiExplorerSettings(IgnoreApi = true)] - public IActionResult GetSettingsHubClients() + public IActionResult GetSettingsRealtimeClients() { try { - var clients = SettingsHub.ConnectedClientIds.ToArray(); - return Ok(new { connected = clients.Length, clients }); + var clients = _realtimeClientRegistry.GetSettingsClientIds(); + return Ok(new { connected = clients.Count, clients }); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { - _logger?.LogWarning(ex, "Failed to retrieve SettingsHub clients"); + _logger?.LogWarning(ex, "Failed to retrieve settings realtime clients"); return StatusCode(500, new { error = "Failed to retrieve clients" }); } } @@ -1132,7 +1133,7 @@ public IActionResult GetSettingsHubClients() /// /// POST /api/v1/indexer /// Accepts a single indexer object (or an array) for compatibility with some clients that POST to the singular route. - /// Delegates to PostIndexers for the actual processing so persistence and SignalR broadcast happen in one place. + /// Delegates to PostIndexers for the actual processing so persistence and realtime broadcasts happen in one place. /// [HttpPost("indexer")] [AllowAnonymous] diff --git a/listenarr.api/Controllers/QualityProfileController.cs b/listenarr.api/Controllers/QualityProfileController.cs index b990ce8e3..b68f95617 100644 --- a/listenarr.api/Controllers/QualityProfileController.cs +++ b/listenarr.api/Controllers/QualityProfileController.cs @@ -217,5 +217,3 @@ public async Task>> ScoreResults( } } } - - diff --git a/listenarr.api/Controllers/RemotePathMappingsController.cs b/listenarr.api/Controllers/RemotePathMappingsController.cs index 2ec6dc708..0cc8322a9 100644 --- a/listenarr.api/Controllers/RemotePathMappingsController.cs +++ b/listenarr.api/Controllers/RemotePathMappingsController.cs @@ -256,5 +256,3 @@ public class TranslatePathRequest public string RemotePath { get; set; } = string.Empty; } } - - diff --git a/listenarr.api/Controllers/RootFoldersController.cs b/listenarr.api/Controllers/RootFoldersController.cs index 2997648f4..1eeb0f800 100644 --- a/listenarr.api/Controllers/RootFoldersController.cs +++ b/listenarr.api/Controllers/RootFoldersController.cs @@ -149,7 +149,7 @@ public async Task Delete(int id, [FromQuery] int? reassignTo = nu /// /// Enqueues a background scan of a root folder to find audio files not in the library. - /// Returns a jobId; subscribe to SignalR "UnmatchedScanComplete" for completion notification. + /// Returns a jobId; subscribe to the realtime "UnmatchedScanComplete" event for completion notification. /// [HttpPost("{id}/scan-unmatched")] public async Task ScanUnmatched(int id) diff --git a/listenarr.api/Controllers/SearchController.cs b/listenarr.api/Controllers/SearchController.cs index fefe6e9d7..c0c0f3976 100644 --- a/listenarr.api/Controllers/SearchController.cs +++ b/listenarr.api/Controllers/SearchController.cs @@ -20,7 +20,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; -using Listenarr.Application.Common; using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; using Listenarr.Application.Search; @@ -58,7 +57,7 @@ public SearchController( } private string BuildApiImagePath(string identifier, string? sourceUrl = null) - => ApiVersionUtils.BuildImagePath(identifier, HttpContext, sourceUrl: sourceUrl); + => HttpApiVersionUtils.BuildImagePath(identifier, HttpContext, sourceUrl: sourceUrl); private static string? NormalizeStructuredAdvancedField(string? value, string prefix) { @@ -1448,4 +1447,3 @@ public async Task> SearchByApi( } } } - diff --git a/listenarr.api/GlobalUsings.cs b/listenarr.api/GlobalUsings.cs new file mode 100644 index 000000000..66c625839 --- /dev/null +++ b/listenarr.api/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using Microsoft.Extensions.Hosting; +global using Listenarr.Api.Security; +global using Listenarr.Api.Common; diff --git a/listenarr.api/Middleware/AntiforgeryValidationMiddleware.cs b/listenarr.api/Middleware/AntiforgeryValidationMiddleware.cs index 493c79b17..eb3fb3ff8 100644 --- a/listenarr.api/Middleware/AntiforgeryValidationMiddleware.cs +++ b/listenarr.api/Middleware/AntiforgeryValidationMiddleware.cs @@ -187,4 +187,3 @@ private static bool IsVersionedIndexerOrSystemPath(string path) => !string.IsNullOrWhiteSpace(path) && VersionedIndexerOrSystemPathRegex.IsMatch(path); } } - diff --git a/listenarr.api/Middleware/ApiKeyMiddleware.cs b/listenarr.api/Middleware/ApiKeyMiddleware.cs index e26b98063..34133ae2b 100644 --- a/listenarr.api/Middleware/ApiKeyMiddleware.cs +++ b/listenarr.api/Middleware/ApiKeyMiddleware.cs @@ -59,7 +59,7 @@ public async Task InvokeAsync(HttpContext context) provided = s.Substring("ApiKey ".Length).Trim(); } - // If headers didn't supply the key, only accept query-string token for SignalR hub connections. + // If headers didn't supply the key, only accept query-string token for realtime hub connections. // Avoiding query-string auth for normal API routes prevents credential leakage via logs/referrers. if (string.IsNullOrWhiteSpace(provided)) { diff --git a/listenarr.api/Middleware/AuthenticationEnforcerMiddleware.cs b/listenarr.api/Middleware/AuthenticationEnforcerMiddleware.cs index d8644f7c9..49bcb9445 100644 --- a/listenarr.api/Middleware/AuthenticationEnforcerMiddleware.cs +++ b/listenarr.api/Middleware/AuthenticationEnforcerMiddleware.cs @@ -92,7 +92,7 @@ public async Task InvokeAsync(HttpContext context) return; } - // Serve SPA assets and client-side routes anonymously: if the request is not for an API or SignalR hub, + // Serve SPA assets and client-side routes anonymously: if the request is not for an API or realtime hub, // let the static file middleware or SPA fallback handle it. This avoids returning 401 for '/'. // Keep API and hub routes protected. if (!path.StartsWith("/api") && !path.StartsWith("/hubs")) diff --git a/listenarr.api/Middleware/RequestBodyLoggingMiddleware.cs b/listenarr.api/Middleware/RequestBodyLoggingMiddleware.cs index 7863ea9dc..1b3be130f 100644 --- a/listenarr.api/Middleware/RequestBodyLoggingMiddleware.cs +++ b/listenarr.api/Middleware/RequestBodyLoggingMiddleware.cs @@ -119,4 +119,3 @@ private static string RedactSensitiveJsonFields(string input) } } } - diff --git a/listenarr.api/Program.Testing.cs b/listenarr.api/Program.Testing.cs index 55e6fafa6..741576b99 100644 --- a/listenarr.api/Program.Testing.cs +++ b/listenarr.api/Program.Testing.cs @@ -27,4 +27,3 @@ static partial void ApplyTestHostPatches(WebApplicationBuilder builder) builder.Configuration.AddInMemoryCollection(inMemory); } } - diff --git a/listenarr.api/Program.cs b/listenarr.api/Program.cs index c32ee465e..0e8342ad7 100644 --- a/listenarr.api/Program.cs +++ b/listenarr.api/Program.cs @@ -29,7 +29,6 @@ using Serilog.Events; using Listenarr.Infrastructure.Extensions; using Listenarr.Application.Interfaces; -using Listenarr.Infrastructure.SignalR; using Listenarr.Application.Downloads; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Common; @@ -42,7 +41,6 @@ using Listenarr.Domain.Models.Configurations; using Listenarr.Application.Audiobooks; using Listenarr.Infrastructure.Persistence.Repositories; -using Microsoft.AspNetCore.SignalR; using Listenarr.Api.Middleware; using Listenarr.Api.Filters; using System.Text.Json.Serialization; @@ -110,7 +108,7 @@ // Configure Serilog for structured logging, file rotation and SignalR broadcasting var logFilePath = Path.Join(builder.Environment.ContentRootPath, "config", "logs", "listenarr-.log"); -var signalRSink = new SignalRLogSink(); +var signalRSink = RealtimeLoggingExtensions.CreateListenarrRealtimeLogSink(); // Prefer explicit environment variable (useful for Docker/runtime overrides) var logLevelEnv = Environment.GetEnvironmentVariable("LISTENARR_LOG_LEVEL"); @@ -417,9 +415,6 @@ ex is IOException builder.Services.AddListenarrHostedServices(builder.Configuration); } -// FIXME: Required for ConfigurationService, what was planned with this feature ? -builder.Services.AddSingleton(new EphemeralDataProtectionProvider().CreateProtector("Listenarr.ConfigurationService.ProwlarrImport")); - // Startup DB normalizer: run once at startup to idempotently normalize legacy JSON columns builder.Services.AddHostedService(); // External request options (Prefer US domain / optional US proxy) @@ -653,8 +648,8 @@ ex is IOException Log.Logger.Debug(ex, "[Startup] Failed to evaluate authentication-enabled startup warning"); } -// Initialize the SignalR sink now that the hub context is available -signalRSink.Initialize(app.Services.GetRequiredService>()); +// Initialize realtime log broadcasting now that the hub context is available. +signalRSink.InitializeListenarrRealtimeLogging(app.Services); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) @@ -775,23 +770,7 @@ ex is IOException app.UseAuthorization(); app.MapControllers(); - -// Map SignalR hub for real-time download updates -if (app.Environment.IsDevelopment()) -{ - app.MapHub("/hubs/downloads").RequireCors("DevOnly"); - // Map SignalR hub for real-time log broadcasting - app.MapHub("/hubs/logs").RequireCors("DevOnly"); - // Map SignalR hub for real-time settings updates - app.MapHub("/hubs/settings").RequireCors("DevOnly"); -} -else -{ - app.MapHub("/hubs/downloads"); - app.MapHub("/hubs/logs"); - // Map SignalR hub for real-time settings updates - app.MapHub("/hubs/settings"); -} +app.MapListenarrRealtimeHubs(app.Environment); // SPA fallback: serve index.html for non-API routes so client-side routing works app.MapFallbackToFile("index.html"); diff --git a/listenarr.api/Security/SecurityRequestHttpContextExtensions.cs b/listenarr.api/Security/SecurityRequestHttpContextExtensions.cs new file mode 100644 index 000000000..3a019d985 --- /dev/null +++ b/listenarr.api/Security/SecurityRequestHttpContextExtensions.cs @@ -0,0 +1,76 @@ +/* + * 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. + */ +using System.Net; + +namespace Listenarr.Api.Security; + +public static class HttpSecurityRequestUtils +{ + public static bool IsLoopbackRequest(HttpContext? context) + { + var ip = context?.Connection?.RemoteIpAddress; + if (ip == null) + { + return true; + } + + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return IPAddress.IsLoopback(ip); + } + + public static bool IsLocalOrPrivateRequest(HttpContext? context) + { + var ip = context?.Connection?.RemoteIpAddress; + if (ip == null) + { + return true; + } + + return Listenarr.Application.Security.SecurityRequestUtils.IsPrivateOrLoopback(ip); + } + + public static bool IsAuthenticatedAdminOrApiKey(HttpContext? context) + { + var user = context?.User; + if (user?.Identity?.IsAuthenticated != true) + { + return false; + } + + if (user.IsInRole("Administrator")) + { + return true; + } + + var authMethod = user.FindFirst("AuthMethod")?.Value; + return !string.IsNullOrWhiteSpace(authMethod) + && string.Equals(authMethod, "ApiKey", StringComparison.Ordinal); + } + + public static bool IsApiKeyAuthenticated(HttpContext? context) + { + var user = context?.User; + if (user?.Identity?.IsAuthenticated != true) + { + return false; + } + + var authMethod = user.FindFirst("AuthMethod")?.Value; + return !string.IsNullOrWhiteSpace(authMethod) + && string.Equals(authMethod, "ApiKey", StringComparison.Ordinal); + } + + public static bool ShouldRedactSecretsForCaller(HttpContext? context) + => !IsLocalOrPrivateRequest(context) && !IsAuthenticatedAdminOrApiKey(context); +} diff --git a/listenarr.application/Audiobooks/AudiobookFileService.cs b/listenarr.application/Audiobooks/AudiobookFileService.cs index b1ad2c65c..63d917653 100644 --- a/listenarr.application/Audiobooks/AudiobookFileService.cs +++ b/listenarr.application/Audiobooks/AudiobookFileService.cs @@ -15,9 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using System.Text.Json; +using Listenarr.Application.Common; using Listenarr.Domain.Common; using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; @@ -241,15 +241,14 @@ public async Task EnsureAudiobookFileAsync(Audiobook audiobook, string fil return true; } - catch (DbUpdateException dbEx) + catch (UniqueConstraintViolationException) + { + logger.LogInformation("AudiobookFile insertion conflict detected (likely already created): {Path}", LogRedaction.SanitizeFilePath(filePath)); + return false; + } + catch (PersistenceException dbEx) { attempts++; - var inner = dbEx.InnerException?.Message ?? dbEx.Message; - if (inner != null && inner.IndexOf("UNIQUE", StringComparison.OrdinalIgnoreCase) >= 0) - { - logger.LogInformation("AudiobookFile insertion conflict detected (likely already created): {Path}", LogRedaction.SanitizeFilePath(filePath)); - return false; - } if (attempts >= 3) { logger.LogWarning(dbEx, "Failed to save AudiobookFile after {Attempts} attempts: {Path}", attempts, LogRedaction.SanitizeFilePath(filePath)); diff --git a/listenarr.application/Audiobooks/MoveQueueService.cs b/listenarr.application/Audiobooks/MoveQueueService.cs index c7c9092ee..3bc1c73ee 100644 --- a/listenarr.application/Audiobooks/MoveQueueService.cs +++ b/listenarr.application/Audiobooks/MoveQueueService.cs @@ -19,10 +19,8 @@ using System.Threading.Channels; using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Notification; using Listenarr.Application.Security; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -129,10 +127,10 @@ public void UpdateJobStatus(Guid id, string status, string? error = null) moveJobRepository.UpdateAsync(dbJob).GetAwaiter().GetResult(); } - // Broadcast status update to SignalR clients so UI can react to Processing/Failed/Completed + // Broadcast status update to realtime clients so UI can react to Processing/Failed/Completed try { - var hub = scope.ServiceProvider.GetRequiredService>(); + var hub = scope.ServiceProvider.GetRequiredService(); var payload = new { jobId = id.ToString(), @@ -143,7 +141,7 @@ public void UpdateJobStatus(Guid id, string status, string? error = null) updatedAt = DateTime.UtcNow }; // Fire and forget but block briefly to surface errors during development - hub.Clients.All.SendAsync("MoveJobUpdate", payload).GetAwaiter().GetResult(); + hub.BroadcastAsync("MoveJobUpdate", payload).GetAwaiter().GetResult(); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -221,5 +219,3 @@ private static bool CanRequeueJobStatus(string status) } } } - - diff --git a/listenarr.application/Common/ApiVersionUtils.cs b/listenarr.application/Common/ApiVersionUtils.cs index 843c80900..cb313088f 100644 --- a/listenarr.application/Common/ApiVersionUtils.cs +++ b/listenarr.application/Common/ApiVersionUtils.cs @@ -17,7 +17,6 @@ */ using System.Text.RegularExpressions; using Listenarr.Domain.Common; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Common @@ -31,29 +30,12 @@ public static class ApiVersionUtils private static readonly Regex ApiVersionFromPathRegex = new(@"^/api/v(?\d+(?:\.\d+)?)(?:/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex LeadingApiPrefixRegex = new(@"^/api(?:/v\d+(?:\.\d+)?)?", RegexOptions.IgnoreCase | RegexOptions.Compiled); - public static string ResolveApiVersion(HttpContext? context, string? fallbackVersion = null, ILogger? logger = null) + public static string ResolveApiVersion(string? path = null, string? fallbackVersion = null, ILogger? logger = null) { var fallback = ApiVersionNormalizer.NormalizeOrDefault(fallbackVersion); try { - if (context?.Request?.RouteValues?.TryGetValue("version", out var routeVersionObj) is true) - { - var routeVersion = routeVersionObj?.ToString(); - if (!string.IsNullOrWhiteSpace(routeVersion)) - { - return ApiVersionNormalizer.NormalizeOrDefault(routeVersion); - } - } - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - logger?.LogWarning(ex, "API version route parse failed."); - } - - try - { - var path = context?.Request?.Path.Value; if (!string.IsNullOrWhiteSpace(path)) { var match = ApiVersionFromPathRegex.Match(path); @@ -75,19 +57,19 @@ public static string ResolveApiVersion(HttpContext? context, string? fallbackVer return fallback; } - public static string GetApiVersionSegment(HttpContext? context, string? fallbackVersion = null) - => $"v{ResolveApiVersion(context, fallbackVersion)}"; + public static string GetApiVersionSegment(string? path = null, string? fallbackVersion = null) + => $"v{ResolveApiVersion(path, fallbackVersion)}"; - public static string BuildApiPath(string endpoint, HttpContext? context = null, string? fallbackVersion = null) + public static string BuildApiPath(string endpoint, string? requestPath = null, string? fallbackVersion = null) { var normalizedEndpoint = NormalizeEndpoint(endpoint); - return $"/api/{GetApiVersionSegment(context, fallbackVersion)}{normalizedEndpoint}"; + return $"/api/{GetApiVersionSegment(requestPath, fallbackVersion)}{normalizedEndpoint}"; } - public static string BuildImagePath(string identifier, HttpContext? context = null, string? fallbackVersion = null, string? sourceUrl = null) + public static string BuildImagePath(string identifier, string? requestPath = null, string? fallbackVersion = null, string? sourceUrl = null) { var encodedIdentifier = Uri.EscapeDataString(identifier ?? string.Empty); - var path = BuildApiPath($"/images/{encodedIdentifier}", context, fallbackVersion); + var path = BuildApiPath($"/images/{encodedIdentifier}", requestPath, fallbackVersion); if (string.IsNullOrWhiteSpace(sourceUrl)) return path; return $"{path}?url={Uri.EscapeDataString(sourceUrl)}"; } diff --git a/listenarr.application/Common/ConfigurationService.cs b/listenarr.application/Common/ConfigurationService.cs index 82581298e..89a8c96ff 100644 --- a/listenarr.application/Common/ConfigurationService.cs +++ b/listenarr.application/Common/ConfigurationService.cs @@ -22,7 +22,6 @@ using Listenarr.Application.Security; using Listenarr.Domain.Models; using Listenarr.Domain.Models.Configurations; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Common @@ -35,7 +34,7 @@ public class ConfigurationService( IUserService userService, IStartupConfigService startupConfigService, IRootFolderRepository rootFolderRepository, - IDataProtector dataProtector) : IConfigurationService + ISecretProtector secretProtector) : IConfigurationService { // API Configuration methods public async Task> GetApiConfigurationsAsync() @@ -337,7 +336,7 @@ public async Task SaveProwlarrImportSettingsAs if (!string.IsNullOrWhiteSpace(settings.ApiKey) && !string.Equals(settings.ApiKey, ApiResponseRedactor.RedactedValue, StringComparison.Ordinal)) { - existing.ProwlarrApiKeyEncrypted = dataProtector.Protect(settings.ApiKey.Trim()); + existing.ProwlarrApiKeyEncrypted = secretProtector.Protect(settings.ApiKey.Trim()); } await settingsRepository.SaveAsync(existing); @@ -359,7 +358,7 @@ public async Task SaveProwlarrImportSettingsAs try { - return dataProtector.Unprotect(encryptedApiKey); + return secretProtector.Unprotect(encryptedApiKey); } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { diff --git a/listenarr.application/Common/PersistenceException.cs b/listenarr.application/Common/PersistenceException.cs new file mode 100644 index 000000000..30f2d0fc6 --- /dev/null +++ b/listenarr.application/Common/PersistenceException.cs @@ -0,0 +1,36 @@ +/* + * 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 . + */ + +namespace Listenarr.Application.Common +{ + public class PersistenceException : Exception + { + public PersistenceException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + public class UniqueConstraintViolationException : PersistenceException + { + public UniqueConstraintViolationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/listenarr.application/Common/StartupConfigService.cs b/listenarr.application/Common/StartupConfigService.cs index 98ebd8388..d8e1407cc 100644 --- a/listenarr.application/Common/StartupConfigService.cs +++ b/listenarr.application/Common/StartupConfigService.cs @@ -30,7 +30,7 @@ public class StartupConfigService : IStartupConfigService private readonly string _configPath; private StartupConfig? _config; - public StartupConfigService(ILogger logger, Microsoft.Extensions.Hosting.IHostEnvironment env) + public StartupConfigService(ILogger logger, IApplicationPathService applicationPathService) { _logger = logger; @@ -38,7 +38,7 @@ public StartupConfigService(ILogger logger, Microsoft.Exte // /listenarr.api/config/config.json for local development // so `npm run dev` uses the repo config file. Otherwise fall back to // content-root-based config (e.g., published/bin layouts). - var contentRoot = env.ContentRootPath ?? AppContext.BaseDirectory; + var contentRoot = applicationPathService.ContentRootPath ?? AppContext.BaseDirectory; // In Development, try to resolve the repository root from the current // working directory (most reliable when running via `npm run dev`). diff --git a/listenarr.application/Downloads/DownloadService.cs b/listenarr.application/Downloads/DownloadService.cs index d80d9d78b..4294d0eb8 100644 --- a/listenarr.application/Downloads/DownloadService.cs +++ b/listenarr.application/Downloads/DownloadService.cs @@ -534,7 +534,7 @@ await downloadHistoryService.RecordGrabbedAsync( await notificationService.SendNotificationAsync("book-downloading", notificationData, settings.WebhookUrl, settings.EnabledNotificationTriggers); - // Trigger immediate queue update via SignalR so the UI shows the new download right away + // Trigger an immediate realtime queue update so the UI shows the new download right away // Add a small delay to allow the download client to process and index the new download try { diff --git a/listenarr.application/Interfaces/IAudibleAuthorPageParser.cs b/listenarr.application/Interfaces/IAudibleAuthorPageParser.cs new file mode 100644 index 000000000..c6dff1672 --- /dev/null +++ b/listenarr.application/Interfaces/IAudibleAuthorPageParser.cs @@ -0,0 +1,27 @@ +/* + * 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 Listenarr.Application.Metadata; + +namespace Listenarr.Application.Interfaces +{ + public interface IAudibleAuthorPageParser + { + List ParseAuthorPage(string html, string author, string authorAsin, string region); + } +} diff --git a/listenarr.application/Interfaces/IAudioTagWriter.cs b/listenarr.application/Interfaces/IAudioTagWriter.cs new file mode 100644 index 000000000..471c38a6e --- /dev/null +++ b/listenarr.application/Interfaces/IAudioTagWriter.cs @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +namespace Listenarr.Application.Interfaces +{ + public interface IAudioTagWriter + { + Task WriteAsinTagAsync(string filePath, string asin); + } +} diff --git a/listenarr.application/Interfaces/IAudiobookFileService.cs b/listenarr.application/Interfaces/IAudiobookFileService.cs index e6d618e5b..3ccc7287e 100644 --- a/listenarr.application/Interfaces/IAudiobookFileService.cs +++ b/listenarr.application/Interfaces/IAudiobookFileService.cs @@ -8,7 +8,7 @@ namespace Listenarr.Application.Interfaces public interface IAudiobookFileService { /// - /// Ensure an Audiobook file record exists for the given audiobook and file path. Extract metadata (ffprobe/taglib) and persist file-level metadata. + /// Ensure an Audiobook file record exists for the given audiobook and file path. Extract metadata and persist file-level metadata. /// /// The audiobook /// Path to the audio file diff --git a/listenarr.application/Interfaces/ICoverImageProbe.cs b/listenarr.application/Interfaces/ICoverImageProbe.cs new file mode 100644 index 000000000..eff33696a --- /dev/null +++ b/listenarr.application/Interfaces/ICoverImageProbe.cs @@ -0,0 +1,27 @@ +/* + * 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 . + */ + +namespace Listenarr.Application.Interfaces +{ + public readonly record struct ImageDimensions(int Width, int Height); + + public interface ICoverImageProbe + { + Task ProbeAsync(string url, CancellationToken cancellationToken = default); + } +} diff --git a/listenarr.application/Interfaces/IHtmlTextExtractor.cs b/listenarr.application/Interfaces/IHtmlTextExtractor.cs new file mode 100644 index 000000000..b5c5c0ef1 --- /dev/null +++ b/listenarr.application/Interfaces/IHtmlTextExtractor.cs @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +namespace Listenarr.Application.Interfaces +{ + public interface IHtmlTextExtractor + { + string ExtractText(string html); + } +} diff --git a/listenarr.application/Interfaces/IHubBroadcaster.cs b/listenarr.application/Interfaces/IHubBroadcaster.cs index dbd5f122d..75dddddd2 100644 --- a/listenarr.application/Interfaces/IHubBroadcaster.cs +++ b/listenarr.application/Interfaces/IHubBroadcaster.cs @@ -19,8 +19,16 @@ namespace Listenarr.Application.Interfaces { + public enum RealtimeHubTarget + { + Downloads, + Settings + } + public interface IHubBroadcaster { Task BroadcastQueueUpdateAsync(QueueSnapshot queueSnapshot); + Task BroadcastAsync(string eventName, object payload, CancellationToken cancellationToken = default); + Task BroadcastAsync(RealtimeHubTarget target, string eventName, object payload, CancellationToken cancellationToken = default); } } diff --git a/listenarr.application/Interfaces/IRealtimeClientRegistry.cs b/listenarr.application/Interfaces/IRealtimeClientRegistry.cs new file mode 100644 index 000000000..c8b7f04b6 --- /dev/null +++ b/listenarr.application/Interfaces/IRealtimeClientRegistry.cs @@ -0,0 +1,25 @@ +/* + * 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 . + */ + +namespace Listenarr.Application.Interfaces +{ + public interface IRealtimeClientRegistry + { + IReadOnlyCollection GetSettingsClientIds(); + } +} diff --git a/listenarr.application/Interfaces/IRequestContextAccessor.cs b/listenarr.application/Interfaces/IRequestContextAccessor.cs new file mode 100644 index 000000000..7d7a804e9 --- /dev/null +++ b/listenarr.application/Interfaces/IRequestContextAccessor.cs @@ -0,0 +1,25 @@ +/* + * 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. + */ +using System.Net; + +namespace Listenarr.Application.Interfaces +{ + public sealed record RequestContextSnapshot( + string? Path, + string? Scheme, + string? Host, + IPAddress? RemoteIpAddress, + bool IsAuthenticatedAdminOrApiKey); + + public interface IRequestContextAccessor + { + RequestContextSnapshot? Current { get; } + } +} diff --git a/listenarr.application/Interfaces/ISecretProtector.cs b/listenarr.application/Interfaces/ISecretProtector.cs new file mode 100644 index 000000000..a6dda4b2b --- /dev/null +++ b/listenarr.application/Interfaces/ISecretProtector.cs @@ -0,0 +1,17 @@ +/* + * 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. + */ +namespace Listenarr.Application.Interfaces +{ + public interface ISecretProtector + { + string Protect(string plaintext); + string Unprotect(string protectedValue); + } +} diff --git a/listenarr.application/Listenarr.Application.csproj b/listenarr.application/Listenarr.Application.csproj index 3201501a9..e143391f1 100644 --- a/listenarr.application/Listenarr.Application.csproj +++ b/listenarr.application/Listenarr.Application.csproj @@ -8,14 +8,12 @@ - - - - - - - + + + + + diff --git a/listenarr.application/Metadata/AudibleService.cs b/listenarr.application/Metadata/AudibleService.cs index 68e9be914..5299973c2 100644 --- a/listenarr.application/Metadata/AudibleService.cs +++ b/listenarr.application/Metadata/AudibleService.cs @@ -18,8 +18,9 @@ using System.Globalization; using System.Text; using System.Text.Json; -using HtmlAgilityPack; +using Listenarr.Application.Interfaces; using Listenarr.Application.Security; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Metadata @@ -71,11 +72,19 @@ public class AudibleService }; private readonly HttpClient _httpClient; private readonly ILogger _logger; + private readonly IAudibleAuthorPageParser? _authorPageParser; public AudibleService(HttpClient httpClient, ILogger logger) + : this(httpClient, logger, null) + { + } + + [ActivatorUtilitiesConstructor] + public AudibleService(HttpClient httpClient, ILogger logger, IAudibleAuthorPageParser? authorPageParser) { _httpClient = httpClient; _logger = logger; + _authorPageParser = authorPageParser; _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.ParseAdd(BrowserAcceptHeader); _httpClient.DefaultRequestHeaders.AcceptLanguage.Clear(); @@ -1504,55 +1513,13 @@ private static string NormalizeComparableText(string? value) return null; } - var htmlDoc = new HtmlDocument(); - htmlDoc.LoadHtml(html); - - var tiles = htmlDoc.DocumentNode.SelectNodes("//adbl-full-width-product-tile"); - var legacyProductListItems = htmlDoc.DocumentNode.SelectNodes("//li[contains(@class, 'productListItem')]"); - if ((tiles == null || tiles.Count == 0) && - (legacyProductListItems == null || legacyProductListItems.Count == 0)) + if (_authorPageParser == null) { - _logger.LogWarning("Audible author page contained no recognizable product tiles for author {Author}", LogRedaction.SanitizeText(author)); + _logger.LogWarning("Audible author page parser is unavailable for author {Author}", LogRedaction.SanitizeText(author)); return null; } - var parsedTiles = new List(); - var seenAsins = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (tiles != null) - { - foreach (var tile in tiles) - { - var parsed = ParseAudibleAuthorTile(tile, author, authorAsin, region); - if (parsed == null) continue; - - var key = string.IsNullOrWhiteSpace(parsed.Asin) - ? $"{parsed.Title}|{parsed.Link}" - : parsed.Asin; - if (seenAsins.Add(key)) - { - parsedTiles.Add(parsed); - } - } - } - - if (legacyProductListItems != null) - { - foreach (var item in legacyProductListItems) - { - var parsed = ParseAudibleAuthorListItem(item, author, authorAsin, region); - if (parsed == null) continue; - - var key = string.IsNullOrWhiteSpace(parsed.Asin) - ? $"{parsed.Title}|{parsed.Link}" - : parsed.Asin; - if (seenAsins.Add(key)) - { - parsedTiles.Add(parsed); - } - } - } - + var parsedTiles = _authorPageParser.ParseAuthorPage(html, author, authorAsin, region); if (parsedTiles.Count == 0) { _logger.LogWarning("Audible author page tiles could not be parsed for author {Author}", LogRedaction.SanitizeText(author)); @@ -1724,124 +1691,6 @@ private static List ParseSeriesLookupItems(string lookupJson) return new List(); } - private static AudibleSearchResult? ParseAudibleAuthorTile(HtmlNode tile, string author, string authorAsin, string region) - { - var productImageNode = tile.SelectSingleNode(".//adbl-product-image") - ?? tile.SelectSingleNode(".//adbl-full-bleed-image"); - var asin = productImageNode?.GetAttributeValue("data-asin", string.Empty); - if (string.IsNullOrWhiteSpace(asin)) - { - asin = tile.SelectSingleNode(".//*[@data-asin]")?.GetAttributeValue("data-asin", string.Empty); - } - if (string.IsNullOrWhiteSpace(asin)) return null; - - var title = HtmlEntity.DeEntitize(tile.SelectSingleNode(".//*[@slot='title']")?.InnerText ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(title)) return null; - - var subtitle = HtmlEntity.DeEntitize(tile.SelectSingleNode(".//*[@slot='subtitle']")?.InnerText ?? string.Empty).Trim(); - var imageUrl = productImageNode?.SelectSingleNode(".//img")?.GetAttributeValue("src", string.Empty); - if (string.IsNullOrWhiteSpace(imageUrl)) - { - imageUrl = productImageNode?.GetAttributeValue("portrait-src", string.Empty); - } - if (string.IsNullOrWhiteSpace(imageUrl)) - { - imageUrl = productImageNode?.GetAttributeValue("landscape-src", string.Empty); - } - var relativeUrl = productImageNode?.GetAttributeValue("data-url", string.Empty); - if (string.IsNullOrWhiteSpace(relativeUrl)) - { - relativeUrl = tile.SelectSingleNode(".//adbl-button[@href]")?.GetAttributeValue("href", string.Empty) - ?? tile.SelectSingleNode(".//a[@href]")?.GetAttributeValue("href", string.Empty); - } - - var authors = ParseAudibleAuthorTileAuthors(tile, author, authorAsin, region); - if (authors.Count == 0 && !string.IsNullOrWhiteSpace(author)) - { - authors.Add(new AudibleAuthor { Asin = authorAsin, Name = author, Region = region }); - } - - return new AudibleSearchResult - { - Asin = asin, - Title = title, - Subtitle = string.IsNullOrWhiteSpace(subtitle) ? null : subtitle, - Authors = authors, - ImageUrl = string.IsNullOrWhiteSpace(imageUrl) ? null : imageUrl, - Link = NormalizeAudibleUrl(relativeUrl, region) - }; - } - - private static AudibleSearchResult? ParseAudibleAuthorListItem(HtmlNode listItem, string author, string authorAsin, string region) - { - var asin = listItem.SelectSingleNode(".//*[@data-asin]")?.GetAttributeValue("data-asin", string.Empty); - if (string.IsNullOrWhiteSpace(asin)) - { - return null; - } - - var title = HtmlEntity.DeEntitize(listItem.GetAttributeValue("aria-label", string.Empty)).Trim(); - if (string.IsNullOrWhiteSpace(title)) - { - title = HtmlEntity.DeEntitize( - listItem.SelectSingleNode(".//h2")?.InnerText ?? string.Empty).Trim(); - } - - if (string.IsNullOrWhiteSpace(title)) - { - return null; - } - - var imageUrl = listItem.SelectSingleNode(".//img[@src]")?.GetAttributeValue("src", string.Empty); - var relativeUrl = listItem.SelectSingleNode(".//a[@href]")?.GetAttributeValue("href", string.Empty); - - return new AudibleSearchResult - { - Asin = asin, - Title = title, - Authors = new List - { - new() - { - Asin = authorAsin, - Name = author, - Region = region - } - }, - ImageUrl = string.IsNullOrWhiteSpace(imageUrl) ? null : imageUrl, - Link = NormalizeAudibleUrl(relativeUrl, region) - }; - } - - private static List ParseAudibleAuthorTileAuthors(HtmlNode tile, string author, string authorAsin, string region) - { - var authors = new List(); - var metadataJson = tile.SelectSingleNode(".//adbl-product-metadata/script[@type='application/json']")?.InnerText; - if (string.IsNullOrWhiteSpace(metadataJson)) return authors; - - try - { - var metadata = JsonSerializer.Deserialize(metadataJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (metadata?.Authors == null) return authors; - - foreach (var metadataAuthor in metadata.Authors.Where(metadataAuthor => !string.IsNullOrWhiteSpace(metadataAuthor.Name))) - { - authors.Add(new AudibleAuthor - { - Asin = string.Equals(metadataAuthor.Name, author, StringComparison.OrdinalIgnoreCase) ? authorAsin : null, - Name = metadataAuthor.Name, - Region = region - }); - } - } - catch (JsonException) - { - // Ignore malformed metadata blobs and fall back to the requested author name. - } - - return authors; - } - private static string BuildAudibleAuthorPageUrl(string author, string authorAsin, string region) { var authorSlug = string.IsNullOrWhiteSpace(author) diff --git a/listenarr.application/Metadata/MetadataService.cs b/listenarr.application/Metadata/MetadataService.cs index 12adbb8ea..313e46288 100644 --- a/listenarr.application/Metadata/MetadataService.cs +++ b/listenarr.application/Metadata/MetadataService.cs @@ -30,13 +30,15 @@ public class MetadataService : IMetadataService private readonly HttpClient _httpClient; private readonly IConfigurationService _configurationService; private readonly IFfmpegService _ffmpegService; + private readonly IAudioTagWriter _audioTagWriter; private readonly ILogger _logger; - public MetadataService(HttpClient httpClient, IConfigurationService configurationService, ILogger logger, IFfmpegService ffmpegService) + public MetadataService(HttpClient httpClient, IConfigurationService configurationService, ILogger logger, IFfmpegService ffmpegService, IAudioTagWriter audioTagWriter) { _httpClient = httpClient; _configurationService = configurationService; _ffmpegService = ffmpegService; + _audioTagWriter = audioTagWriter; _logger = logger; } @@ -213,7 +215,7 @@ public async Task ApplyMetadataAsync(string filePath, AudioMetadata metadata) { try { - // This would use a library like TagLib# to apply metadata to audio files + // File tag writing is handled by an infrastructure adapter. _logger.LogInformation("Applied metadata to file: {File}", LogRedaction.SanitizeText(filePath)); await Task.CompletedTask; } @@ -225,35 +227,7 @@ public async Task ApplyMetadataAsync(string filePath, AudioMetadata metadata) public Task WriteAsinTagAsync(string filePath, string asin) { - if (string.IsNullOrWhiteSpace(filePath) || string.IsNullOrWhiteSpace(asin)) - return Task.CompletedTask; - try - { - using var file = TagLib.File.Create(filePath); - - // M4B / M4A / MP4 — iTunes freeform dash box ----:com.apple.iTunes:ASIN - if (file.Tag is TagLib.Mpeg4.AppleTag appleTag) - appleTag.SetDashBox("com.apple.iTunes", "ASIN", asin); - // MP3 — TXXX frame with description "ASIN" - else if (file.GetTag(TagLib.TagTypes.Id3v2) is TagLib.Id3v2.Tag id3Tag) - { - var frame = TagLib.Id3v2.UserTextInformationFrame.Get(id3Tag, "ASIN", true); - frame.Text = new[] { asin }; - } - // FLAC / OGG / Opus — Vorbis comment - else if (file.GetTag(TagLib.TagTypes.Xiph) is TagLib.Ogg.XiphComment xiph) - xiph.SetField("ASIN", asin); - else - return Task.CompletedTask; // Unknown format — skip silently - - file.Save(); - _logger.LogDebug("Wrote ASIN tag '{Asin}' to {File}", asin, LogRedaction.SanitizeFilePath(filePath)); - } - catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) - { - _logger.LogWarning(ex, "Failed to write ASIN tag to {File} — import will continue", LogRedaction.SanitizeFilePath(filePath)); - } - return Task.CompletedTask; + return _audioTagWriter.WriteAsinTagAsync(filePath, asin); } public async Task DownloadCoverArtAsync(string coverArtUrl) @@ -307,4 +281,3 @@ public Task WriteAsinTagAsync(string filePath, string asin) } } } - diff --git a/listenarr.application/Notification/DiscordBotService.cs b/listenarr.application/Notification/DiscordBotService.cs index 6743fd39e..c77674c8d 100644 --- a/listenarr.application/Notification/DiscordBotService.cs +++ b/listenarr.application/Notification/DiscordBotService.cs @@ -19,7 +19,6 @@ using System.Runtime.InteropServices; using Listenarr.Application.Interfaces; using Listenarr.Application.Security; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Notification @@ -37,7 +36,7 @@ public class DiscordBotService : IDiscordBotService private readonly ILogger _logger; private readonly IStartupConfigService _startupConfigService; private readonly IApplicationPathService _applicationPathService; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IRequestContextAccessor _requestContextAccessor; private readonly IProcessRunner? _processRunner; private string? _botApiKey; private Process? _botProcess; @@ -47,13 +46,13 @@ public DiscordBotService( ILogger logger, IStartupConfigService startupConfigService, IApplicationPathService applicationPathService, - IHttpContextAccessor httpContextAccessor, + IRequestContextAccessor requestContextAccessor, IProcessRunner? processRunner = null) { _logger = logger; _startupConfigService = startupConfigService; _applicationPathService = applicationPathService; - _httpContextAccessor = httpContextAccessor; + _requestContextAccessor = requestContextAccessor; _processRunner = processRunner; } @@ -107,7 +106,7 @@ public async Task StartBotAsync() startInfo.EnvironmentVariables["LISTENARR_URL"] = listenarrUrl; // Pass the server API key into the helper process so it can authenticate - // programmatic requests (SignalR negotiate, settings fetch, etc.). Only set + // programmatic requests (realtime negotiate, settings fetch, etc.). Only set // when an API key is present in the startup config to avoid sending empty // values into the child environment. try @@ -262,31 +261,20 @@ private string GetListenarrUrl() // Priority 2: Construct from current HTTP request (when available) try { - var httpContext = _httpContextAccessor.HttpContext; - if (httpContext != null) + var requestContext = _requestContextAccessor.Current; + if (requestContext != null) { - var request = httpContext.Request; - var scheme = request.Scheme; - var host = request.Host.Value; - - // Check if we're behind a reverse proxy (X-Forwarded headers) - if (request.Headers.TryGetValue("X-Forwarded-Proto", out var forwardedProto)) - { - scheme = forwardedProto.ToString(); - } - if (request.Headers.TryGetValue("X-Forwarded-Host", out var forwardedHost)) - { - host = forwardedHost.ToString(); - } + var scheme = requestContext.Scheme; + var host = requestContext.Host; var url = $"{scheme}://{host}"; - _logger.LogInformation("Constructed URL from HTTP context: {Url}", url); + _logger.LogInformation("Constructed URL from request context: {Url}", url); return url; } } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { - _logger.LogWarning(ex, "Failed to construct URL from HTTP context"); + _logger.LogWarning(ex, "Failed to construct URL from request context"); } // Priority 3: Use startup config diff --git a/listenarr.application/Notification/INotificationPayloadBuilder.cs b/listenarr.application/Notification/INotificationPayloadBuilder.cs index 73866e306..f482203ab 100644 --- a/listenarr.application/Notification/INotificationPayloadBuilder.cs +++ b/listenarr.application/Notification/INotificationPayloadBuilder.cs @@ -16,7 +16,7 @@ * along with this program. If not, see . */ using System.Text.Json.Nodes; -using Microsoft.AspNetCore.Http; +using Listenarr.Application.Interfaces; namespace Listenarr.Application.Notification { @@ -33,7 +33,7 @@ public interface INotificationPayloadBuilder object data, string? startupBaseUrl, HttpClient httpClient, - IHttpContextAccessor? httpContextAccessor = null, + IRequestContextAccessor? requestContextAccessor = null, Action? logInfo = null, Action? logDebug = null, string? apiVersion = null); diff --git a/listenarr.application/Notification/NotificationPayloadBuilder.cs b/listenarr.application/Notification/NotificationPayloadBuilder.cs index 036273561..ad6d85345 100644 --- a/listenarr.application/Notification/NotificationPayloadBuilder.cs +++ b/listenarr.application/Notification/NotificationPayloadBuilder.cs @@ -19,7 +19,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using Listenarr.Application.Common; -using Microsoft.AspNetCore.Http; +using Listenarr.Application.Interfaces; namespace Listenarr.Application.Notification { @@ -259,7 +259,7 @@ static string Truncate(string? value, int max) return payload; } - public static async Task<(JsonObject payload, AttachmentInfo? attachment)> CreateDiscordPayloadWithAttachmentAsync(string trigger, object data, string? startupBaseUrl, HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, Action? logInfo = null, Action? logDebug = null, string? apiVersion = null) + public static async Task<(JsonObject payload, AttachmentInfo? attachment)> CreateDiscordPayloadWithAttachmentAsync(string trigger, object data, string? startupBaseUrl, HttpClient httpClient, IRequestContextAccessor? requestContextAccessor = null, Action? logInfo = null, Action? logDebug = null, string? apiVersion = null) { // Implementation mirrors previous CreateDiscordPayloadWithAttachmentAsync but kept here to centralize payload logic. JsonNode? node = data == null ? null : JsonSerializer.SerializeToNode(data); @@ -341,9 +341,9 @@ static string Truncate(string? value, int max) absoluteImageUrl = startupBaseUrl.TrimEnd('/') + imageUrl; logInfo?.Invoke($"Constructed absolute URL from relative path: {absoluteImageUrl}"); } - else if (imageUrl.StartsWith("/") && startupBaseUrl == null && httpContextAccessor?.HttpContext != null) + else if (imageUrl.StartsWith("/") && startupBaseUrl == null && requestContextAccessor?.Current != null) { - var derived = GetBaseUrlFromHttpContext(httpContextAccessor.HttpContext); + var derived = GetBaseUrlFromRequestContext(requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) absoluteImageUrl = derived.TrimEnd('/') + imageUrl; } } @@ -498,12 +498,11 @@ static string Truncate(string? value, int max) return (payload, attachmentInfo); } - public static string? GetBaseUrlFromHttpContext(HttpContext? ctx) + public static string? GetBaseUrlFromRequestContext(RequestContextSnapshot? ctx) { - if (ctx?.Request == null) return null; - var req = ctx.Request; - var scheme = req.Scheme; - var host = req.Host.Value; + if (ctx == null) return null; + var scheme = ctx.Scheme; + var host = ctx.Host; if (string.IsNullOrWhiteSpace(scheme) || string.IsNullOrWhiteSpace(host)) return null; return scheme + "://" + host; } diff --git a/listenarr.application/Notification/NotificationPayloadBuilderAdapter.cs b/listenarr.application/Notification/NotificationPayloadBuilderAdapter.cs index a3481c27e..70865b0a6 100644 --- a/listenarr.application/Notification/NotificationPayloadBuilderAdapter.cs +++ b/listenarr.application/Notification/NotificationPayloadBuilderAdapter.cs @@ -16,7 +16,7 @@ * along with this program. If not, see . */ using System.Text.Json.Nodes; -using Microsoft.AspNetCore.Http; +using Listenarr.Application.Interfaces; namespace Listenarr.Application.Notification { @@ -36,7 +36,7 @@ public JsonNode CreateDiscordPayload(string trigger, object data, string? startu object data, string? startupBaseUrl, HttpClient httpClient, - IHttpContextAccessor? httpContextAccessor = null, + IRequestContextAccessor? requestContextAccessor = null, Action? logInfo = null, Action? logDebug = null, string? apiVersion = null) @@ -46,7 +46,7 @@ public JsonNode CreateDiscordPayload(string trigger, object data, string? startu data, startupBaseUrl, httpClient, - httpContextAccessor, + requestContextAccessor, logInfo, logDebug, apiVersion); diff --git a/listenarr.application/Notification/NotificationService.cs b/listenarr.application/Notification/NotificationService.cs index 5a258dc77..f103adf1c 100644 --- a/listenarr.application/Notification/NotificationService.cs +++ b/listenarr.application/Notification/NotificationService.cs @@ -23,7 +23,6 @@ using Listenarr.Application.Interfaces; using Listenarr.Application.Security; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Notification @@ -39,17 +38,17 @@ public class NotificationService : INotificationService private readonly HttpClient _httpClientNoRedirect; private readonly ILogger _logger; private readonly IConfigurationService _configurationService; - private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly IRequestContextAccessor? _requestContextAccessor; private readonly INotificationPayloadBuilder _payloadBuilder; - public NotificationService(HttpClient httpClient, ILogger logger, IConfigurationService configurationService, INotificationPayloadBuilder payloadBuilder, IHttpContextAccessor? httpContextAccessor = null) + public NotificationService(HttpClient httpClient, ILogger logger, IConfigurationService configurationService, INotificationPayloadBuilder payloadBuilder, IRequestContextAccessor? requestContextAccessor = null) { _httpClient = httpClient; _httpClientNoRedirect = httpClient; _logger = logger; _configurationService = configurationService; _payloadBuilder = payloadBuilder ?? throw new ArgumentNullException(nameof(payloadBuilder)); - _httpContextAccessor = httpContextAccessor; + _requestContextAccessor = requestContextAccessor; } // INotificationService interface stubs — webhook dispatch goes through SendNotificationAsync; @@ -100,14 +99,15 @@ public async Task SendSystemNotificationAsync(string title, string message) private bool AllowPrivateWebhookTargetsForCurrentRequest() { - var context = _httpContextAccessor?.HttpContext; + var context = _requestContextAccessor?.Current; if (context == null) { return true; } - return SecurityRequestUtils.IsLoopbackRequest(context) - || SecurityRequestUtils.IsAuthenticatedAdminOrApiKey(context); + return context.RemoteIpAddress == null + || System.Net.IPAddress.IsLoopback(context.RemoteIpAddress) + || context.IsAuthenticatedAdminOrApiKey; } private async Task PostValidatedAsync(string url, HttpContent content, CancellationToken cancellationToken = default) @@ -250,9 +250,9 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) var startup = await _configurationService.GetStartupConfigAsync(); var baseUrl = startup?.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } @@ -264,10 +264,10 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) } var (payloadObj, attachment) = await _payloadBuilder.CreateDiscordPayloadWithAttachmentAsync( - trigger, data, baseUrl, _httpClient, _httpContextAccessor, + trigger, data, baseUrl, _httpClient, _requestContextAccessor, logInfo: msg => _logger.LogInformation(msg), logDebug: (ex, msg) => _logger.LogDebug(ex, msg), - apiVersion: ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion) + apiVersion: ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion) ); _logger.LogDebug("Discord payload attachment present? {HasAttachment}", attachment != null); @@ -322,13 +322,13 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } - var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); var title = discordPayload is JsonObject d && d.TryGetPropertyValue("content", out var c) ? (c?.ToString() ?? string.Empty) : string.Empty; var message = title; @@ -400,13 +400,13 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } - var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); var message = discordPayload is JsonObject d && d.TryGetPropertyValue("content", out var c) ? (c?.ToString() ?? string.Empty) : string.Empty; var values = new List> @@ -474,13 +474,13 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } - var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); var text = discordPayload is JsonObject d && d.TryGetPropertyValue("content", out var c) ? (c?.ToString() ?? string.Empty) : string.Empty; var telegramBody = new { chat_id = chatId, text = text ?? string.Empty, disable_notification = true, parse_mode = "Markdown" }; @@ -555,13 +555,13 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } - var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); var message = discordPayload is JsonObject d && d.TryGetPropertyValue("content", out var c) ? (c?.ToString() ?? string.Empty) : string.Empty; var pushObj = new JsonObject @@ -626,13 +626,13 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } - var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var discordPayload = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); var message = discordPayload is JsonObject d && d.TryGetPropertyValue("content", out var c) ? (c?.ToString() ?? string.Empty) : string.Empty; var slackObj = new JsonObject @@ -683,14 +683,14 @@ async Task HandleFailedResponseAsync(HttpResponseMessage response) string? baseUrl = null; var startup = await _configurationService.GetStartupConfigAsync(); if (startup?.UrlBase != null) baseUrl = startup.UrlBase; - if (string.IsNullOrWhiteSpace(baseUrl) && _httpContextAccessor?.HttpContext != null) + if (string.IsNullOrWhiteSpace(baseUrl) && _requestContextAccessor?.Current != null) { - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(_httpContextAccessor.HttpContext); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(_requestContextAccessor.Current); if (!string.IsNullOrWhiteSpace(derived)) baseUrl = derived; } // Prefer rich payload created by the static helper (includes content, embeds, image links, etc.) - var payloadObj = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_httpContextAccessor?.HttpContext, startup?.ApiVersion)); + var payloadObj = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, baseUrl, ApiVersionUtils.ResolveApiVersion(_requestContextAccessor?.Current?.Path, startup?.ApiVersion)); string defaultJson = payloadObj != null ? payloadObj.ToJsonString() : JsonSerializer.Serialize(new { @event = trigger, data = data, timestamp = DateTime.UtcNow }, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); using var defaultContent = new StringContent(defaultJson, Encoding.UTF8, "application/json"); @@ -773,7 +773,3 @@ private static bool TryValidateWebhookTarget(string webhookUrl, out string reaso } } } - - - - diff --git a/listenarr.application/Notification/SearchProgressReporter.cs b/listenarr.application/Notification/SearchProgressReporter.cs index b34953f8a..826ddd29e 100644 --- a/listenarr.application/Notification/SearchProgressReporter.cs +++ b/listenarr.application/Notification/SearchProgressReporter.cs @@ -15,27 +15,27 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.AspNetCore.SignalR; +using Listenarr.Application.Interfaces; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Notification { /// - /// Handles broadcasting search progress updates to connected clients via SignalR. + /// Handles broadcasting search progress updates to connected realtime clients. /// public class SearchProgressReporter { - private readonly IHubContext? _hubContext; + private readonly IHubBroadcaster? _hubBroadcaster; private readonly ILogger _logger; - public SearchProgressReporter(IHubContext? hubContext, ILogger logger) + public SearchProgressReporter(IHubBroadcaster? hubBroadcaster, ILogger logger) { - _hubContext = hubContext; + _hubBroadcaster = hubBroadcaster; _logger = logger; } /// - /// Broadcasts a search progress message to all connected SignalR clients. + /// Broadcasts a search progress message to all connected realtime clients. /// /// The progress message to broadcast /// Optional ASIN associated with this progress update @@ -43,10 +43,10 @@ public async Task BroadcastAsync(string message, string? asin = null) { try { - if (_hubContext != null) + if (_hubBroadcaster != null) { // Structured payload: include a type so clients can distinguish interactive vs automatic - await _hubContext.Clients.All.SendAsync("SearchProgress", new { message, asin, type = "interactive" }); + await _hubBroadcaster.BroadcastAsync("SearchProgress", new { message, asin, type = "interactive" }); } } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) diff --git a/listenarr.application/Search/MetadataConverters.cs b/listenarr.application/Search/MetadataConverters.cs index 91f583683..e43cc18f3 100644 --- a/listenarr.application/Search/MetadataConverters.cs +++ b/listenarr.application/Search/MetadataConverters.cs @@ -4,7 +4,6 @@ using Listenarr.Application.Metadata; using Listenarr.Domain.Common; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Listenarr.Application.Search; @@ -16,13 +15,13 @@ public class MetadataConverters { private readonly IImageCacheService? _imageCacheService; private readonly ILogger _logger; - private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly IRequestContextAccessor? _requestContextAccessor; - public MetadataConverters(IImageCacheService? imageCacheService, ILogger logger, IHttpContextAccessor? httpContextAccessor = null) + public MetadataConverters(IImageCacheService? imageCacheService, ILogger logger, IRequestContextAccessor? requestContextAccessor = null) { _imageCacheService = imageCacheService; _logger = logger; - _httpContextAccessor = httpContextAccessor; + _requestContextAccessor = requestContextAccessor; } private static List? BuildSeriesMemberships(IEnumerable? series) @@ -260,14 +259,14 @@ public async Task ConvertMetadataToSearchResultAsync(AudibleBookMe var cachedPath = await _imageCacheService.GetCachedImagePathAsync(asin); if (!string.IsNullOrWhiteSpace(cachedPath)) { - imageUrl = ApiVersionUtils.BuildImagePath(asin, _httpContextAccessor?.HttpContext); + imageUrl = ApiVersionUtils.BuildImagePath(asin, _requestContextAccessor?.Current?.Path); _logger.LogInformation("Using cached image for ASIN {Asin}: {ImageUrl}", asin, imageUrl); } else { // Even if not cached, map to API endpoint to ensure consistent serving // and avoid external URL failures. Background download will populate cache. - imageUrl = ApiVersionUtils.BuildImagePath(asin, _httpContextAccessor?.HttpContext); + imageUrl = ApiVersionUtils.BuildImagePath(asin, _requestContextAccessor?.Current?.Path); _logger.LogDebug("Mapping to API endpoint for ASIN {Asin} (not yet cached): {ImageUrl}", asin, imageUrl); _logger.LogDebug("Initiating background image cache for ASIN {Asin} with URL: {OriginalUrl}", asin, metadata.ImageUrl ?? imageUrl); _ = _imageCacheService.DownloadAndCacheImageAsync(metadata.ImageUrl ?? imageUrl, asin); @@ -407,14 +406,14 @@ public async Task ConvertMetadataToMetadataSearchResultAsy var cachedPath = await _imageCacheService.GetCachedImagePathAsync(asin); if (!string.IsNullOrWhiteSpace(cachedPath)) { - imageUrl = ApiVersionUtils.BuildImagePath(asin, _httpContextAccessor?.HttpContext); + imageUrl = ApiVersionUtils.BuildImagePath(asin, _requestContextAccessor?.Current?.Path); _logger.LogInformation("Using cached image for ASIN {Asin}: {ImageUrl}", asin, imageUrl); } else { // Even if not cached, map to API endpoint to ensure consistent serving // and avoid external URL failures. Background download will populate cache. - imageUrl = ApiVersionUtils.BuildImagePath(asin, _httpContextAccessor?.HttpContext); + imageUrl = ApiVersionUtils.BuildImagePath(asin, _requestContextAccessor?.Current?.Path); _logger.LogDebug("Mapping to API endpoint for ASIN {Asin} (not yet cached): {ImageUrl}", asin, imageUrl); _logger.LogDebug("Initiating background image cache for ASIN {Asin} with URL: {OriginalUrl}", asin, metadata.ImageUrl ?? imageUrl); _ = _imageCacheService.DownloadAndCacheImageAsync(metadata.ImageUrl ?? imageUrl, asin); diff --git a/listenarr.application/Search/SearchService.cs b/listenarr.application/Search/SearchService.cs index e4293ca9a..b84969585 100644 --- a/listenarr.application/Search/SearchService.cs +++ b/listenarr.application/Search/SearchService.cs @@ -29,7 +29,6 @@ using Microsoft.Extensions.Logging; using Listenarr.Application.Notification; using Listenarr.Application.Metadata; -using SixLabors.ImageSharp; using Listenarr.Application.Security; namespace Listenarr.Application.Search @@ -50,6 +49,8 @@ public class SearchService : ISearchService private readonly AsinSearchHandler _asinSearchHandler; private readonly IMemoryCache? _cache; private readonly IEnumerable _searchProviders; + private readonly ICoverImageProbe? _coverImageProbe; + private readonly IHtmlTextExtractor? _htmlTextExtractor; public SearchService( HttpClient httpClient, @@ -65,7 +66,9 @@ public SearchService( SearchResultScorerService searchResultScorer, AsinSearchHandler asinSearchHandler, IEnumerable? searchProviders = null, - IMemoryCache? cache = null) + IMemoryCache? cache = null, + ICoverImageProbe? coverImageProbe = null, + IHtmlTextExtractor? htmlTextExtractor = null) { _httpClient = httpClient; _configurationService = configurationService; @@ -81,6 +84,8 @@ public SearchService( _searchResultScorer = searchResultScorer; _asinSearchHandler = asinSearchHandler; _cache = cache; + _coverImageProbe = coverImageProbe; + _htmlTextExtractor = htmlTextExtractor; } public async Task> SearchAsync(string query, string? category = null, List? apiIds = null, SearchSortBy sortBy = SearchSortBy.Seeders, SearchSortDirection sortDirection = SearchSortDirection.Descending, bool isAutomaticSearch = false) @@ -1429,30 +1434,19 @@ private static bool isOpenLibraryResult(SearchResult r) try { var url = $"https://covers.openlibrary.org/b/id/{cid}-L.jpg"; - using var resp = await _httpClient.GetAsync(url); - if (!resp.IsSuccessStatusCode) continue; - using var ms = new System.IO.MemoryStream(await resp.Content.ReadAsByteArrayAsync()); - try + var dimensions = _coverImageProbe == null ? null : await _coverImageProbe.ProbeAsync(url); + if (dimensions == null || dimensions.Value.Height == 0) continue; + + var ratio = (double)dimensions.Value.Width / dimensions.Value.Height; + var delta = Math.Abs(ratio - 1.0); + if (delta < bestDelta) { - // Use ImageSharp to measure image dimensions in a cross-platform way - using var img = Image.Load(ms); - if (img.Height == 0) continue; - var ratio = (double)img.Width / img.Height; - var delta = Math.Abs(ratio - 1.0); - if (delta < bestDelta) - { - bestDelta = delta; - bestUrl = url; - } - // If exactly 1:1, short-circuit - if (Math.Abs(delta) < 0.01) - break; - } - catch (Exception imgEx) when (imgEx is not OperationCanceledException && imgEx is not OutOfMemoryException && imgEx is not StackOverflowException) - { - _logger.LogDebug(imgEx, "Failed to measure image dimensions for cover {Url}", url); - continue; + bestDelta = delta; + bestUrl = url; } + + if (Math.Abs(delta) < 0.01) + break; } catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) { @@ -3621,11 +3615,8 @@ internal async Task> ParseTorznabResponseAsync(string if (resp.IsSuccessStatusCode) { var html = await resp.Content.ReadAsStringAsync(); - var htmlDoc = new HtmlAgilityPack.HtmlDocument(); - htmlDoc.LoadHtml(html); - // Look for common comment count patterns in page text - var text = htmlDoc.DocumentNode.InnerText; + var text = _htmlTextExtractor?.ExtractText(html) ?? html; var m = System.Text.RegularExpressions.Regex.Match(text, "(\\d{1,6})\\s+comments?", System.Text.RegularExpressions.RegexOptions.IgnoreCase); if (!m.Success) { @@ -4199,5 +4190,3 @@ public async Task> GetEnabledMetadataSourcesAsync() } } } - - diff --git a/listenarr.application/Security/SecurityRequestUtils.cs b/listenarr.application/Security/SecurityRequestUtils.cs index 9b68c6b31..935f28e39 100644 --- a/listenarr.application/Security/SecurityRequestUtils.cs +++ b/listenarr.application/Security/SecurityRequestUtils.cs @@ -18,89 +18,11 @@ using System.Net; using System.Security.Cryptography; using System.Text; -using Microsoft.AspNetCore.Http; namespace Listenarr.Application.Security; public static class SecurityRequestUtils { - public static bool IsLoopbackRequest(HttpContext? context) - { - var ip = context?.Connection?.RemoteIpAddress; - if (ip == null) - { - // TestServer and some internal calls may not populate RemoteIpAddress. - return true; - } - - if (ip.IsIPv4MappedToIPv6) - { - ip = ip.MapToIPv4(); - } - - return IPAddress.IsLoopback(ip); - } - - public static bool IsLocalOrPrivateRequest(HttpContext? context) - { - var ip = context?.Connection?.RemoteIpAddress; - if (ip == null) - { - // TestServer and some internal calls may not populate RemoteIpAddress. - return true; - } - - return IsPrivateOrLoopback(ip); - } - - public static bool IsAuthenticatedAdminOrApiKey(HttpContext? context) - { - var user = context?.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - if (user.IsInRole("Administrator")) - { - return true; - } - - var authMethod = user.FindFirst("AuthMethod")?.Value; - if (!string.IsNullOrWhiteSpace(authMethod) && - string.Equals(authMethod, "ApiKey", StringComparison.Ordinal)) - { - return true; - } - - return false; - } - - /// - /// Returns if the request is authenticated exclusively via an API key - /// (i.e. the AuthMethod claim equals "ApiKey"). - /// Returns for unauthenticated requests or session-authenticated requests. - /// - /// The current HTTP context, or for non-HTTP callers. - public static bool IsApiKeyAuthenticated(HttpContext? context) - { - var user = context?.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - var authMethod = user.FindFirst("AuthMethod")?.Value; - return !string.IsNullOrWhiteSpace(authMethod) && - string.Equals(authMethod, "ApiKey", StringComparison.Ordinal); - } - - public static bool ShouldRedactSecretsForCaller(HttpContext? context) - // *Arr standard trust model: - // - trusted local/private-network callers may receive non-redacted config payloads - // - public-network callers must authenticate as admin/API-key to receive secrets - => !IsLocalOrPrivateRequest(context) && !IsAuthenticatedAdminOrApiKey(context); - public static string HashSecretForLog(string? secret, string prefix = "sha256") { if (string.IsNullOrWhiteSpace(secret)) diff --git a/listenarr.application/Security/SessionService.cs b/listenarr.application/Security/SessionService.cs index 08db03be0..91803b0da 100644 --- a/listenarr.application/Security/SessionService.cs +++ b/listenarr.application/Security/SessionService.cs @@ -16,9 +16,9 @@ * along with this program. If not, see . */ +using Listenarr.Application.Common; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Domain.Models; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Security.Claims; using System.Security.Cryptography; @@ -79,7 +79,7 @@ public async Task CreateSessionAsync(string username, bool isAdmin, bool _logger.LogInformation("Created session for user {Username} (RememberMe: {RememberMe})", username, rememberMe); return sessionToken; } - catch (DbUpdateException) when (attempt < 2) + catch (UniqueConstraintViolationException) when (attempt < 2) { // Try another token when uniqueness is violated. } diff --git a/listenarr.infrastructure/Adapters/NzbgetAdapter.cs b/listenarr.infrastructure/Adapters/NzbgetAdapter.cs index 65c7f253d..b1ddb0090 100644 --- a/listenarr.infrastructure/Adapters/NzbgetAdapter.cs +++ b/listenarr.infrastructure/Adapters/NzbgetAdapter.cs @@ -21,7 +21,6 @@ using System.Text; using System.Text.Json; using System.Xml.Linq; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; using Listenarr.Application.Security; using Listenarr.Domain.Common; diff --git a/listenarr.infrastructure/Adapters/QbittorrentAdapter.cs b/listenarr.infrastructure/Adapters/QbittorrentAdapter.cs index 0fc19052f..022f40de4 100644 --- a/listenarr.infrastructure/Adapters/QbittorrentAdapter.cs +++ b/listenarr.infrastructure/Adapters/QbittorrentAdapter.cs @@ -19,8 +19,6 @@ using System.Text.Json; using BencodeNET.Parsing; using BencodeNET.Torrents; -using Listenarr.Application.Common; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; using Listenarr.Application.Security; using Listenarr.Domain.Common; diff --git a/listenarr.infrastructure/Adapters/SabnzbdAdapter.cs b/listenarr.infrastructure/Adapters/SabnzbdAdapter.cs index a77787c9c..be1eaccb8 100644 --- a/listenarr.infrastructure/Adapters/SabnzbdAdapter.cs +++ b/listenarr.infrastructure/Adapters/SabnzbdAdapter.cs @@ -17,7 +17,6 @@ */ using System.Net; using System.Text.Json; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; using Listenarr.Application.Security; using Listenarr.Domain.Common; diff --git a/listenarr.infrastructure/Adapters/TransmissionAdapter.cs b/listenarr.infrastructure/Adapters/TransmissionAdapter.cs index c1f6a38bb..6d195e3ea 100644 --- a/listenarr.infrastructure/Adapters/TransmissionAdapter.cs +++ b/listenarr.infrastructure/Adapters/TransmissionAdapter.cs @@ -21,7 +21,6 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; using Listenarr.Application.Security; using Listenarr.Domain.Common; diff --git a/listenarr.infrastructure/Extensions/AppServiceRegistrationExtensions.cs b/listenarr.infrastructure/Extensions/AppServiceRegistrationExtensions.cs index 55975459c..404783a32 100644 --- a/listenarr.infrastructure/Extensions/AppServiceRegistrationExtensions.cs +++ b/listenarr.infrastructure/Extensions/AppServiceRegistrationExtensions.cs @@ -16,13 +16,8 @@ * along with this program. If not, see . */ // csharp -using Listenarr.Application.Audiobooks; -using Listenarr.Application.Common; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; -using Listenarr.Application.Metadata; using Listenarr.Application.Notification; -using Listenarr.Application.Search; using Listenarr.Application.Security; using Listenarr.Domain.Models.Configurations; using Listenarr.Infrastructure.Ffmpeg; @@ -33,7 +28,7 @@ using Listenarr.Infrastructure.Search.Providers; using Listenarr.Infrastructure.Security; using Listenarr.Infrastructure.Services; -using Listenarr.Infrastructure.SignalR; +using Listenarr.Infrastructure.Web; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -49,6 +44,9 @@ public static IServiceCollection AddListenarrAppServices(this IServiceCollection { // Core services and application logic services.AddScoped(); + services.AddDataProtection(); + services.AddSingleton(); + services.AddSingleton(); // Startup config: read config.json (optional) and expose via IStartupConfigService services.AddSingleton(); diff --git a/listenarr.infrastructure/Extensions/HostedServiceRegistrationExtensions.cs b/listenarr.infrastructure/Extensions/HostedServiceRegistrationExtensions.cs index 6ddf5c36d..49de51774 100644 --- a/listenarr.infrastructure/Extensions/HostedServiceRegistrationExtensions.cs +++ b/listenarr.infrastructure/Extensions/HostedServiceRegistrationExtensions.cs @@ -16,12 +16,7 @@ * along with this program. If not, see . */ // csharp -using Listenarr.Application.Audiobooks; -using Listenarr.Application.Common; -using Listenarr.Application.Downloads; using Listenarr.Application.Interfaces; -using Listenarr.Application.Metadata; -using Listenarr.Application.Search; using Listenarr.Infrastructure.Ffmpeg; using Listenarr.Infrastructure.FileSystem; using Microsoft.Extensions.Configuration; diff --git a/listenarr.infrastructure/Extensions/InfrastructureServiceRegistrationExtensions.cs b/listenarr.infrastructure/Extensions/InfrastructureServiceRegistrationExtensions.cs index bbe72747a..caa2e7185 100644 --- a/listenarr.infrastructure/Extensions/InfrastructureServiceRegistrationExtensions.cs +++ b/listenarr.infrastructure/Extensions/InfrastructureServiceRegistrationExtensions.cs @@ -74,6 +74,10 @@ public static IServiceCollection AddListenarrInfrastructure( services.AddScoped(); services.AddScoped(); services.AddSingleton(_ => new ApplicationPathService(contentRootPath)); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient(); services.AddHttpClient() .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { diff --git a/listenarr.infrastructure/Extensions/RealtimeEndpointRouteBuilderExtensions.cs b/listenarr.infrastructure/Extensions/RealtimeEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..38d25bc58 --- /dev/null +++ b/listenarr.infrastructure/Extensions/RealtimeEndpointRouteBuilderExtensions.cs @@ -0,0 +1,42 @@ +/* + * 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.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Listenarr.Infrastructure.Extensions +{ + public static class RealtimeEndpointRouteBuilderExtensions + { + public static IEndpointRouteBuilder MapListenarrRealtimeHubs(this IEndpointRouteBuilder endpoints, IHostEnvironment environment) + { + if (environment.IsDevelopment()) + { + endpoints.MapHub("/hubs/downloads").RequireCors("DevOnly"); + endpoints.MapHub("/hubs/logs").RequireCors("DevOnly"); + endpoints.MapHub("/hubs/settings").RequireCors("DevOnly"); + return endpoints; + } + + endpoints.MapHub("/hubs/downloads"); + endpoints.MapHub("/hubs/logs"); + endpoints.MapHub("/hubs/settings"); + return endpoints; + } + } +} diff --git a/listenarr.infrastructure/Extensions/RealtimeLoggingExtensions.cs b/listenarr.infrastructure/Extensions/RealtimeLoggingExtensions.cs new file mode 100644 index 000000000..6512521ac --- /dev/null +++ b/listenarr.infrastructure/Extensions/RealtimeLoggingExtensions.cs @@ -0,0 +1,35 @@ +/* + * 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.DependencyInjection; + +namespace Listenarr.Infrastructure.Extensions +{ + public static class RealtimeLoggingExtensions + { + public static SignalRLogSink CreateListenarrRealtimeLogSink() + { + return new SignalRLogSink(); + } + + public static void InitializeListenarrRealtimeLogging(this SignalRLogSink signalRSink, IServiceProvider serviceProvider) + { + signalRSink.Initialize(serviceProvider.GetRequiredService>()); + } + } +} diff --git a/listenarr.infrastructure/Extensions/ServiceRegistrationExtensions.cs b/listenarr.infrastructure/Extensions/ServiceRegistrationExtensions.cs index 758ebade1..e17a4a073 100644 --- a/listenarr.infrastructure/Extensions/ServiceRegistrationExtensions.cs +++ b/listenarr.infrastructure/Extensions/ServiceRegistrationExtensions.cs @@ -19,13 +19,10 @@ using System.Net; using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Factories; -using Listenarr.Application.Downloads; using Listenarr.Infrastructure.Adapters; using Listenarr.Domain.Models.Configurations; using Listenarr.Application.Notification; using Listenarr.Infrastructure.FileSystem; -using Listenarr.Infrastructure.SignalR; -using Listenarr.Application.Metadata; using Microsoft.Extensions.DependencyInjection; using Polly.Extensions.Http; using Microsoft.Extensions.Configuration; @@ -222,6 +219,7 @@ public static IServiceCollection AddListenarrAdapters(this IServiceCollection se // SignalR broadcaster abstraction used to centralize broadcast logic and simplify testing services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/listenarr.infrastructure/Ffmpeg/FfmpegInstallBackgroundService.cs b/listenarr.infrastructure/Ffmpeg/FfmpegInstallBackgroundService.cs index 9d747b664..95b1cef8b 100644 --- a/listenarr.infrastructure/Ffmpeg/FfmpegInstallBackgroundService.cs +++ b/listenarr.infrastructure/Ffmpeg/FfmpegInstallBackgroundService.cs @@ -16,9 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.Ffmpeg diff --git a/listenarr.infrastructure/FileSystem/ArchiveExtractor.cs b/listenarr.infrastructure/FileSystem/ArchiveExtractor.cs index 1335023f0..9f593e840 100644 --- a/listenarr.infrastructure/FileSystem/ArchiveExtractor.cs +++ b/listenarr.infrastructure/FileSystem/ArchiveExtractor.cs @@ -15,7 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Common; using Listenarr.Application.Interfaces; using Microsoft.Extensions.Logging; using SharpCompress.Archives; diff --git a/listenarr.infrastructure/FileSystem/MoveBackgroundService.cs b/listenarr.infrastructure/FileSystem/MoveBackgroundService.cs index 774428e50..471418a71 100644 --- a/listenarr.infrastructure/FileSystem/MoveBackgroundService.cs +++ b/listenarr.infrastructure/FileSystem/MoveBackgroundService.cs @@ -18,12 +18,9 @@ using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Mapping; -using Listenarr.Application.Notification; using Listenarr.Application.Security; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.FileSystem diff --git a/listenarr.infrastructure/GlobalUsings.cs b/listenarr.infrastructure/GlobalUsings.cs new file mode 100644 index 000000000..64e0257ca --- /dev/null +++ b/listenarr.infrastructure/GlobalUsings.cs @@ -0,0 +1,15 @@ +global using Microsoft.AspNetCore.DataProtection; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.SignalR; +global using Microsoft.Extensions.Hosting; +global using Listenarr.Application.Audiobooks; +global using Listenarr.Application.Common; +global using Listenarr.Application.Downloads; +global using Listenarr.Application.Metadata; +global using Listenarr.Application.Search; +global using Listenarr.Infrastructure.SignalR; +global using Listenarr.Infrastructure.HostedServices.Audiobooks; +global using Listenarr.Infrastructure.HostedServices.Common; +global using Listenarr.Infrastructure.HostedServices.Downloads; +global using Listenarr.Infrastructure.HostedServices.Metadata; +global using Listenarr.Infrastructure.HostedServices.Search; diff --git a/listenarr.application/Audiobooks/AuthorMonitoringBackgroundService.cs b/listenarr.infrastructure/HostedServices/Audiobooks/AuthorMonitoringBackgroundService.cs similarity index 97% rename from listenarr.application/Audiobooks/AuthorMonitoringBackgroundService.cs rename to listenarr.infrastructure/HostedServices/Audiobooks/AuthorMonitoringBackgroundService.cs index a5a1490f6..ec12feffe 100644 --- a/listenarr.application/Audiobooks/AuthorMonitoringBackgroundService.cs +++ b/listenarr.infrastructure/HostedServices/Audiobooks/AuthorMonitoringBackgroundService.cs @@ -17,10 +17,9 @@ */ using Listenarr.Application.Interfaces; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Audiobooks +namespace Listenarr.Infrastructure.HostedServices.Audiobooks { public class AuthorMonitoringBackgroundService : BackgroundService { diff --git a/listenarr.application/Audiobooks/ScanBackgroundService.cs b/listenarr.infrastructure/HostedServices/Audiobooks/ScanBackgroundService.cs similarity index 99% rename from listenarr.application/Audiobooks/ScanBackgroundService.cs rename to listenarr.infrastructure/HostedServices/Audiobooks/ScanBackgroundService.cs index 7ffa3e857..b81119c04 100644 --- a/listenarr.application/Audiobooks/ScanBackgroundService.cs +++ b/listenarr.infrastructure/HostedServices/Audiobooks/ScanBackgroundService.cs @@ -23,12 +23,10 @@ using Listenarr.Application.Security; using Listenarr.Domain.Common; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Audiobooks +namespace Listenarr.Infrastructure.HostedServices.Audiobooks { public class ScanBackgroundService : BackgroundService { @@ -684,4 +682,3 @@ private string GetCommonPath(List paths) } - diff --git a/listenarr.application/Audiobooks/SeriesMonitoringBackgroundService.cs b/listenarr.infrastructure/HostedServices/Audiobooks/SeriesMonitoringBackgroundService.cs similarity index 97% rename from listenarr.application/Audiobooks/SeriesMonitoringBackgroundService.cs rename to listenarr.infrastructure/HostedServices/Audiobooks/SeriesMonitoringBackgroundService.cs index 69ca84e3f..5f999f867 100644 --- a/listenarr.application/Audiobooks/SeriesMonitoringBackgroundService.cs +++ b/listenarr.infrastructure/HostedServices/Audiobooks/SeriesMonitoringBackgroundService.cs @@ -17,10 +17,9 @@ */ using Listenarr.Application.Interfaces; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Audiobooks +namespace Listenarr.Infrastructure.HostedServices.Audiobooks { public class SeriesMonitoringBackgroundService : BackgroundService { diff --git a/listenarr.application/Audiobooks/UnmatchedScanBackgroundService.cs b/listenarr.infrastructure/HostedServices/Audiobooks/UnmatchedScanBackgroundService.cs similarity index 98% rename from listenarr.application/Audiobooks/UnmatchedScanBackgroundService.cs rename to listenarr.infrastructure/HostedServices/Audiobooks/UnmatchedScanBackgroundService.cs index e1885bc82..a2d72b7a0 100644 --- a/listenarr.application/Audiobooks/UnmatchedScanBackgroundService.cs +++ b/listenarr.infrastructure/HostedServices/Audiobooks/UnmatchedScanBackgroundService.cs @@ -15,19 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.EntityFrameworkCore; using System.Text.RegularExpressions; using Listenarr.Domain.Common; using Listenarr.Application.Interfaces; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.SignalR; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Metadata; -using Listenarr.Application.Notification; -namespace Listenarr.Application.Audiobooks +namespace Listenarr.Infrastructure.HostedServices.Audiobooks { public class UnmatchedScanBackgroundService : BackgroundService { @@ -97,7 +92,7 @@ await _hubContext.Clients.All.SendAsync( { await HandleJobFailureAsync(job.Id, ex, stoppingToken); } - catch (DbUpdateException ex) + catch (PersistenceException ex) { await HandleJobFailureAsync(job.Id, ex, stoppingToken); } diff --git a/listenarr.application/Common/ImageCacheCleanupService.cs b/listenarr.infrastructure/HostedServices/Common/ImageCacheCleanupService.cs similarity index 98% rename from listenarr.application/Common/ImageCacheCleanupService.cs rename to listenarr.infrastructure/HostedServices/Common/ImageCacheCleanupService.cs index f211cc2d3..132298568 100644 --- a/listenarr.application/Common/ImageCacheCleanupService.cs +++ b/listenarr.infrastructure/HostedServices/Common/ImageCacheCleanupService.cs @@ -16,11 +16,10 @@ * along with this program. If not, see . */ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Listenarr.Application.Interfaces; -namespace Listenarr.Application.Common +namespace Listenarr.Infrastructure.HostedServices.Common { /// /// Background service that runs daily to clean up temporary image cache diff --git a/listenarr.application/Downloads/DownloadMonitorService.cs b/listenarr.infrastructure/HostedServices/Downloads/DownloadMonitorService.cs similarity index 99% rename from listenarr.application/Downloads/DownloadMonitorService.cs rename to listenarr.infrastructure/HostedServices/Downloads/DownloadMonitorService.cs index 2bd485210..fdb8d3730 100644 --- a/listenarr.application/Downloads/DownloadMonitorService.cs +++ b/listenarr.infrastructure/HostedServices/Downloads/DownloadMonitorService.cs @@ -22,10 +22,9 @@ using Listenarr.Domain.Models; using Listenarr.Domain.Models.Exceptions; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Downloads +namespace Listenarr.Infrastructure.HostedServices.Downloads { /// /// Background service that monitors downloads diff --git a/listenarr.application/Downloads/DownloadProcessingJobProcessor.cs b/listenarr.infrastructure/HostedServices/Downloads/DownloadProcessingJobProcessor.cs similarity index 99% rename from listenarr.application/Downloads/DownloadProcessingJobProcessor.cs rename to listenarr.infrastructure/HostedServices/Downloads/DownloadProcessingJobProcessor.cs index 7f7edb800..5e684a604 100644 --- a/listenarr.application/Downloads/DownloadProcessingJobProcessor.cs +++ b/listenarr.infrastructure/HostedServices/Downloads/DownloadProcessingJobProcessor.cs @@ -18,12 +18,11 @@ using Listenarr.Application.Interfaces; using Listenarr.Domain.Models.Exceptions; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Hosting; using Listenarr.Domain.Models; using Microsoft.Extensions.DependencyInjection; using Listenarr.Application.Interfaces.Repositories; -namespace Listenarr.Application.Downloads +namespace Listenarr.Infrastructure.HostedServices.Downloads { /// /// Process the download processing jobs queued diff --git a/listenarr.application/Downloads/MovedDownloadProcessor.cs b/listenarr.infrastructure/HostedServices/Downloads/MovedDownloadProcessor.cs similarity index 99% rename from listenarr.application/Downloads/MovedDownloadProcessor.cs rename to listenarr.infrastructure/HostedServices/Downloads/MovedDownloadProcessor.cs index 783857cb5..f09eaa5e1 100644 --- a/listenarr.application/Downloads/MovedDownloadProcessor.cs +++ b/listenarr.infrastructure/HostedServices/Downloads/MovedDownloadProcessor.cs @@ -20,10 +20,9 @@ using Listenarr.Application.Interfaces.Repositories; using Listenarr.Domain.Models; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Downloads +namespace Listenarr.Infrastructure.HostedServices.Downloads { /// /// Background service that handles moved downloads to remove them from client @@ -306,4 +305,3 @@ private async Task ProcessDeferredRemovalsAsync( } } } - diff --git a/listenarr.application/Downloads/QueueMonitorService.cs b/listenarr.infrastructure/HostedServices/Downloads/QueueMonitorService.cs similarity index 98% rename from listenarr.application/Downloads/QueueMonitorService.cs rename to listenarr.infrastructure/HostedServices/Downloads/QueueMonitorService.cs index 81d78b8dc..d15bab632 100644 --- a/listenarr.application/Downloads/QueueMonitorService.cs +++ b/listenarr.infrastructure/HostedServices/Downloads/QueueMonitorService.cs @@ -17,14 +17,11 @@ */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Downloads +namespace Listenarr.Infrastructure.HostedServices.Downloads { /// /// Background service that polls external download client queues and pushes updates via SignalR @@ -229,4 +226,3 @@ private bool HasQueueChanged(QueueSnapshot oldSnapshot, QueueSnapshot newSnapsho } } - diff --git a/listenarr.application/Metadata/MetadataRescanService.cs b/listenarr.infrastructure/HostedServices/Metadata/MetadataRescanService.cs similarity index 99% rename from listenarr.application/Metadata/MetadataRescanService.cs rename to listenarr.infrastructure/HostedServices/Metadata/MetadataRescanService.cs index e4df286a9..3e08efcb3 100644 --- a/listenarr.application/Metadata/MetadataRescanService.cs +++ b/listenarr.infrastructure/HostedServices/Metadata/MetadataRescanService.cs @@ -21,10 +21,9 @@ using Listenarr.Application.Security; using Listenarr.Domain.Common; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Metadata +namespace Listenarr.Infrastructure.HostedServices.Metadata { // Background hosted service to rescan files missing metadata and populate DB fields public class MetadataRescanService : BackgroundService diff --git a/listenarr.application/Search/AutomaticSearchService.cs b/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs similarity index 99% rename from listenarr.application/Search/AutomaticSearchService.cs rename to listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs index 2e48bea4e..2226813c3 100644 --- a/listenarr.application/Search/AutomaticSearchService.cs +++ b/listenarr.infrastructure/HostedServices/Search/AutomaticSearchService.cs @@ -18,14 +18,11 @@ using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Notification; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Search +namespace Listenarr.Infrastructure.HostedServices.Search { public class AutomaticSearchService : BackgroundService { @@ -672,4 +669,3 @@ private async Task GetAppropriateDownloadClientAsync(SearchResult search } } } - diff --git a/listenarr.infrastructure/Listenarr.Infrastructure.csproj b/listenarr.infrastructure/Listenarr.Infrastructure.csproj index 21776a55f..2fab32af2 100644 --- a/listenarr.infrastructure/Listenarr.Infrastructure.csproj +++ b/listenarr.infrastructure/Listenarr.Infrastructure.csproj @@ -11,19 +11,22 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/listenarr.infrastructure/OpenLibrary/OpenLibraryService.cs b/listenarr.infrastructure/OpenLibrary/OpenLibraryService.cs index f390e6505..744ecb7cf 100644 --- a/listenarr.infrastructure/OpenLibrary/OpenLibraryService.cs +++ b/listenarr.infrastructure/OpenLibrary/OpenLibraryService.cs @@ -20,7 +20,6 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Listenarr.Application.Security; -using Listenarr.Application.Search; namespace Listenarr.Infrastructure.OpenLibrary { diff --git a/listenarr.infrastructure/Persistence/ListenArrDbContext.cs b/listenarr.infrastructure/Persistence/ListenArrDbContext.cs index b9af5ef24..acfdcb2c8 100644 --- a/listenarr.infrastructure/Persistence/ListenArrDbContext.cs +++ b/listenarr.infrastructure/Persistence/ListenArrDbContext.cs @@ -53,6 +53,41 @@ public ListenArrDbContext(DbContextOptions options) { } + public override int SaveChanges() + { + try + { + return base.SaveChanges(); + } + catch (DbUpdateException ex) + { + throw TranslatePersistenceException(ex); + } + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + try + { + return await base.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) + { + throw TranslatePersistenceException(ex); + } + } + + private static PersistenceException TranslatePersistenceException(DbUpdateException ex) + { + var message = ex.InnerException?.Message ?? ex.Message; + if (message.IndexOf("UNIQUE", StringComparison.OrdinalIgnoreCase) >= 0) + { + return new UniqueConstraintViolationException("A unique persistence constraint was violated.", ex); + } + + return new PersistenceException("A persistence operation failed.", ex); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Only configure SQLite if no provider was configured externally (e.g. tests using InMemory) diff --git a/listenarr.infrastructure/Persistence/Repositories/EfAudiobookFileRepository.cs b/listenarr.infrastructure/Persistence/Repositories/EfAudiobookFileRepository.cs index eb018e471..63014f389 100644 --- a/listenarr.infrastructure/Persistence/Repositories/EfAudiobookFileRepository.cs +++ b/listenarr.infrastructure/Persistence/Repositories/EfAudiobookFileRepository.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Audiobooks; using Listenarr.Domain.Models; using Microsoft.EntityFrameworkCore; diff --git a/listenarr.infrastructure/Persistence/StartupDbNormalizer.cs b/listenarr.infrastructure/Persistence/StartupDbNormalizer.cs index bb79edfe7..a2cad7d0d 100644 --- a/listenarr.infrastructure/Persistence/StartupDbNormalizer.cs +++ b/listenarr.infrastructure/Persistence/StartupDbNormalizer.cs @@ -18,7 +18,6 @@ using System.Diagnostics; using Listenarr.Application.Interfaces.Repositories; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.Persistence diff --git a/listenarr.infrastructure/Platform/ApplicationVersionService.cs b/listenarr.infrastructure/Platform/ApplicationVersionService.cs index 45c013007..0029c942f 100644 --- a/listenarr.infrastructure/Platform/ApplicationVersionService.cs +++ b/listenarr.infrastructure/Platform/ApplicationVersionService.cs @@ -19,7 +19,6 @@ using System.Diagnostics; using System.Reflection; using Listenarr.Application.Interfaces; -using Microsoft.Extensions.Hosting; namespace Listenarr.Infrastructure.Platform { diff --git a/listenarr.infrastructure/Search/Providers/InternetArchiveSearchProvider.cs b/listenarr.infrastructure/Search/Providers/InternetArchiveSearchProvider.cs index 544b43111..7adb52ab8 100644 --- a/listenarr.infrastructure/Search/Providers/InternetArchiveSearchProvider.cs +++ b/listenarr.infrastructure/Search/Providers/InternetArchiveSearchProvider.cs @@ -18,7 +18,6 @@ using System.Text.Json; using Listenarr.Application.Interfaces; -using Listenarr.Application.Search; using Listenarr.Domain.Models; using Microsoft.Extensions.Logging; diff --git a/listenarr.infrastructure/Search/Providers/MyAnonamouseSearchProvider.cs b/listenarr.infrastructure/Search/Providers/MyAnonamouseSearchProvider.cs index f485fc357..3a1420849 100644 --- a/listenarr.infrastructure/Search/Providers/MyAnonamouseSearchProvider.cs +++ b/listenarr.infrastructure/Search/Providers/MyAnonamouseSearchProvider.cs @@ -15,7 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Common; using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Domain.Models; @@ -23,7 +22,6 @@ using System.Text.RegularExpressions; using AsyncKeyedLock; using Microsoft.Extensions.Logging; -using Listenarr.Application.Search; using Listenarr.Application.Security; namespace Listenarr.Infrastructure.Search.Providers diff --git a/listenarr.infrastructure/Search/Providers/TorznabNewznabSearchProvider.cs b/listenarr.infrastructure/Search/Providers/TorznabNewznabSearchProvider.cs index 14bcfd832..e3452b81a 100644 --- a/listenarr.infrastructure/Search/Providers/TorznabNewznabSearchProvider.cs +++ b/listenarr.infrastructure/Search/Providers/TorznabNewznabSearchProvider.cs @@ -19,7 +19,6 @@ using System.Text.RegularExpressions; using HtmlAgilityPack; using Listenarr.Application.Interfaces; -using Listenarr.Application.Search; using Listenarr.Application.Security; using Listenarr.Domain.Models; using Microsoft.Extensions.Logging; diff --git a/listenarr.infrastructure/Security/DataProtectionSecretProtector.cs b/listenarr.infrastructure/Security/DataProtectionSecretProtector.cs new file mode 100644 index 000000000..a49d36414 --- /dev/null +++ b/listenarr.infrastructure/Security/DataProtectionSecretProtector.cs @@ -0,0 +1,27 @@ +/* + * 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. + */ +using Listenarr.Application.Interfaces; + +namespace Listenarr.Infrastructure.Security +{ + public sealed class DataProtectionSecretProtector : ISecretProtector + { + private readonly IDataProtector _protector; + + public DataProtectionSecretProtector(IDataProtectionProvider provider) + { + _protector = provider.CreateProtector("Listenarr.ConfigurationService.ProwlarrImport"); + } + + public string Protect(string plaintext) => _protector.Protect(plaintext); + + public string Unprotect(string protectedValue) => _protector.Unprotect(protectedValue); + } +} diff --git a/listenarr.infrastructure/Services/HtmlAgilityPackAudibleAuthorPageParser.cs b/listenarr.infrastructure/Services/HtmlAgilityPackAudibleAuthorPageParser.cs new file mode 100644 index 000000000..c8272e95a --- /dev/null +++ b/listenarr.infrastructure/Services/HtmlAgilityPackAudibleAuthorPageParser.cs @@ -0,0 +1,223 @@ +/* + * 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 System.Text.Json; +using HtmlAgilityPack; +using Listenarr.Application.Interfaces; + +namespace Listenarr.Infrastructure.Services +{ + public class HtmlAgilityPackAudibleAuthorPageParser : IAudibleAuthorPageParser + { + public List ParseAuthorPage(string html, string author, string authorAsin, string region) + { + if (string.IsNullOrWhiteSpace(html)) + { + return new List(); + } + + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + var parsedTiles = new List(); + var seenAsins = new HashSet(StringComparer.OrdinalIgnoreCase); + var tiles = htmlDoc.DocumentNode.SelectNodes("//adbl-full-width-product-tile"); + var legacyProductListItems = htmlDoc.DocumentNode.SelectNodes("//li[contains(@class, 'productListItem')]"); + + if (tiles != null) + { + foreach (var tile in tiles) + { + AddParsedResult(parsedTiles, seenAsins, ParseAudibleAuthorTile(tile, author, authorAsin, region)); + } + } + + if (legacyProductListItems != null) + { + foreach (var item in legacyProductListItems) + { + AddParsedResult(parsedTiles, seenAsins, ParseAudibleAuthorListItem(item, author, authorAsin, region)); + } + } + + return parsedTiles; + } + + private static void AddParsedResult(List results, HashSet seenAsins, AudibleSearchResult? parsed) + { + if (parsed == null) + { + return; + } + + var key = string.IsNullOrWhiteSpace(parsed.Asin) + ? $"{parsed.Title}|{parsed.Link}" + : parsed.Asin; + if (seenAsins.Add(key)) + { + results.Add(parsed); + } + } + + private static AudibleSearchResult? ParseAudibleAuthorTile(HtmlNode tile, string author, string authorAsin, string region) + { + var productImageNode = tile.SelectSingleNode(".//adbl-product-image") + ?? tile.SelectSingleNode(".//adbl-full-bleed-image"); + var asin = productImageNode?.GetAttributeValue("data-asin", string.Empty); + if (string.IsNullOrWhiteSpace(asin)) + { + asin = tile.SelectSingleNode(".//*[@data-asin]")?.GetAttributeValue("data-asin", string.Empty); + } + if (string.IsNullOrWhiteSpace(asin)) return null; + + var title = HtmlEntity.DeEntitize(tile.SelectSingleNode(".//*[@slot='title']")?.InnerText ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(title)) return null; + + var subtitle = HtmlEntity.DeEntitize(tile.SelectSingleNode(".//*[@slot='subtitle']")?.InnerText ?? string.Empty).Trim(); + var imageUrl = productImageNode?.SelectSingleNode(".//img")?.GetAttributeValue("src", string.Empty); + if (string.IsNullOrWhiteSpace(imageUrl)) + { + imageUrl = productImageNode?.GetAttributeValue("portrait-src", string.Empty); + } + if (string.IsNullOrWhiteSpace(imageUrl)) + { + imageUrl = productImageNode?.GetAttributeValue("landscape-src", string.Empty); + } + + var relativeUrl = productImageNode?.GetAttributeValue("data-url", string.Empty); + if (string.IsNullOrWhiteSpace(relativeUrl)) + { + relativeUrl = tile.SelectSingleNode(".//adbl-button[@href]")?.GetAttributeValue("href", string.Empty) + ?? tile.SelectSingleNode(".//a[@href]")?.GetAttributeValue("href", string.Empty); + } + + var authors = ParseAudibleAuthorTileAuthors(tile, author, authorAsin, region); + if (authors.Count == 0 && !string.IsNullOrWhiteSpace(author)) + { + authors.Add(new AudibleAuthor { Asin = authorAsin, Name = author, Region = region }); + } + + return new AudibleSearchResult + { + Asin = asin, + Title = title, + Subtitle = string.IsNullOrWhiteSpace(subtitle) ? null : subtitle, + Authors = authors, + ImageUrl = string.IsNullOrWhiteSpace(imageUrl) ? null : imageUrl, + Link = NormalizeAudibleUrl(relativeUrl, region) + }; + } + + private static AudibleSearchResult? ParseAudibleAuthorListItem(HtmlNode listItem, string author, string authorAsin, string region) + { + var asin = listItem.SelectSingleNode(".//*[@data-asin]")?.GetAttributeValue("data-asin", string.Empty); + if (string.IsNullOrWhiteSpace(asin)) + { + return null; + } + + var title = HtmlEntity.DeEntitize(listItem.GetAttributeValue("aria-label", string.Empty)).Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + title = HtmlEntity.DeEntitize( + listItem.SelectSingleNode(".//h2")?.InnerText ?? string.Empty).Trim(); + } + + if (string.IsNullOrWhiteSpace(title)) + { + return null; + } + + var imageUrl = listItem.SelectSingleNode(".//img[@src]")?.GetAttributeValue("src", string.Empty); + var relativeUrl = listItem.SelectSingleNode(".//a[@href]")?.GetAttributeValue("href", string.Empty); + + return new AudibleSearchResult + { + Asin = asin, + Title = title, + Authors = new List + { + new() + { + Asin = authorAsin, + Name = author, + Region = region + } + }, + ImageUrl = string.IsNullOrWhiteSpace(imageUrl) ? null : imageUrl, + Link = NormalizeAudibleUrl(relativeUrl, region) + }; + } + + private static List ParseAudibleAuthorTileAuthors(HtmlNode tile, string author, string authorAsin, string region) + { + var authors = new List(); + var metadataJson = tile.SelectSingleNode(".//adbl-product-metadata/script[@type='application/json']")?.InnerText; + if (string.IsNullOrWhiteSpace(metadataJson)) return authors; + + try + { + var metadata = JsonSerializer.Deserialize(metadataJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (metadata?.Authors == null) return authors; + + foreach (var metadataAuthor in metadata.Authors.Where(metadataAuthor => !string.IsNullOrWhiteSpace(metadataAuthor.Name))) + { + authors.Add(new AudibleAuthor + { + Asin = string.Equals(metadataAuthor.Name, author, StringComparison.OrdinalIgnoreCase) ? authorAsin : null, + Name = metadataAuthor.Name, + Region = region + }); + } + } + catch (JsonException) + { + } + + return authors; + } + + private static string? NormalizeAudibleUrl(string? url, string region) + { + if (string.IsNullOrWhiteSpace(url)) return null; + if (Uri.TryCreate(url, UriKind.Absolute, out var absoluteUri) + && !string.Equals(absoluteUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + { + return absoluteUri.ToString(); + } + return $"{GetAudibleBaseUrl(region)}{url}"; + } + + private static string GetAudibleBaseUrl(string region) + { + return region?.Trim().ToLowerInvariant() switch + { + "au" => "https://www.audible.com.au", + "ca" => "https://www.audible.ca", + "de" => "https://www.audible.de", + "es" => "https://www.audible.es", + "fr" => "https://www.audible.fr", + "in" => "https://www.audible.in", + "it" => "https://www.audible.it", + "jp" => "https://www.audible.co.jp", + "uk" => "https://www.audible.co.uk", + _ => "https://www.audible.com" + }; + } + } +} diff --git a/listenarr.infrastructure/Services/HtmlAgilityPackTextExtractor.cs b/listenarr.infrastructure/Services/HtmlAgilityPackTextExtractor.cs new file mode 100644 index 000000000..6933a17d3 --- /dev/null +++ b/listenarr.infrastructure/Services/HtmlAgilityPackTextExtractor.cs @@ -0,0 +1,38 @@ +/* + * 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 HtmlAgilityPack; +using Listenarr.Application.Interfaces; + +namespace Listenarr.Infrastructure.Services +{ + public class HtmlAgilityPackTextExtractor : IHtmlTextExtractor + { + public string ExtractText(string html) + { + if (string.IsNullOrWhiteSpace(html)) + { + return string.Empty; + } + + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + return htmlDoc.DocumentNode.InnerText; + } + } +} diff --git a/listenarr.infrastructure/Services/ImageSharpCoverImageProbe.cs b/listenarr.infrastructure/Services/ImageSharpCoverImageProbe.cs new file mode 100644 index 000000000..9f491aac2 --- /dev/null +++ b/listenarr.infrastructure/Services/ImageSharpCoverImageProbe.cs @@ -0,0 +1,55 @@ +/* + * 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 Listenarr.Application.Interfaces; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; + +namespace Listenarr.Infrastructure.Services +{ + public class ImageSharpCoverImageProbe : ICoverImageProbe + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public ImageSharpCoverImageProbe(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task ProbeAsync(string url, CancellationToken cancellationToken = default) + { + try + { + using var resp = await _httpClient.GetAsync(url, cancellationToken); + if (!resp.IsSuccessStatusCode) + return null; + + using var ms = new MemoryStream(await resp.Content.ReadAsByteArrayAsync(cancellationToken)); + using var img = Image.Load(ms); + return img.Height == 0 ? null : new ImageDimensions(img.Width, img.Height); + } + catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) + { + _logger.LogDebug(ex, "Failed to measure image dimensions for cover {Url}", url); + return null; + } + } + } +} diff --git a/listenarr.infrastructure/Services/TagLibAudioTagWriter.cs b/listenarr.infrastructure/Services/TagLibAudioTagWriter.cs new file mode 100644 index 000000000..444cb5167 --- /dev/null +++ b/listenarr.infrastructure/Services/TagLibAudioTagWriter.cs @@ -0,0 +1,66 @@ +/* + * 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 Listenarr.Application.Interfaces; +using Listenarr.Application.Security; +using Microsoft.Extensions.Logging; + +namespace Listenarr.Infrastructure.Services +{ + public class TagLibAudioTagWriter : IAudioTagWriter + { + private readonly ILogger _logger; + + public TagLibAudioTagWriter(ILogger logger) + { + _logger = logger; + } + + public Task WriteAsinTagAsync(string filePath, string asin) + { + if (string.IsNullOrWhiteSpace(filePath) || string.IsNullOrWhiteSpace(asin)) + return Task.CompletedTask; + + try + { + using var file = TagLib.File.Create(filePath); + + if (file.Tag is TagLib.Mpeg4.AppleTag appleTag) + appleTag.SetDashBox("com.apple.iTunes", "ASIN", asin); + else if (file.GetTag(TagLib.TagTypes.Id3v2) is TagLib.Id3v2.Tag id3Tag) + { + var frame = TagLib.Id3v2.UserTextInformationFrame.Get(id3Tag, "ASIN", true); + frame.Text = new[] { asin }; + } + else if (file.GetTag(TagLib.TagTypes.Xiph) is TagLib.Ogg.XiphComment xiph) + xiph.SetField("ASIN", asin); + else + return Task.CompletedTask; + + file.Save(); + _logger.LogDebug("Wrote ASIN tag '{Asin}' to {File}", asin, LogRedaction.SanitizeFilePath(filePath)); + } + catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) + { + _logger.LogWarning(ex, "Failed to write ASIN tag to {File} - import will continue", LogRedaction.SanitizeFilePath(filePath)); + } + + return Task.CompletedTask; + } + } +} diff --git a/listenarr.application/Notification/DownloadHub.cs b/listenarr.infrastructure/SignalR/DownloadHub.cs similarity index 97% rename from listenarr.application/Notification/DownloadHub.cs rename to listenarr.infrastructure/SignalR/DownloadHub.cs index 83e423bc1..d71ad48cf 100644 --- a/listenarr.application/Notification/DownloadHub.cs +++ b/listenarr.infrastructure/SignalR/DownloadHub.cs @@ -18,10 +18,9 @@ using Listenarr.Application.Interfaces; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Notification +namespace Listenarr.Infrastructure.SignalR { /// /// SignalR hub for real-time download progress updates @@ -73,4 +72,3 @@ public async Task PushDownloadUpdate(Download download) } } - diff --git a/listenarr.infrastructure/SignalR/DownloadPushService.cs b/listenarr.infrastructure/SignalR/DownloadPushService.cs index b4ad9bbc0..1580ac76b 100644 --- a/listenarr.infrastructure/SignalR/DownloadPushService.cs +++ b/listenarr.infrastructure/SignalR/DownloadPushService.cs @@ -16,9 +16,7 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; diff --git a/listenarr.infrastructure/SignalR/LogHub.cs b/listenarr.infrastructure/SignalR/LogHub.cs index 386ab6d59..3ff40832c 100644 --- a/listenarr.infrastructure/SignalR/LogHub.cs +++ b/listenarr.infrastructure/SignalR/LogHub.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.SignalR diff --git a/listenarr.application/Notification/SettingsHub.cs b/listenarr.infrastructure/SignalR/SettingsHub.cs similarity index 96% rename from listenarr.application/Notification/SettingsHub.cs rename to listenarr.infrastructure/SignalR/SettingsHub.cs index 33c703aee..530f80c86 100644 --- a/listenarr.application/Notification/SettingsHub.cs +++ b/listenarr.infrastructure/SignalR/SettingsHub.cs @@ -16,10 +16,9 @@ * along with this program. If not, see . */ -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -namespace Listenarr.Application.Notification +namespace Listenarr.Infrastructure.SignalR { /// /// SignalR hub for real-time settings updates diff --git a/listenarr.infrastructure/SignalR/SignalRClientRegistry.cs b/listenarr.infrastructure/SignalR/SignalRClientRegistry.cs new file mode 100644 index 000000000..8c3163722 --- /dev/null +++ b/listenarr.infrastructure/SignalR/SignalRClientRegistry.cs @@ -0,0 +1,30 @@ +/* + * 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 Listenarr.Application.Interfaces; + +namespace Listenarr.Infrastructure.SignalR +{ + public sealed class SignalRClientRegistry : IRealtimeClientRegistry + { + public IReadOnlyCollection GetSettingsClientIds() + { + return SettingsHub.ConnectedClientIds.ToArray(); + } + } +} diff --git a/listenarr.infrastructure/SignalR/SignalRHubBroadcaster.cs b/listenarr.infrastructure/SignalR/SignalRHubBroadcaster.cs index eb23d226b..d9c8c9538 100644 --- a/listenarr.infrastructure/SignalR/SignalRHubBroadcaster.cs +++ b/listenarr.infrastructure/SignalR/SignalRHubBroadcaster.cs @@ -16,20 +16,23 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.SignalR { public class SignalRHubBroadcaster : IHubBroadcaster { - private readonly IHubContext _hubContext; + private readonly IHubContext _downloadHubContext; + private readonly IHubContext? _settingsHubContext; private readonly ILogger _logger; - public SignalRHubBroadcaster(IHubContext hubContext, ILogger logger) + public SignalRHubBroadcaster( + IHubContext downloadHubContext, + ILogger logger, + IHubContext? settingsHubContext = null) { - _hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext)); + _downloadHubContext = downloadHubContext ?? throw new ArgumentNullException(nameof(downloadHubContext)); + _settingsHubContext = settingsHubContext; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -38,7 +41,7 @@ public async Task BroadcastQueueUpdateAsync(Domain.Models.QueueSnapshot queueSna try { // Primary, public API - var clientProxy = _hubContext.Clients.All; + var clientProxy = _downloadHubContext.Clients.All; await clientProxy.SendAsync("QueueUpdate", queueSnapshot); // Some tests/mocks expect SendCoreAsync; call as a compatibility step @@ -56,6 +59,35 @@ public async Task BroadcastQueueUpdateAsync(Domain.Models.QueueSnapshot queueSna _logger.LogWarning(ex, "Failed to broadcast QueueUpdate"); } } + + public async Task BroadcastAsync(string eventName, object payload, CancellationToken cancellationToken = default) + { + await BroadcastAsync(RealtimeHubTarget.Downloads, eventName, payload, cancellationToken); + } + + public async Task BroadcastAsync(RealtimeHubTarget target, string eventName, object payload, CancellationToken cancellationToken = default) + { + try + { + if (target == RealtimeHubTarget.Settings && _settingsHubContext is null) + { + _logger.LogWarning("Cannot broadcast {EventName} to {HubTarget} because the settings hub context is not registered", eventName, target); + return; + } + + var clientProxy = target switch + { + RealtimeHubTarget.Downloads => _downloadHubContext.Clients.All, + RealtimeHubTarget.Settings => _settingsHubContext!.Clients.All, + _ => _downloadHubContext.Clients.All + }; + + await clientProxy.SendAsync(eventName, payload, cancellationToken); + } + catch (Exception ex) when (ex is not OperationCanceledException && ex is not OutOfMemoryException && ex is not StackOverflowException) + { + _logger.LogWarning(ex, "Failed to broadcast {EventName} to {HubTarget}", eventName, target); + } + } } } - diff --git a/listenarr.infrastructure/SignalR/SignalRLogSink.cs b/listenarr.infrastructure/SignalR/SignalRLogSink.cs index 36d933499..fec1fd1ea 100644 --- a/listenarr.infrastructure/SignalR/SignalRLogSink.cs +++ b/listenarr.infrastructure/SignalR/SignalRLogSink.cs @@ -18,7 +18,6 @@ using System.Diagnostics; using Listenarr.Domain.Models; -using Microsoft.AspNetCore.SignalR; using Serilog.Core; using Serilog.Events; diff --git a/listenarr.infrastructure/SignalR/ToastService.cs b/listenarr.infrastructure/SignalR/ToastService.cs index 2a051cc4f..94996ef6e 100644 --- a/listenarr.infrastructure/SignalR/ToastService.cs +++ b/listenarr.infrastructure/SignalR/ToastService.cs @@ -16,8 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace Listenarr.Infrastructure.SignalR diff --git a/listenarr.infrastructure/Web/AspNetRequestContextAccessor.cs b/listenarr.infrastructure/Web/AspNetRequestContextAccessor.cs new file mode 100644 index 000000000..a5ae17297 --- /dev/null +++ b/listenarr.infrastructure/Web/AspNetRequestContextAccessor.cs @@ -0,0 +1,47 @@ +/* + * 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. + */ +using Listenarr.Application.Interfaces; + +namespace Listenarr.Infrastructure.Web +{ + public sealed class AspNetRequestContextAccessor : IRequestContextAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public AspNetRequestContextAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public RequestContextSnapshot? Current + { + get + { + var context = _httpContextAccessor.HttpContext; + if (context == null) + { + return null; + } + + var user = context.User; + var isAuthenticatedAdminOrApiKey = user?.Identity?.IsAuthenticated == true + && (user.IsInRole("Administrator") + || string.Equals(user.FindFirst("AuthMethod")?.Value, "ApiKey", StringComparison.Ordinal)); + + return new RequestContextSnapshot( + context.Request.Path.Value, + context.Request.Scheme, + context.Request.Host.Value, + context.Connection.RemoteIpAddress, + isAuthenticatedAdminOrApiKey); + } + } + } +} diff --git a/tests/Builders/ApplicationSettingsBuilder.cs b/tests/Builders/ApplicationSettingsBuilder.cs index fa406ac03..bbbdfab3b 100644 --- a/tests/Builders/ApplicationSettingsBuilder.cs +++ b/tests/Builders/ApplicationSettingsBuilder.cs @@ -1,4 +1,3 @@ -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; namespace Listenarr.Tests.Builders diff --git a/tests/Builders/AudioMetadataBuilder.cs b/tests/Builders/AudioMetadataBuilder.cs index 5e8fa0405..acbad7820 100644 --- a/tests/Builders/AudioMetadataBuilder.cs +++ b/tests/Builders/AudioMetadataBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class AudioMetadataBuilder diff --git a/tests/Builders/AudiobookBuilder.cs b/tests/Builders/AudiobookBuilder.cs index 36e3bbe41..366ef0741 100644 --- a/tests/Builders/AudiobookBuilder.cs +++ b/tests/Builders/AudiobookBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class AudiobookBuilder diff --git a/tests/Builders/AudiobookFileBuilder.cs b/tests/Builders/AudiobookFileBuilder.cs index e677fda3e..74a484d45 100644 --- a/tests/Builders/AudiobookFileBuilder.cs +++ b/tests/Builders/AudiobookFileBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class AudiobookFileBuilder diff --git a/tests/Builders/DownloadBuilder.cs b/tests/Builders/DownloadBuilder.cs index f2ab4a9d8..75039c44a 100644 --- a/tests/Builders/DownloadBuilder.cs +++ b/tests/Builders/DownloadBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class DownloadBuilder diff --git a/tests/Builders/DownloadClientConfigurationBuilder.cs b/tests/Builders/DownloadClientConfigurationBuilder.cs index 5e982171d..69e69cddc 100644 --- a/tests/Builders/DownloadClientConfigurationBuilder.cs +++ b/tests/Builders/DownloadClientConfigurationBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class DownloadClientConfigurationBuilder diff --git a/tests/Builders/DownloadProcessingJobBuilder.cs b/tests/Builders/DownloadProcessingJobBuilder.cs index c32b09c2e..9add73a81 100644 --- a/tests/Builders/DownloadProcessingJobBuilder.cs +++ b/tests/Builders/DownloadProcessingJobBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class DownloadProcessingJobBuilder diff --git a/tests/Builders/IndexerBuilder.cs b/tests/Builders/IndexerBuilder.cs index 8962ef7d9..f6d9877e5 100644 --- a/tests/Builders/IndexerBuilder.cs +++ b/tests/Builders/IndexerBuilder.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Listenarr.Domain.Models; namespace Listenarr.Tests.Builders { diff --git a/tests/Builders/QualityProfileBuilder.cs b/tests/Builders/QualityProfileBuilder.cs index 9c2b90ace..77fb6fb0f 100644 --- a/tests/Builders/QualityProfileBuilder.cs +++ b/tests/Builders/QualityProfileBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class QualityProfileBuilder diff --git a/tests/Builders/QueueItemBuilder.cs b/tests/Builders/QueueItemBuilder.cs index 537c450fb..35b2b110d 100644 --- a/tests/Builders/QueueItemBuilder.cs +++ b/tests/Builders/QueueItemBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class QueueItemBuilder diff --git a/tests/Builders/RemotePathMappingBuilder.cs b/tests/Builders/RemotePathMappingBuilder.cs index 5349c4300..17faafe34 100644 --- a/tests/Builders/RemotePathMappingBuilder.cs +++ b/tests/Builders/RemotePathMappingBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class RemotePathMappingBuilder diff --git a/tests/Builders/RootFolderBuilder.cs b/tests/Builders/RootFolderBuilder.cs index 0dfdb1372..38f40b271 100644 --- a/tests/Builders/RootFolderBuilder.cs +++ b/tests/Builders/RootFolderBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class RootFolderBuilder diff --git a/tests/Builders/SearchResultBuilder.cs b/tests/Builders/SearchResultBuilder.cs index 60a9e2659..67e0dd364 100644 --- a/tests/Builders/SearchResultBuilder.cs +++ b/tests/Builders/SearchResultBuilder.cs @@ -1,5 +1,3 @@ -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Builders { public class SearchResultBuilder diff --git a/tests/Builders/SeriesCatalogFetchResultBuilder.cs b/tests/Builders/SeriesCatalogFetchResultBuilder.cs index 95b6deb18..a9b99e21b 100644 --- a/tests/Builders/SeriesCatalogFetchResultBuilder.cs +++ b/tests/Builders/SeriesCatalogFetchResultBuilder.cs @@ -1,4 +1,3 @@ -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; namespace Listenarr.Tests.Builders diff --git a/tests/Builders/ServiceCollectionBuilder.cs b/tests/Builders/ServiceCollectionBuilder.cs index 63bfd501e..0b18f4d13 100644 --- a/tests/Builders/ServiceCollectionBuilder.cs +++ b/tests/Builders/ServiceCollectionBuilder.cs @@ -1,14 +1,10 @@ using Listenarr.Api.Controllers; using Listenarr.Application.Audiobooks; -using Listenarr.Application.Common; using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; -using Listenarr.Application.Notification; using Listenarr.Application.Search; using Listenarr.Application.Search.Filters; using Listenarr.Application.Search.Strategies; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Extensions; using Listenarr.Infrastructure.FileSystem; using Listenarr.Tests.Mocks; @@ -17,9 +13,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Moq; namespace Listenarr.Tests.Builders { diff --git a/tests/Common/BaseTests.cs b/tests/Common/BaseTests.cs index 06dc8fbcf..622958cb7 100644 --- a/tests/Common/BaseTests.cs +++ b/tests/Common/BaseTests.cs @@ -1,11 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Tests.Builders; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Common { diff --git a/tests/Common/MockUtils.cs b/tests/Common/MockUtils.cs index 387490806..238f4a285 100644 --- a/tests/Common/MockUtils.cs +++ b/tests/Common/MockUtils.cs @@ -1,12 +1,7 @@ using System.Net; using System.Text; using Listenarr.Api.Controllers; -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Notification; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; using Listenarr.Infrastructure.Adapters; using Listenarr.Infrastructure.Search.Providers; @@ -14,9 +9,6 @@ using Listenarr.Tests.Builders; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Moq; namespace Listenarr.Tests.Common { diff --git a/tests/Common/TempFileService.cs b/tests/Common/TempFileService.cs index b4b07cd77..44646c03c 100644 --- a/tests/Common/TempFileService.cs +++ b/tests/Common/TempFileService.cs @@ -1,5 +1,3 @@ -using Xunit; - namespace Listenarr.Tests.Common { public class TempFileService : IAsyncLifetime diff --git a/tests/Common/TestUtils.cs b/tests/Common/TestUtils.cs index 27b518087..876bb919f 100644 --- a/tests/Common/TestUtils.cs +++ b/tests/Common/TestUtils.cs @@ -1,8 +1,6 @@ using System.Runtime.CompilerServices; using Asp.Versioning.ApiExplorer; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; -using Microsoft.Extensions.DependencyInjection; namespace Listenarr.Tests.Common { diff --git a/tests/Features/Api/Controllers/ConfigurationControllerDownloadClientTests.cs b/tests/Features/Api/Controllers/ConfigurationControllerDownloadClientTests.cs index 66384cf80..f6440687b 100644 --- a/tests/Features/Api/Controllers/ConfigurationControllerDownloadClientTests.cs +++ b/tests/Features/Api/Controllers/ConfigurationControllerDownloadClientTests.cs @@ -19,19 +19,13 @@ using Listenarr.Api.Attributes; using Listenarr.Api.Controllers; using Listenarr.Api.Controllers.Configurations; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; -using Listenarr.Domain.Models; using Listenarr.Tests.Mocks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ConfigurationControllerSettingsTests.cs b/tests/Features/Api/Controllers/ConfigurationControllerSettingsTests.cs index 84842d6d6..0c01bdd4a 100644 --- a/tests/Features/Api/Controllers/ConfigurationControllerSettingsTests.cs +++ b/tests/Features/Api/Controllers/ConfigurationControllerSettingsTests.cs @@ -16,14 +16,8 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers.Configurations; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; -using Listenarr.Domain.Models.Configurations; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { @@ -47,7 +41,7 @@ public async Task GetApplicationSettings_DoesNotReturnEncryptedProwlarrApiKey() var controller = new SettingsController( configurationService.Object, NullLogger.Instance, - Mock.Of>()); + Mock.Of()); var result = await controller.GetApplicationSettings(); var ok = Assert.IsType(result.Result); diff --git a/tests/Features/Api/Controllers/DownloadsControllerTests.cs b/tests/Features/Api/Controllers/DownloadsControllerTests.cs index fe4f96287..208e7d884 100644 --- a/tests/Features/Api/Controllers/DownloadsControllerTests.cs +++ b/tests/Features/Api/Controllers/DownloadsControllerTests.cs @@ -15,11 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Microsoft.AspNetCore.Mvc; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_AlternateAsinCachedImageAliasTests.cs b/tests/Features/Api/Controllers/ImagesController_AlternateAsinCachedImageAliasTests.cs index da54a6523..83065cfc9 100644 --- a/tests/Features/Api/Controllers/ImagesController_AlternateAsinCachedImageAliasTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_AlternateAsinCachedImageAliasTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_AudnexusAuthorByAsinTests.cs b/tests/Features/Api/Controllers/ImagesController_AudnexusAuthorByAsinTests.cs index 23cfa68bc..f6fbe3c2b 100644 --- a/tests/Features/Api/Controllers/ImagesController_AudnexusAuthorByAsinTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_AudnexusAuthorByAsinTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_AuthorFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_AuthorFallbackTests.cs index 201298642..970a6dd6a 100644 --- a/tests/Features/Api/Controllers/ImagesController_AuthorFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_AuthorFallbackTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_AuthorStoredAsinTests.cs b/tests/Features/Api/Controllers/ImagesController_AuthorStoredAsinTests.cs index a8be1cf03..eecf1bd2a 100644 --- a/tests/Features/Api/Controllers/ImagesController_AuthorStoredAsinTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_AuthorStoredAsinTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_ContentRootResolutionTests.cs b/tests/Features/Api/Controllers/ImagesController_ContentRootResolutionTests.cs index e87ddfa18..4a55232b3 100644 --- a/tests/Features/Api/Controllers/ImagesController_ContentRootResolutionTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_ContentRootResolutionTests.cs @@ -16,12 +16,8 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; using System.Reflection; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/ImagesController_LocalIsbnOpenLibraryFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_LocalIsbnOpenLibraryFallbackTests.cs index 708c9be01..ce6fab9f7 100644 --- a/tests/Features/Api/Controllers/ImagesController_LocalIsbnOpenLibraryFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_LocalIsbnOpenLibraryFallbackTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_LocalTitleAuthorOpenLibraryFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_LocalTitleAuthorOpenLibraryFallbackTests.cs index 83e97d217..7252a5fd1 100644 --- a/tests/Features/Api/Controllers/ImagesController_LocalTitleAuthorOpenLibraryFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_LocalTitleAuthorOpenLibraryFallbackTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_MetadataDescriptionDoesNotBlockFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_MetadataDescriptionDoesNotBlockFallbackTests.cs index 85c3062b6..fce847310 100644 --- a/tests/Features/Api/Controllers/ImagesController_MetadataDescriptionDoesNotBlockFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_MetadataDescriptionDoesNotBlockFallbackTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_MetadataDownloadFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_MetadataDownloadFallbackTests.cs index cfa684dbc..791e3ec8b 100644 --- a/tests/Features/Api/Controllers/ImagesController_MetadataDownloadFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_MetadataDownloadFallbackTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_MetadataDownloadTests.cs b/tests/Features/Api/Controllers/ImagesController_MetadataDownloadTests.cs index 0f0c14dfc..697b2e6e9 100644 --- a/tests/Features/Api/Controllers/ImagesController_MetadataDownloadTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_MetadataDownloadTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_PlaceholderFallbackTests.cs b/tests/Features/Api/Controllers/ImagesController_PlaceholderFallbackTests.cs index 7c17cc60c..fef5bf903 100644 --- a/tests/Features/Api/Controllers/ImagesController_PlaceholderFallbackTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_PlaceholderFallbackTests.cs @@ -16,14 +16,10 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ImagesController_TempToLibraryForAudiobookTests.cs b/tests/Features/Api/Controllers/ImagesController_TempToLibraryForAudiobookTests.cs index f86182b6d..05122cb4d 100644 --- a/tests/Features/Api/Controllers/ImagesController_TempToLibraryForAudiobookTests.cs +++ b/tests/Features/Api/Controllers/ImagesController_TempToLibraryForAudiobookTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/IntelligentSearchIntegrationTests.cs b/tests/Features/Api/Controllers/IntelligentSearchIntegrationTests.cs index d2ec31375..26fe1e867 100644 --- a/tests/Features/Api/Controllers/IntelligentSearchIntegrationTests.cs +++ b/tests/Features/Api/Controllers/IntelligentSearchIntegrationTests.cs @@ -15,11 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Domain.Models; using Listenarr.Api.Controllers; -using Moq; -using Xunit; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; using Listenarr.Application.Search; diff --git a/tests/Features/Api/Controllers/LibraryController_AddToLibraryTests.cs b/tests/Features/Api/Controllers/LibraryController_AddToLibraryTests.cs index b21b3bd48..f57d88792 100644 --- a/tests/Features/Api/Controllers/LibraryController_AddToLibraryTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_AddToLibraryTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Listenarr.Tests.Builders; -using Listenarr.Application.Interfaces; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/LibraryController_BasePathTests.cs b/tests/Features/Api/Controllers/LibraryController_BasePathTests.cs index af7faf9a7..38a565845 100644 --- a/tests/Features/Api/Controllers/LibraryController_BasePathTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_BasePathTests.cs @@ -16,11 +16,9 @@ * along with this program. If not, see . */ using System.Reflection; -using Xunit; using Listenarr.Api.Controllers; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/LibraryController_BulkUpdateTests.cs b/tests/Features/Api/Controllers/LibraryController_BulkUpdateTests.cs index 393a1c740..d98863846 100644 --- a/tests/Features/Api/Controllers/LibraryController_BulkUpdateTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_BulkUpdateTests.cs @@ -17,13 +17,9 @@ */ using System.Text.Json; using Listenarr.Api.Controllers; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/LibraryController_DeleteFilesystemTests.cs b/tests/Features/Api/Controllers/LibraryController_DeleteFilesystemTests.cs index 71c66c033..2ec777622 100644 --- a/tests/Features/Api/Controllers/LibraryController_DeleteFilesystemTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_DeleteFilesystemTests.cs @@ -16,12 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/LibraryController_DeleteImageSafetyTests.cs b/tests/Features/Api/Controllers/LibraryController_DeleteImageSafetyTests.cs index cee5392f5..4b044d90f 100644 --- a/tests/Features/Api/Controllers/LibraryController_DeleteImageSafetyTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_DeleteImageSafetyTests.cs @@ -15,12 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; -using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/LibraryController_LibraryListSlimPayloadTests.cs b/tests/Features/Api/Controllers/LibraryController_LibraryListSlimPayloadTests.cs index 2f0b461f6..9d84f604e 100644 --- a/tests/Features/Api/Controllers/LibraryController_LibraryListSlimPayloadTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_LibraryListSlimPayloadTests.cs @@ -17,11 +17,7 @@ */ using System.Text.Json; using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/LibraryController_MoveTests.cs b/tests/Features/Api/Controllers/LibraryController_MoveTests.cs index e2d743a42..e7e8bdc37 100644 --- a/tests/Features/Api/Controllers/LibraryController_MoveTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_MoveTests.cs @@ -16,12 +16,7 @@ * along with this program. If not, see . */ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Common; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/LibraryController_QualityCutoffTests.cs b/tests/Features/Api/Controllers/LibraryController_QualityCutoffTests.cs index 6bc7d7f9d..edf83ba0f 100644 --- a/tests/Features/Api/Controllers/LibraryController_QualityCutoffTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_QualityCutoffTests.cs @@ -17,12 +17,8 @@ */ using System.Reflection; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/LibraryController_ScanPathConfigFailureTests.cs b/tests/Features/Api/Controllers/LibraryController_ScanPathConfigFailureTests.cs index ce237c9b5..5bba1eda2 100644 --- a/tests/Features/Api/Controllers/LibraryController_ScanPathConfigFailureTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_ScanPathConfigFailureTests.cs @@ -16,11 +16,7 @@ * along with this program. If not, see . */ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/LibraryController_ScanPathValidationTests.cs b/tests/Features/Api/Controllers/LibraryController_ScanPathValidationTests.cs index cb64a1ebd..04417d27a 100644 --- a/tests/Features/Api/Controllers/LibraryController_ScanPathValidationTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_ScanPathValidationTests.cs @@ -16,11 +16,7 @@ * along with this program. If not, see . */ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/LibraryController_UpdateAudiobookTests.cs b/tests/Features/Api/Controllers/LibraryController_UpdateAudiobookTests.cs index e38064772..2bb1bf4d7 100644 --- a/tests/Features/Api/Controllers/LibraryController_UpdateAudiobookTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_UpdateAudiobookTests.cs @@ -16,10 +16,7 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Tests.Common; namespace Listenarr.Tests.Features.Api.Controllers diff --git a/tests/Features/Api/Controllers/LibraryController_WantedFlagRegressionTests.cs b/tests/Features/Api/Controllers/LibraryController_WantedFlagRegressionTests.cs index d3e8ad0d2..b1d3f7574 100644 --- a/tests/Features/Api/Controllers/LibraryController_WantedFlagRegressionTests.cs +++ b/tests/Features/Api/Controllers/LibraryController_WantedFlagRegressionTests.cs @@ -18,8 +18,6 @@ using System.Text.Json; using Listenarr.Api.Controllers; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Features/Api/Controllers/ManualImportControllerTests.cs b/tests/Features/Api/Controllers/ManualImportControllerTests.cs index 0b479c691..b8d25b94e 100644 --- a/tests/Features/Api/Controllers/ManualImportControllerTests.cs +++ b/tests/Features/Api/Controllers/ManualImportControllerTests.cs @@ -15,16 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; using Listenarr.Api.Controllers; using Microsoft.Extensions.Logging.Abstractions; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; -using Listenarr.Application.Common; using Listenarr.Infrastructure.FileSystem; using Listenarr.Api.Dtos.ManualImport; diff --git a/tests/Features/Api/Controllers/MetadataController_AuthorCatalogTests.cs b/tests/Features/Api/Controllers/MetadataController_AuthorCatalogTests.cs index 77ba19c9e..9d953716b 100644 --- a/tests/Features/Api/Controllers/MetadataController_AuthorCatalogTests.cs +++ b/tests/Features/Api/Controllers/MetadataController_AuthorCatalogTests.cs @@ -17,14 +17,10 @@ */ using Listenarr.Api.Controllers; using Listenarr.Application.Audiobooks; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/MetadataController_AuthorLookupTests.cs b/tests/Features/Api/Controllers/MetadataController_AuthorLookupTests.cs index 9a36a1e84..e4bfee9cc 100644 --- a/tests/Features/Api/Controllers/MetadataController_AuthorLookupTests.cs +++ b/tests/Features/Api/Controllers/MetadataController_AuthorLookupTests.cs @@ -16,15 +16,10 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/MetadataController_SeriesTests.cs b/tests/Features/Api/Controllers/MetadataController_SeriesTests.cs index b6e9ede18..19638f2b5 100644 --- a/tests/Features/Api/Controllers/MetadataController_SeriesTests.cs +++ b/tests/Features/Api/Controllers/MetadataController_SeriesTests.cs @@ -16,15 +16,10 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/ProwlarrCompatControllerTests.cs b/tests/Features/Api/Controllers/ProwlarrCompatControllerTests.cs index 5ca0dae88..b50e2a4f9 100644 --- a/tests/Features/Api/Controllers/ProwlarrCompatControllerTests.cs +++ b/tests/Features/Api/Controllers/ProwlarrCompatControllerTests.cs @@ -15,19 +15,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Microsoft.EntityFrameworkCore; using System.Text.Json; using System.Reflection; using Listenarr.Infrastructure.Persistence.Repositories; using Listenarr.Infrastructure.Persistence; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; namespace Listenarr.Tests.Features.Api.Controllers { @@ -46,22 +39,21 @@ private static IApplicationVersionService CreateApplicationVersionService() return Mock.Of(service => service.Resolve() == "0.4.2"); } + private static IRealtimeClientRegistry CreateRealtimeClientRegistry() + { + return Mock.Of(registry => registry.GetSettingsClientIds() == Array.Empty()); + } + [Fact] - public async Task PostIndexers_BroadcastsSignalR_WhenNewIndexersCreated() + public async Task PostIndexers_BroadcastsRealtimeUpdate_WhenNewIndexersCreated() { var db = CreateInMemoryDb(); - var mockClientProxy = new Mock(); - var mockHubClients = new Mock(); - mockHubClients.Setup(c => c.All).Returns(mockClientProxy.Object); - - var mockHubContext = new Mock>(); - mockHubContext.SetupGet(h => h.Clients).Returns(mockHubClients.Object); - + var mockHubBroadcaster = new Mock(); var mockLogger = new Mock>(); var mockToastService = new Mock(); var mockStartupConfigService = new Mock(); mockStartupConfigService.Setup(s => s.GetConfig()).Returns(new StartupConfig { AuthenticationRequired = "false" }); - var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubContext.Object, mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); + var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubBroadcaster.Object, CreateRealtimeClientRegistry(), mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); var newIndexer = new { name = "Unit Test Indexer", implementation = "Newznab", baseUrl = "http://localhost", apiPath = "api", apiKey = "KEY" }; var arr = JsonSerializer.Serialize(new[] { newIndexer }); @@ -77,9 +69,8 @@ public async Task PostIndexers_BroadcastsSignalR_WhenNewIndexersCreated() var payload = JsonDocument.Parse(arr).RootElement; _ = await controller.PostIndexers(payload); - // Verify that SendCoreAsync (SignalR) was invoked for the indexer update - mockClientProxy.Verify( - p => p.SendCoreAsync("IndexersUpdated", It.IsAny(), default), + mockHubBroadcaster.Verify( + b => b.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", It.IsAny(), It.IsAny()), Times.Once); // Verify a 'Created indexer' log entry exists @@ -100,16 +91,12 @@ public async Task PostIndexers_BroadcastsSignalR_WhenNewIndexersCreated() public async Task PostIndexer_ReturnsCreatedIndex_WhenSingleIndexerPosted() { var db = CreateInMemoryDb(); - var mockClientProxy = new Mock(); - var mockHubClients = new Mock(); - mockHubClients.Setup(c => c.All).Returns(mockClientProxy.Object); - var mockHubContext = new Mock>(); - mockHubContext.SetupGet(h => h.Clients).Returns(mockHubClients.Object); + var mockHubBroadcaster = new Mock(); var mockLogger = new Mock>(); var mockToastService = new Mock(); var mockStartupConfigService = new Mock(); mockStartupConfigService.Setup(s => s.GetConfig()).Returns(new StartupConfig { AuthenticationRequired = "false" }); - var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubContext.Object, mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); + var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubBroadcaster.Object, CreateRealtimeClientRegistry(), mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); var newIndexer = new { name = "Unit Test Indexer", implementation = "Newznab", baseUrl = "http://localhost", apiPath = "api", apiKey = "KEY" }; // Clear static toast maps to avoid test interdependence @@ -218,8 +205,8 @@ public async Task PostIndexer_ReturnsCreatedIndex_WhenSingleIndexerPosted() Assert.True(dbIndexed.Count(i => NormalizeIndexerUrl(i.Url) == NormalizeIndexerUrl("http://example.local/api")) == 1); // Verify a broadcast and notification occurred - mockClientProxy.Verify( - p => p.SendCoreAsync("IndexersUpdated", It.IsAny(), default), + mockHubBroadcaster.Verify( + b => b.BroadcastAsync(RealtimeHubTarget.Settings, "IndexersUpdated", It.IsAny(), It.IsAny()), Times.AtLeastOnce); mockToastService.Verify( s => s.PublishNotificationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), @@ -230,16 +217,12 @@ public async Task PostIndexer_ReturnsCreatedIndex_WhenSingleIndexerPosted() public async Task PutIndexer_SuppressesUpdateToast_IfIndexerRecentlyCreated() { var db = CreateInMemoryDb(); - var mockClientProxy = new Mock(); - var mockHubClients = new Mock(); - mockHubClients.Setup(c => c.All).Returns(mockClientProxy.Object); - var mockHubContext = new Mock>(); - mockHubContext.SetupGet(h => h.Clients).Returns(mockHubClients.Object); + var mockHubBroadcaster = new Mock(); var mockLogger = new Mock>(); var mockToastService = new Mock(); var mockStartupConfigService = new Mock(); mockStartupConfigService.Setup(s => s.GetConfig()).Returns(new StartupConfig { AuthenticationRequired = "false" }); - var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubContext.Object, mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); + var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubBroadcaster.Object, CreateRealtimeClientRegistry(), mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); // Create indexer via POST (this publishes one notification) var newIndexer = new { name = "Recent Import", implementation = "Newznab", baseUrl = "http://localhost:9090", apiPath = "api", apiKey = "KEY" }; @@ -273,16 +256,12 @@ public async Task PutIndexer_SuppressesUpdateToast_IfIndexerRecentlyCreated() public async Task PutIndexer_DeduplicatesUpdateToasts_OnRapidConsecutivePuts() { var db = CreateInMemoryDb(); - var mockClientProxy = new Mock(); - var mockHubClients = new Mock(); - mockHubClients.Setup(c => c.All).Returns(mockClientProxy.Object); - var mockHubContext = new Mock>(); - mockHubContext.SetupGet(h => h.Clients).Returns(mockHubClients.Object); + var mockHubBroadcaster = new Mock(); var mockLogger = new Mock>(); var mockToastService = new Mock(); var mockStartupConfigService = new Mock(); mockStartupConfigService.Setup(s => s.GetConfig()).Returns(new StartupConfig { AuthenticationRequired = "false" }); - var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubContext.Object, mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); + var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubBroadcaster.Object, CreateRealtimeClientRegistry(), mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); // Seed an existing indexer (older CreatedAt so created-based suppression doesn't interfere) var idx = new Indexer { Name = "Rapid Update", Url = "http://rapid", ApiKey = "K", Categories = "", CreatedAt = DateTime.UtcNow.AddMinutes(-10), UpdatedAt = DateTime.UtcNow.AddMinutes(-10), IsEnabled = true }; @@ -330,16 +309,12 @@ public async Task GetIndexers_IncludesFieldsAndTags() db.Indexers.Add(new Indexer { Name = "Seeded", Url = "http://seed", ApiKey = "K", Categories = "1,2" }); db.SaveChanges(); - var mockClientProxy = new Mock(); - var mockHubClients = new Mock(); - mockHubClients.Setup(c => c.All).Returns(mockClientProxy.Object); - var mockHubContext = new Mock>(); - mockHubContext.SetupGet(h => h.Clients).Returns(mockHubClients.Object); + var mockHubBroadcaster = new Mock(); var mockLogger = new Mock>(); var mockToastService = new Mock(); var mockStartupConfigService = new Mock(); mockStartupConfigService.Setup(s => s.GetConfig()).Returns(new StartupConfig { AuthenticationRequired = "false" }); - var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubContext.Object, mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); + var controller = new ProwlarrCompatController(mockLogger.Object, new EfIndexerRepository(db), mockHubBroadcaster.Object, CreateRealtimeClientRegistry(), mockToastService.Object, mockStartupConfigService.Object, CreateApplicationVersionService()); var result = await controller.GetIndexers(); var ok = Assert.IsType(result); diff --git a/tests/Features/Api/Controllers/RootFoldersControllerTests.cs b/tests/Features/Api/Controllers/RootFoldersControllerTests.cs index 8c2dd3e64..915ad668b 100644 --- a/tests/Features/Api/Controllers/RootFoldersControllerTests.cs +++ b/tests/Features/Api/Controllers/RootFoldersControllerTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Microsoft.EntityFrameworkCore; -using Xunit; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Audiobooks; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Persistence.Repositories; namespace Listenarr.Tests.Features.Api.Controllers diff --git a/tests/Features/Api/Controllers/SearchControllerAdvancedNormalizationTests.cs b/tests/Features/Api/Controllers/SearchControllerAdvancedNormalizationTests.cs index fb34b814e..671a272dc 100644 --- a/tests/Features/Api/Controllers/SearchControllerAdvancedNormalizationTests.cs +++ b/tests/Features/Api/Controllers/SearchControllerAdvancedNormalizationTests.cs @@ -17,14 +17,9 @@ */ using System.Text.Json; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; using Listenarr.Application.Search; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Controllers/SearchControllerTests.cs b/tests/Features/Api/Controllers/SearchControllerTests.cs index 5b8a21b3f..f0cf449ee 100644 --- a/tests/Features/Api/Controllers/SearchControllerTests.cs +++ b/tests/Features/Api/Controllers/SearchControllerTests.cs @@ -16,15 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; using System.Text.Json; -using Listenarr.Domain.Models.Configurations; using Listenarr.Application.Metadata; -using Listenarr.Application.Interfaces; using Listenarr.Application.Search; namespace Listenarr.Tests.Features.Api.Controllers diff --git a/tests/Features/Api/Controllers/SearchControllerUnifiedTests.cs b/tests/Features/Api/Controllers/SearchControllerUnifiedTests.cs index dfad811af..b8e42023c 100644 --- a/tests/Features/Api/Controllers/SearchControllerUnifiedTests.cs +++ b/tests/Features/Api/Controllers/SearchControllerUnifiedTests.cs @@ -17,14 +17,10 @@ */ using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; using Listenarr.Application.Search; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Controllers { diff --git a/tests/Features/Api/Extensions/HostedServicesRegistrationTests.cs b/tests/Features/Api/Extensions/HostedServicesRegistrationTests.cs index 39f035440..ae49494a9 100644 --- a/tests/Features/Api/Extensions/HostedServicesRegistrationTests.cs +++ b/tests/Features/Api/Extensions/HostedServicesRegistrationTests.cs @@ -16,18 +16,10 @@ * along with this program. If not, see . */ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Xunit; -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Audiobooks; using Listenarr.Infrastructure.Extensions; using Listenarr.Infrastructure.FileSystem; -using Listenarr.Application.Common; using Listenarr.Infrastructure.Ffmpeg; -using Listenarr.Application.Metadata; -using Listenarr.Application.Search; namespace Listenarr.Tests.Features.Api.Extensions { diff --git a/tests/Features/Api/Extensions/SwaggerSecurityRequirementDocumentFilterTests.cs b/tests/Features/Api/Extensions/SwaggerSecurityRequirementDocumentFilterTests.cs index 62af23819..e3f1e5718 100644 --- a/tests/Features/Api/Extensions/SwaggerSecurityRequirementDocumentFilterTests.cs +++ b/tests/Features/Api/Extensions/SwaggerSecurityRequirementDocumentFilterTests.cs @@ -18,7 +18,6 @@ using System.Text.Json; using Listenarr.Api.Filters; using Microsoft.OpenApi; -using Xunit; namespace Listenarr.Tests.Features.Api.Extensions { diff --git a/tests/Features/Api/ForwardedHeadersTrustModelTests.cs b/tests/Features/Api/ForwardedHeadersTrustModelTests.cs index af7857b74..3afe4d770 100644 --- a/tests/Features/Api/ForwardedHeadersTrustModelTests.cs +++ b/tests/Features/Api/ForwardedHeadersTrustModelTests.cs @@ -16,12 +16,12 @@ * along with this program. If not, see . */ using System.Net; +using Listenarr.Infrastructure.Web; using Listenarr.Tests.Mocks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Xunit; namespace Listenarr.Tests.Features.Api { @@ -51,6 +51,27 @@ public void ForwardedHeadersOptions_TrustsCommonPrivateProxyNetworks() Assert.Contains(options.KnownIPNetworks, network => Matches(network, "fe80::", 10)); } + [Fact] + public void RequestContextAccessor_IgnoresRawForwardedHostHeaders() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("listenarr.internal:4545"); + httpContext.Request.Headers["X-Forwarded-Proto"] = "https"; + httpContext.Request.Headers["X-Forwarded-Host"] = "attacker.example"; + + var accessor = new AspNetRequestContextAccessor(new HttpContextAccessor + { + HttpContext = httpContext + }); + + var snapshot = accessor.Current; + + Assert.NotNull(snapshot); + Assert.Equal("http", snapshot.Scheme); + Assert.Equal("listenarr.internal:4545", snapshot.Host); + } + private static bool Matches(System.Net.IPNetwork network, string prefix, int prefixLength) { return network.BaseAddress.Equals(IPAddress.Parse(prefix)) && network.PrefixLength == prefixLength; diff --git a/tests/Features/Api/LibraryController_GetAllResilienceTests.cs b/tests/Features/Api/LibraryController_GetAllResilienceTests.cs index 185d05e48..7c6c1ff42 100644 --- a/tests/Features/Api/LibraryController_GetAllResilienceTests.cs +++ b/tests/Features/Api/LibraryController_GetAllResilienceTests.cs @@ -17,12 +17,9 @@ */ using System.Net; using Asp.Versioning.ApiExplorer; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Mocks; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api { diff --git a/tests/Features/Api/LibraryController_IdentifierDeduplicationTests.cs b/tests/Features/Api/LibraryController_IdentifierDeduplicationTests.cs index 45239bc80..478e6e158 100644 --- a/tests/Features/Api/LibraryController_IdentifierDeduplicationTests.cs +++ b/tests/Features/Api/LibraryController_IdentifierDeduplicationTests.cs @@ -17,11 +17,8 @@ */ using System.Text; using System.Text.Json; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Mocks; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api { diff --git a/tests/Features/Api/LibraryController_MetadataRescanTests.cs b/tests/Features/Api/LibraryController_MetadataRescanTests.cs index 969efc3a0..a391c90fa 100644 --- a/tests/Features/Api/LibraryController_MetadataRescanTests.cs +++ b/tests/Features/Api/LibraryController_MetadataRescanTests.cs @@ -17,16 +17,11 @@ */ using System.Net; using System.Text.Json; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Mocks; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api { diff --git a/tests/Features/Api/Middleware/AuthenticationMiddlewareTests.cs b/tests/Features/Api/Middleware/AuthenticationMiddlewareTests.cs index 03dfa6051..08af58ef2 100644 --- a/tests/Features/Api/Middleware/AuthenticationMiddlewareTests.cs +++ b/tests/Features/Api/Middleware/AuthenticationMiddlewareTests.cs @@ -18,12 +18,8 @@ using System.Net; using System.Text; using Asp.Versioning.ApiExplorer; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Mocks; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Middleware { diff --git a/tests/Features/Api/Models/AudiobookDtoFactoryTests.cs b/tests/Features/Api/Models/AudiobookDtoFactoryTests.cs index 94f7ead81..bcfb622df 100644 --- a/tests/Features/Api/Models/AudiobookDtoFactoryTests.cs +++ b/tests/Features/Api/Models/AudiobookDtoFactoryTests.cs @@ -16,8 +16,6 @@ * along with this program. If not, see . */ using Microsoft.EntityFrameworkCore; -using Xunit; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Mapping; using Listenarr.Tests.Builders; diff --git a/tests/Features/Api/ProwlarrEndpointsTests.cs b/tests/Features/Api/ProwlarrEndpointsTests.cs index f49a9f48b..1d03a3b5a 100644 --- a/tests/Features/Api/ProwlarrEndpointsTests.cs +++ b/tests/Features/Api/ProwlarrEndpointsTests.cs @@ -17,7 +17,6 @@ */ using System.Net; using System.Text.Json; -using Xunit; using Listenarr.Tests.Mocks; namespace Listenarr.Tests.Features.Api diff --git a/tests/Features/Api/Services/AudibleServiceAuthorFallbackTests.cs b/tests/Features/Api/Services/AudibleServiceAuthorFallbackTests.cs index 4e41d9918..979e0dc45 100644 --- a/tests/Features/Api/Services/AudibleServiceAuthorFallbackTests.cs +++ b/tests/Features/Api/Services/AudibleServiceAuthorFallbackTests.cs @@ -17,8 +17,6 @@ */ using Listenarr.Application.Metadata; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AudibleServiceTests.cs b/tests/Features/Api/Services/AudibleServiceTests.cs index 719f41b66..d7ff5dc4b 100644 --- a/tests/Features/Api/Services/AudibleServiceTests.cs +++ b/tests/Features/Api/Services/AudibleServiceTests.cs @@ -17,7 +17,6 @@ */ using System.Reflection; using Listenarr.Application.Metadata; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AudibleServiceTitleSearchTests.cs b/tests/Features/Api/Services/AudibleServiceTitleSearchTests.cs index 8998d4f95..9697952a3 100644 --- a/tests/Features/Api/Services/AudibleServiceTitleSearchTests.cs +++ b/tests/Features/Api/Services/AudibleServiceTitleSearchTests.cs @@ -17,8 +17,6 @@ */ using Listenarr.Application.Metadata; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AudioFileServiceTests.cs b/tests/Features/Api/Services/AudioFileServiceTests.cs index eda4d42b1..690efaa9b 100644 --- a/tests/Features/Api/Services/AudioFileServiceTests.cs +++ b/tests/Features/Api/Services/AudioFileServiceTests.cs @@ -16,11 +16,6 @@ * along with this program. If not, see . */ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Moq; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Common; using Listenarr.Tests.Builders; using Listenarr.Infrastructure.Persistence; diff --git a/tests/Features/Api/Services/AudioFileService_UpdateAudiobookFieldsTests.cs b/tests/Features/Api/Services/AudioFileService_UpdateAudiobookFieldsTests.cs index 4521facb0..10244a9a7 100644 --- a/tests/Features/Api/Services/AudioFileService_UpdateAudiobookFieldsTests.cs +++ b/tests/Features/Api/Services/AudioFileService_UpdateAudiobookFieldsTests.cs @@ -15,11 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Common; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/AudiobookMetadataServiceTests.cs b/tests/Features/Api/Services/AudiobookMetadataServiceTests.cs index 86c239e4d..0bec690df 100644 --- a/tests/Features/Api/Services/AudiobookMetadataServiceTests.cs +++ b/tests/Features/Api/Services/AudiobookMetadataServiceTests.cs @@ -15,12 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models.Configurations; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AudiobookStatusEvaluatorTests.cs b/tests/Features/Api/Services/AudiobookStatusEvaluatorTests.cs index 50eb6db68..9790af78f 100644 --- a/tests/Features/Api/Services/AudiobookStatusEvaluatorTests.cs +++ b/tests/Features/Api/Services/AudiobookStatusEvaluatorTests.cs @@ -17,8 +17,6 @@ */ using Listenarr.Application.Metadata; using Listenarr.Application.Audiobooks; -using Listenarr.Domain.Models; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AuthorCatalogServiceTests.cs b/tests/Features/Api/Services/AuthorCatalogServiceTests.cs index 783e1a718..c7b428b1d 100644 --- a/tests/Features/Api/Services/AuthorCatalogServiceTests.cs +++ b/tests/Features/Api/Services/AuthorCatalogServiceTests.cs @@ -16,13 +16,8 @@ * along with this program. If not, see . */ using Listenarr.Application.Audiobooks; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/AuthorMonitoringServiceTests.cs b/tests/Features/Api/Services/AuthorMonitoringServiceTests.cs index ef5ca8e90..362b8a745 100644 --- a/tests/Features/Api/Services/AuthorMonitoringServiceTests.cs +++ b/tests/Features/Api/Services/AuthorMonitoringServiceTests.cs @@ -16,15 +16,10 @@ * along with this program. If not, see . */ using Listenarr.Application.Audiobooks; -using Listenarr.Application.Interfaces; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/ConfigurationServiceTests.cs b/tests/Features/Api/Services/ConfigurationServiceTests.cs index f75190fca..347129d72 100644 --- a/tests/Features/Api/Services/ConfigurationServiceTests.cs +++ b/tests/Features/Api/Services/ConfigurationServiceTests.cs @@ -15,19 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; -using Moq; -using Xunit; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Listenarr.Tests.Common; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; using Listenarr.Infrastructure.Persistence; -using Listenarr.Domain.Models.Configurations; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DiscordBotServiceTests.cs b/tests/Features/Api/Services/DiscordBotServiceTests.cs index 6878ab314..23f8a0349 100644 --- a/tests/Features/Api/Services/DiscordBotServiceTests.cs +++ b/tests/Features/Api/Services/DiscordBotServiceTests.cs @@ -16,15 +16,7 @@ * along with this program. If not, see . */ using System.Diagnostics; -using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; -using Xunit; using System.Runtime.InteropServices; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; -using Moq; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; namespace Listenarr.Tests.Features.Api.Services { @@ -107,11 +99,11 @@ public async Task StartAndStopBot_WithFakeRunner_StartsAndStopsProcess() pathService.SetupGet(service => service.DiscordBotRootPath).Returns(botDir); var cfg = new StartupConfig { ApiKey = "test-api-key", EnableSsl = false, Port = 5000 }; var startupService = new FakeStartupConfigService(cfg); - var httpAccessor = new HttpContextAccessor(); + var requestContextAccessor = Mock.Of(); var logger = new Mock>().Object; var fakeRunner = new FakeProcessRunner(); - var svc = new DiscordBotService(logger, startupService, pathService.Object, httpAccessor, fakeRunner); + var svc = new DiscordBotService(logger, startupService, pathService.Object, requestContextAccessor, fakeRunner); try { diff --git a/tests/Features/Api/Services/DownloadClientCategoryFilterTests.cs b/tests/Features/Api/Services/DownloadClientCategoryFilterTests.cs index f069d4cc2..1946aa659 100644 --- a/tests/Features/Api/Services/DownloadClientCategoryFilterTests.cs +++ b/tests/Features/Api/Services/DownloadClientCategoryFilterTests.cs @@ -18,13 +18,10 @@ using System.Net; using System.Text; using Listenarr.Application.Downloads; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Adapters; using Listenarr.Infrastructure.Torrents; using Listenarr.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DownloadHashRetrievalServiceTests.cs b/tests/Features/Api/Services/DownloadHashRetrievalServiceTests.cs index 89951ba84..0a7955db5 100644 --- a/tests/Features/Api/Services/DownloadHashRetrievalServiceTests.cs +++ b/tests/Features/Api/Services/DownloadHashRetrievalServiceTests.cs @@ -17,14 +17,9 @@ */ using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DownloadMonitorServiceTests.cs b/tests/Features/Api/Services/DownloadMonitorServiceTests.cs index 9fc83a29e..50f33daf0 100644 --- a/tests/Features/Api/Services/DownloadMonitorServiceTests.cs +++ b/tests/Features/Api/Services/DownloadMonitorServiceTests.cs @@ -16,17 +16,10 @@ * along with this program. If not, see . */ using System.Reflection; -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Common; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DownloadNaming_AudiobookMetadataTests.cs b/tests/Features/Api/Services/DownloadNaming_AudiobookMetadataTests.cs index 97a441360..a2e8daa2d 100644 --- a/tests/Features/Api/Services/DownloadNaming_AudiobookMetadataTests.cs +++ b/tests/Features/Api/Services/DownloadNaming_AudiobookMetadataTests.cs @@ -15,12 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Listenarr.Tests.Builders; -using Listenarr.Application.Interfaces; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DownloadNaming_PatternCollapseTests.cs b/tests/Features/Api/Services/DownloadNaming_PatternCollapseTests.cs index bf00cc255..5b1447c5f 100644 --- a/tests/Features/Api/Services/DownloadNaming_PatternCollapseTests.cs +++ b/tests/Features/Api/Services/DownloadNaming_PatternCollapseTests.cs @@ -15,12 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; -using Listenarr.Application.Interfaces; -using Microsoft.Extensions.Logging; -using Listenarr.Application.Common; - namespace Listenarr.Tests.Features.Api.Services { public class DownloadNaming_PatternCollapseTests diff --git a/tests/Features/Api/Services/DownloadQueueServiceReconciliationTests.cs b/tests/Features/Api/Services/DownloadQueueServiceReconciliationTests.cs index 562d9a56d..b8d0392a8 100644 --- a/tests/Features/Api/Services/DownloadQueueServiceReconciliationTests.cs +++ b/tests/Features/Api/Services/DownloadQueueServiceReconciliationTests.cs @@ -17,14 +17,8 @@ */ using System.Text.Json; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; using Listenarr.Application.Downloads; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/DownloadStateMachineTests.cs b/tests/Features/Api/Services/DownloadStateMachineTests.cs index fec798c2c..d2770da13 100644 --- a/tests/Features/Api/Services/DownloadStateMachineTests.cs +++ b/tests/Features/Api/Services/DownloadStateMachineTests.cs @@ -17,13 +17,9 @@ */ using Listenarr.Application.Downloads; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/DownloadValidationPipelineTests.cs b/tests/Features/Api/Services/DownloadValidationPipelineTests.cs index 6ded29061..b4b6eeb32 100644 --- a/tests/Features/Api/Services/DownloadValidationPipelineTests.cs +++ b/tests/Features/Api/Services/DownloadValidationPipelineTests.cs @@ -17,13 +17,9 @@ */ using Listenarr.Application.Downloads; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/FfmpegServiceTests.cs b/tests/Features/Api/Services/FfmpegServiceTests.cs index 71cb88301..52fabef3b 100644 --- a/tests/Features/Api/Services/FfmpegServiceTests.cs +++ b/tests/Features/Api/Services/FfmpegServiceTests.cs @@ -1,10 +1,5 @@ -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Ffmpeg; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/FileMoverFallbackTests.cs b/tests/Features/Api/Services/FileMoverFallbackTests.cs index b13246bb5..37e520f29 100644 --- a/tests/Features/Api/Services/FileMoverFallbackTests.cs +++ b/tests/Features/Api/Services/FileMoverFallbackTests.cs @@ -17,12 +17,9 @@ */ using System.Diagnostics; using System.Runtime.InteropServices; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; using Listenarr.Infrastructure.FileSystem; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/FileMoverHardlinkTests.cs b/tests/Features/Api/Services/FileMoverHardlinkTests.cs index 4bec19fb8..ca9d9e946 100644 --- a/tests/Features/Api/Services/FileMoverHardlinkTests.cs +++ b/tests/Features/Api/Services/FileMoverHardlinkTests.cs @@ -17,7 +17,6 @@ */ using Listenarr.Infrastructure.FileSystem; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/FileNamingService_PathLengthTests.cs b/tests/Features/Api/Services/FileNamingService_PathLengthTests.cs index 6e8e6d635..e2c4f41c1 100644 --- a/tests/Features/Api/Services/FileNamingService_PathLengthTests.cs +++ b/tests/Features/Api/Services/FileNamingService_PathLengthTests.cs @@ -16,11 +16,6 @@ * along with this program. If not, see . */ using System.Runtime.InteropServices; -using Xunit; -using Moq; -using Microsoft.Extensions.Logging; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/FileNamingService_PatternSelectionTests.cs b/tests/Features/Api/Services/FileNamingService_PatternSelectionTests.cs index 78b77667a..c57b2543e 100644 --- a/tests/Features/Api/Services/FileNamingService_PatternSelectionTests.cs +++ b/tests/Features/Api/Services/FileNamingService_PatternSelectionTests.cs @@ -15,15 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; -using Microsoft.Extensions.Logging; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Common; -using Listenarr.Domain.Models.Configurations; - namespace Listenarr.Tests.Features.Api.Services { /// diff --git a/tests/Features/Api/Services/ImportServiceHardlinkTests.cs b/tests/Features/Api/Services/ImportServiceHardlinkTests.cs index e33158614..2fee849f2 100644 --- a/tests/Features/Api/Services/ImportServiceHardlinkTests.cs +++ b/tests/Features/Api/Services/ImportServiceHardlinkTests.cs @@ -15,13 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Tests.Common; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/ImportServiceTests.cs b/tests/Features/Api/Services/ImportServiceTests.cs index 0b7120cc2..01d0a711b 100644 --- a/tests/Features/Api/Services/ImportServiceTests.cs +++ b/tests/Features/Api/Services/ImportServiceTests.cs @@ -17,14 +17,8 @@ */ using System.Runtime.InteropServices; using System.Text; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/Import_PatternIntegrationTests.cs b/tests/Features/Api/Services/Import_PatternIntegrationTests.cs index 57b1dfc3d..ab7202364 100644 --- a/tests/Features/Api/Services/Import_PatternIntegrationTests.cs +++ b/tests/Features/Api/Services/Import_PatternIntegrationTests.cs @@ -15,14 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; -using Listenarr.Domain.Models; -using Microsoft.Extensions.Logging; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; -using Listenarr.Application.Common; - namespace Listenarr.Tests.Features.Api.Services { /// diff --git a/tests/Features/Api/Services/LegacyOutputPathMigratorTests.cs b/tests/Features/Api/Services/LegacyOutputPathMigratorTests.cs index 7ec4448ae..b7b904a59 100644 --- a/tests/Features/Api/Services/LegacyOutputPathMigratorTests.cs +++ b/tests/Features/Api/Services/LegacyOutputPathMigratorTests.cs @@ -16,13 +16,6 @@ * along with this program. If not, see . */ using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/LogRedactionTests.cs b/tests/Features/Api/Services/LogRedactionTests.cs index 2d4f6e534..456ebf723 100644 --- a/tests/Features/Api/Services/LogRedactionTests.cs +++ b/tests/Features/Api/Services/LogRedactionTests.cs @@ -15,9 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Security; -using Xunit; - namespace Listenarr.Tests.Features.Api.Services { public class LogRedactionTests diff --git a/tests/Features/Api/Services/LoginRateLimiterTests.cs b/tests/Features/Api/Services/LoginRateLimiterTests.cs index b0a0221c3..06ab375a9 100644 --- a/tests/Features/Api/Services/LoginRateLimiterTests.cs +++ b/tests/Features/Api/Services/LoginRateLimiterTests.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Infrastructure.Security; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/MetadataServiceTests.cs b/tests/Features/Api/Services/MetadataServiceTests.cs index 49bb02f2e..a2ada6962 100644 --- a/tests/Features/Api/Services/MetadataServiceTests.cs +++ b/tests/Features/Api/Services/MetadataServiceTests.cs @@ -1,9 +1,5 @@ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/MoveBackgroundServiceTests.cs b/tests/Features/Api/Services/MoveBackgroundServiceTests.cs index 15c343a5a..c49780760 100644 --- a/tests/Features/Api/Services/MoveBackgroundServiceTests.cs +++ b/tests/Features/Api/Services/MoveBackgroundServiceTests.cs @@ -15,12 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.FileSystem; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/MoveBackgroundService_BroadcastTests.cs b/tests/Features/Api/Services/MoveBackgroundService_BroadcastTests.cs index ea5046db1..b7da07941 100644 --- a/tests/Features/Api/Services/MoveBackgroundService_BroadcastTests.cs +++ b/tests/Features/Api/Services/MoveBackgroundService_BroadcastTests.cs @@ -15,14 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.SignalR; using Listenarr.Tests.Common; using System.Text.Json; -using Listenarr.Application.Notification; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.FileSystem; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/MoveBackgroundService_FailureTests.cs b/tests/Features/Api/Services/MoveBackgroundService_FailureTests.cs index 68d39caaf..fe56a6f17 100644 --- a/tests/Features/Api/Services/MoveBackgroundService_FailureTests.cs +++ b/tests/Features/Api/Services/MoveBackgroundService_FailureTests.cs @@ -15,11 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.FileSystem; namespace Listenarr.Tests.Features.Api.Services diff --git a/tests/Features/Api/Services/MoveBackgroundService_FilePathPreservationTests.cs b/tests/Features/Api/Services/MoveBackgroundService_FilePathPreservationTests.cs index 3da416878..622ed13b2 100644 --- a/tests/Features/Api/Services/MoveBackgroundService_FilePathPreservationTests.cs +++ b/tests/Features/Api/Services/MoveBackgroundService_FilePathPreservationTests.cs @@ -15,11 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Moq; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Common; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Infrastructure.FileSystem; diff --git a/tests/Features/Api/Services/MoveQueueServiceTests.cs b/tests/Features/Api/Services/MoveQueueServiceTests.cs index 065b82606..d96c4afd2 100644 --- a/tests/Features/Api/Services/MoveQueueServiceTests.cs +++ b/tests/Features/Api/Services/MoveQueueServiceTests.cs @@ -15,10 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.EntityFrameworkCore; -using Xunit; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Infrastructure.Persistence.Repositories; using Listenarr.Infrastructure.Persistence; diff --git a/tests/Features/Api/Services/MyAnonamouseTorrentAnnounceExtractionTests.cs b/tests/Features/Api/Services/MyAnonamouseTorrentAnnounceExtractionTests.cs index efc36a33a..67ab9bf6c 100644 --- a/tests/Features/Api/Services/MyAnonamouseTorrentAnnounceExtractionTests.cs +++ b/tests/Features/Api/Services/MyAnonamouseTorrentAnnounceExtractionTests.cs @@ -15,9 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; using System.Text; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/NotificationPayloadBuilderAdapterTests.cs b/tests/Features/Api/Services/NotificationPayloadBuilderAdapterTests.cs index 39ea69d8c..014662c50 100644 --- a/tests/Features/Api/Services/NotificationPayloadBuilderAdapterTests.cs +++ b/tests/Features/Api/Services/NotificationPayloadBuilderAdapterTests.cs @@ -16,12 +16,6 @@ * along with this program. If not, see . */ using System.Net; -using Microsoft.AspNetCore.Http; -using Moq; -using Moq.Protected; -using Xunit; -using Microsoft.Extensions.DependencyInjection; -using Listenarr.Application.Notification; namespace Listenarr.Tests.Features.Api.Services { @@ -89,7 +83,7 @@ public async Task CreateDiscordPayloadWithAttachmentAsync_DownloadsImageAndRetur }; // Act - var (payload, attachment) = await adapter.CreateDiscordPayloadWithAttachmentAsync("book-added", data, "https://listenarr.example.com", httpClient, Mock.Of()); + var (payload, attachment) = await adapter.CreateDiscordPayloadWithAttachmentAsync("book-added", data, "https://listenarr.example.com", httpClient, Mock.Of()); // Assert Assert.NotNull(payload); diff --git a/tests/Features/Api/Services/NotificationServiceTests.cs b/tests/Features/Api/Services/NotificationServiceTests.cs index ea115387c..e56e2605d 100644 --- a/tests/Features/Api/Services/NotificationServiceTests.cs +++ b/tests/Features/Api/Services/NotificationServiceTests.cs @@ -16,16 +16,7 @@ * along with this program. If not, see . */ using System.Text.Json.Nodes; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Xunit; using System.Net; -using Moq; -using Moq.Protected; -using Microsoft.Extensions.Logging; -using Listenarr.Domain.Models; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Notification; namespace Listenarr.Api.Tests { @@ -191,13 +182,11 @@ public void CreateDiscordPayload_ConvertsRelativeImageUrlToAbsolute_WhenBaseUrlP public partial class NotificationServiceTests { [Fact] - public void GetBaseUrlFromHttpContext_ReturnsExpectedBase() + public void GetBaseUrlFromRequestContext_ReturnsExpectedBase() { - var ctx = new DefaultHttpContext(); - ctx.Request.Scheme = "https"; - ctx.Request.Host = new HostString("listenarr.example.com"); + var ctx = new RequestContextSnapshot(null, "https", "listenarr.example.com", null, false); - var baseUrl = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(ctx); + var baseUrl = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(ctx); Assert.Equal("https://listenarr.example.com", baseUrl); } @@ -212,11 +201,9 @@ public void CreateDiscordPayload_UsesDerivedBaseForThumbnail_WhenProvided() asin = "B123DERIVE" }; - var ctx = new DefaultHttpContext(); - ctx.Request.Scheme = "https"; - ctx.Request.Host = new HostString("listenarr.example.com"); + var ctx = new RequestContextSnapshot(null, "https", "listenarr.example.com", null, false); - var derived = NotificationPayloadBuilder.GetBaseUrlFromHttpContext(ctx); + var derived = NotificationPayloadBuilder.GetBaseUrlFromRequestContext(ctx); Assert.NotNull(derived); var node = NotificationPayloadBuilder.CreateDiscordPayload(trigger, data, derived); @@ -315,7 +302,7 @@ public async Task SendNotificationAsync_PostsCorrectJsonToDiscordWebhook() .ReturnsAsync(startupConfig); // Mock HttpContextAccessor (optional for this test) - var mockHttpContextAccessor = new Mock(); + var mockHttpContextAccessor = new Mock(); // Create service var services = new ServiceCollection(); @@ -492,7 +479,7 @@ public async Task SendNotificationAsync_AttachesImageAndReferencesAttachmentInPa Mock.Of>(), mockConfigService.Object, payloadBuilder, - Mock.Of() + Mock.Of() ); // Act diff --git a/tests/Features/Api/Services/ParseLanguageTests.cs b/tests/Features/Api/Services/ParseLanguageTests.cs index 8605db2e6..7ba904c89 100644 --- a/tests/Features/Api/Services/ParseLanguageTests.cs +++ b/tests/Features/Api/Services/ParseLanguageTests.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Application.Search; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/PathMetadataParserTests.cs b/tests/Features/Api/Services/PathMetadataParserTests.cs index ce5e5846b..5a681fa07 100644 --- a/tests/Features/Api/Services/PathMetadataParserTests.cs +++ b/tests/Features/Api/Services/PathMetadataParserTests.cs @@ -17,7 +17,6 @@ */ using System.Text.Json; using Listenarr.Application.Metadata; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/QualityProfileScoringTests.cs b/tests/Features/Api/Services/QualityProfileScoringTests.cs index 5781d77d4..1bdfab67a 100644 --- a/tests/Features/Api/Services/QualityProfileScoringTests.cs +++ b/tests/Features/Api/Services/QualityProfileScoringTests.cs @@ -15,10 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Moq; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.EntityFrameworkCore; diff --git a/tests/Features/Api/Services/QualityScoringTests.cs b/tests/Features/Api/Services/QualityScoringTests.cs index 46f608761..475ca1c7c 100644 --- a/tests/Features/Api/Services/QualityScoringTests.cs +++ b/tests/Features/Api/Services/QualityScoringTests.cs @@ -16,11 +16,9 @@ * along with this program. If not, see . */ using System.Reflection; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Audiobooks; diff --git a/tests/Features/Api/Services/RenameServiceTests.cs b/tests/Features/Api/Services/RenameServiceTests.cs index 48a2be5f2..2fa66ceff 100644 --- a/tests/Features/Api/Services/RenameServiceTests.cs +++ b/tests/Features/Api/Services/RenameServiceTests.cs @@ -16,18 +16,11 @@ * along with this program. If not, see . */ using Listenarr.Application.Audiobooks; -using Listenarr.Application.Common; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/RootFolderServiceTests.cs b/tests/Features/Api/Services/RootFolderServiceTests.cs index 858caffac..c22439fa9 100644 --- a/tests/Features/Api/Services/RootFolderServiceTests.cs +++ b/tests/Features/Api/Services/RootFolderServiceTests.cs @@ -16,16 +16,10 @@ * along with this program. If not, see . */ using Microsoft.EntityFrameworkCore; -using Xunit; using Xunit.Abstractions; -using Microsoft.Extensions.Logging; -using Moq; using Listenarr.Infrastructure.Persistence.Repositories; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/Search/Providers/IndexersAuthTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersAuthTests.cs index cc04e0ab4..245fa1031 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersAuthTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersAuthTests.cs @@ -16,11 +16,9 @@ * along with this program. If not, see . */ using System.Net; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/IndexersControllerProwlarrImportTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersControllerProwlarrImportTests.cs index b469ff86d..5dc835f56 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersControllerProwlarrImportTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersControllerProwlarrImportTests.cs @@ -19,12 +19,8 @@ using System.Text; using Listenarr.Api.Controllers; using Listenarr.Api.Dtos; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/IndexersControllerTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersControllerTests.cs index 2a021a87e..3f3ed54c5 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersControllerTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersControllerTests.cs @@ -17,12 +17,9 @@ */ using System.Net; using Listenarr.Api.Controllers; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Common; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/IndexersNewznabAuthTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersNewznabAuthTests.cs index f1f1ee333..0d83dc693 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersNewznabAuthTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersNewznabAuthTests.cs @@ -16,9 +16,7 @@ * along with this program. If not, see . */ using System.Net; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/IndexersNewznabParsingTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersNewznabParsingTests.cs index d13aa549e..f105040e9 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersNewznabParsingTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersNewznabParsingTests.cs @@ -16,17 +16,12 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; -using Xunit; using Microsoft.Extensions.Logging.Abstractions; using System.Net; using Microsoft.EntityFrameworkCore; -using Moq; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Persistence; using Listenarr.Application.Metadata; using Listenarr.Application.Search; -using Listenarr.Application.Notification; using Listenarr.Application.Search.Filters; using Listenarr.Application.Search.Strategies; using Listenarr.Infrastructure.Search.Providers; diff --git a/tests/Features/Api/Services/Search/Providers/IndexersPersistedAuthTests.cs b/tests/Features/Api/Services/Search/Providers/IndexersPersistedAuthTests.cs index b2c7a7fac..f41908682 100644 --- a/tests/Features/Api/Services/Search/Providers/IndexersPersistedAuthTests.cs +++ b/tests/Features/Api/Services/Search/Providers/IndexersPersistedAuthTests.cs @@ -16,9 +16,7 @@ * along with this program. If not, see . */ using System.Net; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/MyAnonamouseCookieTests.cs b/tests/Features/Api/Services/Search/Providers/MyAnonamouseCookieTests.cs index 10e3bba35..59a87e5cb 100644 --- a/tests/Features/Api/Services/Search/Providers/MyAnonamouseCookieTests.cs +++ b/tests/Features/Api/Services/Search/Providers/MyAnonamouseCookieTests.cs @@ -15,16 +15,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Microsoft.Extensions.DependencyInjection; using System.Reflection; using System.Text; using Listenarr.Tests.Common; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Mocks.Api; using Listenarr.Application.Downloads; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentAnnounceRewriteTests.cs b/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentAnnounceRewriteTests.cs index 7af8d3d78..df8ab76d5 100644 --- a/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentAnnounceRewriteTests.cs +++ b/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentAnnounceRewriteTests.cs @@ -16,8 +16,6 @@ * along with this program. If not, see . */ using System.Text; -using Listenarr.Application.Common; -using Xunit; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentRewriteTests.cs b/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentRewriteTests.cs index aa2c3f80a..c635e712c 100644 --- a/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentRewriteTests.cs +++ b/tests/Features/Api/Services/Search/Providers/MyAnonamouseTorrentRewriteTests.cs @@ -16,8 +16,6 @@ * along with this program. If not, see . */ using System.Text; -using Xunit; -using Listenarr.Application.Common; namespace Listenarr.Tests.Features.Api.Services.Search.Providers { diff --git a/tests/Features/Api/Services/SearchServiceFixesTests.cs b/tests/Features/Api/Services/SearchServiceFixesTests.cs index e709ecbe1..83e913422 100644 --- a/tests/Features/Api/Services/SearchServiceFixesTests.cs +++ b/tests/Features/Api/Services/SearchServiceFixesTests.cs @@ -15,15 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Listenarr.Application.Interfaces; using Listenarr.Application.Search; using Listenarr.Application.Metadata; -using Listenarr.Application.Notification; using Listenarr.Application.Search.Strategies; using Listenarr.Application.Search.Filters; diff --git a/tests/Features/Api/Services/SearchServiceScoringTests.cs b/tests/Features/Api/Services/SearchServiceScoringTests.cs index dbae53ef8..fa71ae5d7 100644 --- a/tests/Features/Api/Services/SearchServiceScoringTests.cs +++ b/tests/Features/Api/Services/SearchServiceScoringTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; -using Listenarr.Application.Interfaces; using Listenarr.Application.Search; using Listenarr.Application.Metadata; -using Listenarr.Application.Notification; using Listenarr.Application.Search.Strategies; using Listenarr.Application.Search.Filters; diff --git a/tests/Features/Api/Services/SearchServiceSortingTests.cs b/tests/Features/Api/Services/SearchServiceSortingTests.cs index 781fddc68..cce4bd7b6 100644 --- a/tests/Features/Api/Services/SearchServiceSortingTests.cs +++ b/tests/Features/Api/Services/SearchServiceSortingTests.cs @@ -17,8 +17,6 @@ */ using System.Reflection; using Listenarr.Application.Search; -using Listenarr.Domain.Models; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/SecurityRedactionTests.cs b/tests/Features/Api/Services/SecurityRedactionTests.cs index 477faa8fa..3471e030f 100644 --- a/tests/Features/Api/Services/SecurityRedactionTests.cs +++ b/tests/Features/Api/Services/SecurityRedactionTests.cs @@ -16,17 +16,7 @@ * along with this program. If not, see . */ using System.Net; -using Microsoft.Extensions.Logging; -using Moq; -using Moq.Protected; -using Xunit; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Listenarr.Domain.Models; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; -using Listenarr.Application.Notification; namespace Listenarr.Tests.Features.Api.Services { @@ -93,7 +83,7 @@ public async Task NotificationService_LogsAreRedacted_WhenResponseContainsSensit services.AddSingleton(); var provider = services.BuildServiceProvider(); var payloadBuilder = provider.GetRequiredService(); - var service = new NotificationService(httpClient, mockLogger.Object, mockConfigService.Object, payloadBuilder, Mock.Of()); + var service = new NotificationService(httpClient, mockLogger.Object, mockConfigService.Object, payloadBuilder, Mock.Of()); // Act await service.SendNotificationAsync(trigger, data, webhookUrl, enabledTriggers); diff --git a/tests/Features/Api/Services/SeriesCatalogServiceTests.cs b/tests/Features/Api/Services/SeriesCatalogServiceTests.cs index da778a242..2b6b1f9fd 100644 --- a/tests/Features/Api/Services/SeriesCatalogServiceTests.cs +++ b/tests/Features/Api/Services/SeriesCatalogServiceTests.cs @@ -18,10 +18,6 @@ using Listenarr.Application.Audiobooks; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Application.Metadata; -using Listenarr.Domain.Models; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/StartupConfigServiceTests.cs b/tests/Features/Api/Services/StartupConfigServiceTests.cs index 16e22219b..034c4d8ff 100644 --- a/tests/Features/Api/Services/StartupConfigServiceTests.cs +++ b/tests/Features/Api/Services/StartupConfigServiceTests.cs @@ -15,11 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.Logging; -using Xunit; -using Listenarr.Domain.Models; -using Listenarr.Application.Common; - namespace Listenarr.Tests.Features.Api.Services { public class StartupConfigServiceTests @@ -33,8 +28,8 @@ public async Task SaveAsync_PreservesAuthenticationRequired() using var loggerFactory = new LoggerFactory(); var logger = loggerFactory.CreateLogger(); - var envMock = new Moq.Mock(); - envMock.Setup(e => e.ContentRootPath).Returns(AppContext.BaseDirectory); + var pathServiceMock = new Moq.Mock(); + pathServiceMock.Setup(e => e.ContentRootPath).Returns(AppContext.BaseDirectory); try { @@ -43,7 +38,7 @@ public async Task SaveAsync_PreservesAuthenticationRequired() Directory.Delete(cfgDir, recursive: true); } - var svc = new StartupConfigService(logger, envMock.Object); + var svc = new StartupConfigService(logger, pathServiceMock.Object); // default config should exist and have false auth var original = svc.GetConfig(); diff --git a/tests/Features/Api/Services/SystemProcessRunnerTests.cs b/tests/Features/Api/Services/SystemProcessRunnerTests.cs index 5ae8a1251..7de4505b5 100644 --- a/tests/Features/Api/Services/SystemProcessRunnerTests.cs +++ b/tests/Features/Api/Services/SystemProcessRunnerTests.cs @@ -19,7 +19,6 @@ using System.Runtime.InteropServices; using Listenarr.Infrastructure.Platform; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/Services/UnmatchedScanBackgroundServiceTests.cs b/tests/Features/Api/Services/UnmatchedScanBackgroundServiceTests.cs index bd6111da7..eba5c8d14 100644 --- a/tests/Features/Api/Services/UnmatchedScanBackgroundServiceTests.cs +++ b/tests/Features/Api/Services/UnmatchedScanBackgroundServiceTests.cs @@ -15,9 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Audiobooks; using Listenarr.Application.Metadata; -using Xunit; namespace Listenarr.Tests.Features.Api.Services { diff --git a/tests/Features/Api/SessionCookieAuthTests.cs b/tests/Features/Api/SessionCookieAuthTests.cs index 46cf4a533..d2165faed 100644 --- a/tests/Features/Api/SessionCookieAuthTests.cs +++ b/tests/Features/Api/SessionCookieAuthTests.cs @@ -19,18 +19,12 @@ using System.Net.Http.Headers; using System.Text.Json; using Listenarr.Api.Controllers; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Security; -using Listenarr.Domain.Models; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Api { diff --git a/tests/Features/Api/Utils/FinalizePathHelperTests.cs b/tests/Features/Api/Utils/FinalizePathHelperTests.cs index 78adcecfe..6a1270c55 100644 --- a/tests/Features/Api/Utils/FinalizePathHelperTests.cs +++ b/tests/Features/Api/Utils/FinalizePathHelperTests.cs @@ -15,10 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Domain.Common; -using Xunit; -using Listenarr.Domain.Models.Configurations; -using Listenarr.Application.Common; using Listenarr.Tests.Builders; namespace Listenarr.Tests.Features.Api.Utils diff --git a/tests/Features/Application/Audiobooks/SeriesMonitoringServiceTests.cs b/tests/Features/Application/Audiobooks/SeriesMonitoringServiceTests.cs index bd7550ac5..33eab81fb 100644 --- a/tests/Features/Application/Audiobooks/SeriesMonitoringServiceTests.cs +++ b/tests/Features/Application/Audiobooks/SeriesMonitoringServiceTests.cs @@ -15,13 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Application.Audiobooks { diff --git a/tests/Features/Application/Downloads/DownloadClientGatewayTests.cs b/tests/Features/Application/Downloads/DownloadClientGatewayTests.cs index 42cfbe971..60b5ced06 100644 --- a/tests/Features/Application/Downloads/DownloadClientGatewayTests.cs +++ b/tests/Features/Application/Downloads/DownloadClientGatewayTests.cs @@ -1,12 +1,7 @@ using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Downloads/DownloadClientUriBuilderTests.cs b/tests/Features/Application/Downloads/DownloadClientUriBuilderTests.cs index 22fa196ec..21112c798 100644 --- a/tests/Features/Application/Downloads/DownloadClientUriBuilderTests.cs +++ b/tests/Features/Application/Downloads/DownloadClientUriBuilderTests.cs @@ -16,8 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Application.Downloads; -using Listenarr.Domain.Models; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Downloads/DownloadImportServiceTests.cs b/tests/Features/Application/Downloads/DownloadImportServiceTests.cs index 2ce966677..b2f958fc3 100644 --- a/tests/Features/Application/Downloads/DownloadImportServiceTests.cs +++ b/tests/Features/Application/Downloads/DownloadImportServiceTests.cs @@ -15,15 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Tests.Common; using Listenarr.Tests.Builders; -using Listenarr.Application.Interfaces; using System.Runtime.InteropServices; using System.IO.Compression; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; using Listenarr.Tests.Mocks; diff --git a/tests/Features/Application/Downloads/DownloadIntegrationTests.cs b/tests/Features/Application/Downloads/DownloadIntegrationTests.cs index 7e8dac60c..e0bac3807 100644 --- a/tests/Features/Application/Downloads/DownloadIntegrationTests.cs +++ b/tests/Features/Application/Downloads/DownloadIntegrationTests.cs @@ -15,13 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Domain.Models; -using Moq; -using Xunit; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models.Configurations; using Listenarr.Domain.Models.Enumerations; using Listenarr.Tests.Builders; using Listenarr.Application.Downloads; diff --git a/tests/Features/Application/Downloads/DownloadMonitorServiceTests.cs b/tests/Features/Application/Downloads/DownloadMonitorServiceTests.cs index f1e52f9c6..577145072 100644 --- a/tests/Features/Application/Downloads/DownloadMonitorServiceTests.cs +++ b/tests/Features/Application/Downloads/DownloadMonitorServiceTests.cs @@ -1,11 +1,6 @@ using System.Reflection; -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Downloads/DownloadProcessingJobProcessorIntegrationTests.cs b/tests/Features/Application/Downloads/DownloadProcessingJobProcessorIntegrationTests.cs index d5e676659..4d70bcd1e 100644 --- a/tests/Features/Application/Downloads/DownloadProcessingJobProcessorIntegrationTests.cs +++ b/tests/Features/Application/Downloads/DownloadProcessingJobProcessorIntegrationTests.cs @@ -1,11 +1,8 @@ using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Downloads/DownloadProcessingJobProcessorTests.cs b/tests/Features/Application/Downloads/DownloadProcessingJobProcessorTests.cs index e22285b24..67716ec60 100644 --- a/tests/Features/Application/Downloads/DownloadProcessingJobProcessorTests.cs +++ b/tests/Features/Application/Downloads/DownloadProcessingJobProcessorTests.cs @@ -1,11 +1,5 @@ -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Listenarr.Application.Interfaces; -using Listenarr.Application.Downloads; -using Moq; using Listenarr.Tests.Mocks; namespace Listenarr.Tests.Features.Application.Downloads diff --git a/tests/Features/Application/Downloads/DownloadProcessingJobServiceTests.cs b/tests/Features/Application/Downloads/DownloadProcessingJobServiceTests.cs index a0c982b8e..87ef7b42d 100644 --- a/tests/Features/Application/Downloads/DownloadProcessingJobServiceTests.cs +++ b/tests/Features/Application/Downloads/DownloadProcessingJobServiceTests.cs @@ -15,14 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Downloads/DownloadServiceTests.cs b/tests/Features/Application/Downloads/DownloadServiceTests.cs index b8e0ac54d..c4670b518 100644 --- a/tests/Features/Application/Downloads/DownloadServiceTests.cs +++ b/tests/Features/Application/Downloads/DownloadServiceTests.cs @@ -1,13 +1,8 @@ using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Application.Downloads { diff --git a/tests/Features/Application/Notifications/NotificationTests.cs b/tests/Features/Application/Notifications/NotificationTests.cs index 0a6c989a8..cd72124b0 100644 --- a/tests/Features/Application/Notifications/NotificationTests.cs +++ b/tests/Features/Application/Notifications/NotificationTests.cs @@ -1,11 +1,5 @@ -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Application.Notifications { diff --git a/tests/Features/Domain/Common/AudiobookSeriesMembershipHelperTests.cs b/tests/Features/Domain/Common/AudiobookSeriesMembershipHelperTests.cs index 11efd1b38..984371e8d 100644 --- a/tests/Features/Domain/Common/AudiobookSeriesMembershipHelperTests.cs +++ b/tests/Features/Domain/Common/AudiobookSeriesMembershipHelperTests.cs @@ -16,10 +16,6 @@ * along with this program. If not, see . */ -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; -using Xunit; - namespace Listenarr.Tests.Features.Domain.Common { [Trait("Name", "AudiobookSeriesMembershipHelperTests")] diff --git a/tests/Features/Domain/Models/DownloadClientItemTests.cs b/tests/Features/Domain/Models/DownloadClientItemTests.cs index 38df4bd9d..712dcc928 100644 --- a/tests/Features/Domain/Models/DownloadClientItemTests.cs +++ b/tests/Features/Domain/Models/DownloadClientItemTests.cs @@ -16,9 +16,6 @@ * along with this program. If not, see . */ -using Xunit; -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Features.Domain.Models { /// diff --git a/tests/Features/Domain/Models/DownloadProcessingJobTests.cs b/tests/Features/Domain/Models/DownloadProcessingJobTests.cs index a06c71690..e7d7318c7 100644 --- a/tests/Features/Domain/Models/DownloadProcessingJobTests.cs +++ b/tests/Features/Domain/Models/DownloadProcessingJobTests.cs @@ -1,7 +1,5 @@ -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Xunit; namespace Listenarr.Tests.Features.Domain.Models { diff --git a/tests/Features/Domain/Utils/FileUtilsTests.cs b/tests/Features/Domain/Utils/FileUtilsTests.cs index 4642aa3a2..ffcd75938 100644 --- a/tests/Features/Domain/Utils/FileUtilsTests.cs +++ b/tests/Features/Domain/Utils/FileUtilsTests.cs @@ -15,10 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; using System.Security.AccessControl; using System.Security.Principal; -using Listenarr.Domain.Common; namespace Listenarr.Tests.Features.Domain.Utils { diff --git a/tests/Features/Domain/Utils/TitleMatchingServiceTests.cs b/tests/Features/Domain/Utils/TitleMatchingServiceTests.cs index b5eea766f..7066c3aa1 100644 --- a/tests/Features/Domain/Utils/TitleMatchingServiceTests.cs +++ b/tests/Features/Domain/Utils/TitleMatchingServiceTests.cs @@ -15,9 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Xunit; -using Listenarr.Domain.Common; - namespace Listenarr.Tests.Features.Domain.Utils { public class TitleMatchingServiceTests diff --git a/tests/Features/Infrastructure/Adapters/DownloadClientAdapterTests.cs b/tests/Features/Infrastructure/Adapters/DownloadClientAdapterTests.cs index 3f97da73a..9a3f6030d 100644 --- a/tests/Features/Infrastructure/Adapters/DownloadClientAdapterTests.cs +++ b/tests/Features/Infrastructure/Adapters/DownloadClientAdapterTests.cs @@ -15,12 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks.Api; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Adapters { diff --git a/tests/Features/Infrastructure/Adapters/NzbgetAdapterTests.cs b/tests/Features/Infrastructure/Adapters/NzbgetAdapterTests.cs index 2a7ba3957..db1bb6ca5 100644 --- a/tests/Features/Infrastructure/Adapters/NzbgetAdapterTests.cs +++ b/tests/Features/Infrastructure/Adapters/NzbgetAdapterTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ using System.Net; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Adapters; using Listenarr.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Adapters { diff --git a/tests/Features/Infrastructure/Adapters/QbittorrentAdapterTests.cs b/tests/Features/Infrastructure/Adapters/QbittorrentAdapterTests.cs index 1380ae4ff..9404dff89 100644 --- a/tests/Features/Infrastructure/Adapters/QbittorrentAdapterTests.cs +++ b/tests/Features/Infrastructure/Adapters/QbittorrentAdapterTests.cs @@ -16,15 +16,9 @@ * along with this program. If not, see . */ using System.Text.Json; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Listenarr.Tests.Common; -using Moq; -using Xunit; using Listenarr.Infrastructure.Adapters; using Listenarr.Tests.Builders; -using Microsoft.Extensions.DependencyInjection; -using Listenarr.Application.Interfaces; using Listenarr.Tests.Mocks.Api; using Listenarr.Application.Downloads; using Listenarr.Infrastructure.Torrents; diff --git a/tests/Features/Infrastructure/Adapters/QbittorrentHelpersTests.cs b/tests/Features/Infrastructure/Adapters/QbittorrentHelpersTests.cs index 5542f0e7b..28f7cc83e 100644 --- a/tests/Features/Infrastructure/Adapters/QbittorrentHelpersTests.cs +++ b/tests/Features/Infrastructure/Adapters/QbittorrentHelpersTests.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ using Listenarr.Infrastructure.Adapters; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Adapters { diff --git a/tests/Features/Infrastructure/Adapters/SabnzbdAdapterTests.cs b/tests/Features/Infrastructure/Adapters/SabnzbdAdapterTests.cs index 90f2e61ed..1b1e59bf8 100644 --- a/tests/Features/Infrastructure/Adapters/SabnzbdAdapterTests.cs +++ b/tests/Features/Infrastructure/Adapters/SabnzbdAdapterTests.cs @@ -15,14 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Downloads; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks.Api; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Adapters { diff --git a/tests/Features/Infrastructure/Adapters/TransmissionAdapterTests.cs b/tests/Features/Infrastructure/Adapters/TransmissionAdapterTests.cs index 4f31074cf..f3c774b5b 100644 --- a/tests/Features/Infrastructure/Adapters/TransmissionAdapterTests.cs +++ b/tests/Features/Infrastructure/Adapters/TransmissionAdapterTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ using System.Runtime.InteropServices; -using Listenarr.Domain.Models; -using Listenarr.Domain.Common; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; using Listenarr.Tests.Mocks.Api; -using Microsoft.Extensions.DependencyInjection; -using Moq; -using Xunit; using Listenarr.Infrastructure.Torrents; namespace Listenarr.Tests.Features.Infrastructure.Adapters diff --git a/tests/Features/Infrastructure/Adapters/UsenetAdapterFilteringTests.cs b/tests/Features/Infrastructure/Adapters/UsenetAdapterFilteringTests.cs index c936c5953..bb2c8a584 100644 --- a/tests/Features/Infrastructure/Adapters/UsenetAdapterFilteringTests.cs +++ b/tests/Features/Infrastructure/Adapters/UsenetAdapterFilteringTests.cs @@ -17,13 +17,9 @@ */ using System.Net; using System.Text; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Adapters; using Listenarr.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Adapters { diff --git a/tests/Features/Infrastructure/Cache/ImageCacheServiceTests.cs b/tests/Features/Infrastructure/Cache/ImageCacheServiceTests.cs index 408d4709d..935095377 100644 --- a/tests/Features/Infrastructure/Cache/ImageCacheServiceTests.cs +++ b/tests/Features/Infrastructure/Cache/ImageCacheServiceTests.cs @@ -15,12 +15,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Cache; using Listenarr.Tests.Common; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Cache { diff --git a/tests/Features/Infrastructure/Converters/JsonValueConvertersTests.cs b/tests/Features/Infrastructure/Converters/JsonValueConvertersTests.cs index e4b215660..3e4b4ce2f 100644 --- a/tests/Features/Infrastructure/Converters/JsonValueConvertersTests.cs +++ b/tests/Features/Infrastructure/Converters/JsonValueConvertersTests.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ // csharp -using Xunit; using Listenarr.Infrastructure.Persistence.Converters; namespace Listenarr.Tests.Features.Infrastructure.Converters diff --git a/tests/Features/Infrastructure/Extensions/DependencyInjectionTests.cs b/tests/Features/Infrastructure/Extensions/DependencyInjectionTests.cs index 33b56a63e..b66664742 100644 --- a/tests/Features/Infrastructure/Extensions/DependencyInjectionTests.cs +++ b/tests/Features/Infrastructure/Extensions/DependencyInjectionTests.cs @@ -16,9 +16,7 @@ * along with this program. If not, see . */ // csharp -using Microsoft.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; -using Xunit; using Listenarr.Infrastructure.Extensions; using Listenarr.Application.Interfaces.Repositories; diff --git a/tests/Features/Infrastructure/Extensions/InfrastructureServiceRegistrationExtensionsTests.cs b/tests/Features/Infrastructure/Extensions/InfrastructureServiceRegistrationExtensionsTests.cs index c0728e482..4ea000e3b 100644 --- a/tests/Features/Infrastructure/Extensions/InfrastructureServiceRegistrationExtensionsTests.cs +++ b/tests/Features/Infrastructure/Extensions/InfrastructureServiceRegistrationExtensionsTests.cs @@ -15,10 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Microsoft.Extensions.DependencyInjection; -using Xunit; using Listenarr.Infrastructure.Extensions; -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Cache; using Listenarr.Infrastructure.Platform; using Microsoft.Extensions.Http; diff --git a/tests/Features/Infrastructure/Migrations/MigrationMetadataTests.cs b/tests/Features/Infrastructure/Migrations/MigrationMetadataTests.cs index 29e691e4e..7ff9242d2 100644 --- a/tests/Features/Infrastructure/Migrations/MigrationMetadataTests.cs +++ b/tests/Features/Infrastructure/Migrations/MigrationMetadataTests.cs @@ -18,7 +18,6 @@ using System.Reflection; using Listenarr.Infrastructure.Persistence.Migrations; using Microsoft.EntityFrameworkCore.Migrations; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Migrations { diff --git a/tests/Features/Infrastructure/Persistence/DatabaseIsolationTests.cs b/tests/Features/Infrastructure/Persistence/DatabaseIsolationTests.cs index 75c3e78d8..5220dec93 100644 --- a/tests/Features/Infrastructure/Persistence/DatabaseIsolationTests.cs +++ b/tests/Features/Infrastructure/Persistence/DatabaseIsolationTests.cs @@ -18,8 +18,6 @@ using Listenarr.Tests.Mocks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Persistence { diff --git a/tests/Features/Infrastructure/Platform/ApplicationVersionServiceTests.cs b/tests/Features/Infrastructure/Platform/ApplicationVersionServiceTests.cs index 06dbeb1ec..a12e9a333 100644 --- a/tests/Features/Infrastructure/Platform/ApplicationVersionServiceTests.cs +++ b/tests/Features/Infrastructure/Platform/ApplicationVersionServiceTests.cs @@ -19,8 +19,6 @@ using Listenarr.Infrastructure.Platform; using Listenarr.Tests.Common; using Microsoft.Extensions.Hosting; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Platform { diff --git a/tests/Features/Infrastructure/Platform/DiskSpaceProbeTests.cs b/tests/Features/Infrastructure/Platform/DiskSpaceProbeTests.cs index d4dd35d64..f98c40bfa 100644 --- a/tests/Features/Infrastructure/Platform/DiskSpaceProbeTests.cs +++ b/tests/Features/Infrastructure/Platform/DiskSpaceProbeTests.cs @@ -19,7 +19,6 @@ using Listenarr.Infrastructure.Platform; using Listenarr.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Platform { diff --git a/tests/Features/Infrastructure/Platform/SystemServiceStorageTests.cs b/tests/Features/Infrastructure/Platform/SystemServiceStorageTests.cs index 9ce9e8538..4f47f49c6 100644 --- a/tests/Features/Infrastructure/Platform/SystemServiceStorageTests.cs +++ b/tests/Features/Infrastructure/Platform/SystemServiceStorageTests.cs @@ -16,14 +16,10 @@ * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; using Listenarr.Infrastructure.Platform; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Platform { diff --git a/tests/Features/Infrastructure/Platform/SystemServiceVersionTests.cs b/tests/Features/Infrastructure/Platform/SystemServiceVersionTests.cs index 0925cd293..0eae91468 100644 --- a/tests/Features/Infrastructure/Platform/SystemServiceVersionTests.cs +++ b/tests/Features/Infrastructure/Platform/SystemServiceVersionTests.cs @@ -16,14 +16,9 @@ * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; -using Listenarr.Domain.Models.Configurations; using Listenarr.Infrastructure.Platform; using Listenarr.Tests.Common; using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Platform { diff --git a/tests/Features/Infrastructure/Repositories/AudiobookRepositoryTests.cs b/tests/Features/Infrastructure/Repositories/AudiobookRepositoryTests.cs index f8059258f..091021bcf 100644 --- a/tests/Features/Infrastructure/Repositories/AudiobookRepositoryTests.cs +++ b/tests/Features/Infrastructure/Repositories/AudiobookRepositoryTests.cs @@ -16,7 +16,6 @@ * along with this program. If not, see . */ using Microsoft.EntityFrameworkCore; -using Xunit; using Listenarr.Infrastructure.Persistence; using Listenarr.Tests.Builders; diff --git a/tests/Features/Infrastructure/Repositories/DownloadHistoryRepositoryTests.cs b/tests/Features/Infrastructure/Repositories/DownloadHistoryRepositoryTests.cs index f6a326188..6a21cd3d5 100644 --- a/tests/Features/Infrastructure/Repositories/DownloadHistoryRepositoryTests.cs +++ b/tests/Features/Infrastructure/Repositories/DownloadHistoryRepositoryTests.cs @@ -16,11 +16,9 @@ * along with this program. If not, see . */ -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Persistence.Repositories; using Microsoft.EntityFrameworkCore; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Repositories { diff --git a/tests/Features/Infrastructure/Repositories/DownloadProcessingJobRepositoryTests.cs b/tests/Features/Infrastructure/Repositories/DownloadProcessingJobRepositoryTests.cs index f51938fe3..0139e3f78 100644 --- a/tests/Features/Infrastructure/Repositories/DownloadProcessingJobRepositoryTests.cs +++ b/tests/Features/Infrastructure/Repositories/DownloadProcessingJobRepositoryTests.cs @@ -3,7 +3,6 @@ using Listenarr.Tests.Common; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Repositories { diff --git a/tests/Features/Infrastructure/Services/ApplicationPathServiceTests.cs b/tests/Features/Infrastructure/Services/ApplicationPathServiceTests.cs index f8c1884c7..ab544a30f 100644 --- a/tests/Features/Infrastructure/Services/ApplicationPathServiceTests.cs +++ b/tests/Features/Infrastructure/Services/ApplicationPathServiceTests.cs @@ -17,7 +17,6 @@ */ using Listenarr.Infrastructure.Services; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Services { diff --git a/tests/Features/Infrastructure/Services/DownloadHistoryServiceTests.cs b/tests/Features/Infrastructure/Services/DownloadHistoryServiceTests.cs index 1e7ad8d00..6308bdab8 100644 --- a/tests/Features/Infrastructure/Services/DownloadHistoryServiceTests.cs +++ b/tests/Features/Infrastructure/Services/DownloadHistoryServiceTests.cs @@ -16,13 +16,9 @@ * along with this program. If not, see . */ -using Listenarr.Domain.Models; using Listenarr.Infrastructure.Persistence; using Listenarr.Infrastructure.Services; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Services { diff --git a/tests/Features/Infrastructure/Services/RemotePathMappingServiceTests.cs b/tests/Features/Infrastructure/Services/RemotePathMappingServiceTests.cs index 2d241b632..e1bd51260 100644 --- a/tests/Features/Infrastructure/Services/RemotePathMappingServiceTests.cs +++ b/tests/Features/Infrastructure/Services/RemotePathMappingServiceTests.cs @@ -1,11 +1,6 @@ using System.Runtime.InteropServices; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Listenarr.Tests.Features.Infrastructure.Services { diff --git a/tests/GlobalUsings.cs b/tests/GlobalUsings.cs new file mode 100644 index 000000000..9338de628 --- /dev/null +++ b/tests/GlobalUsings.cs @@ -0,0 +1,18 @@ +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using Moq; +global using Moq.Protected; +global using Xunit; +global using Listenarr.Application.Common; +global using Listenarr.Application.Interfaces; +global using Listenarr.Application.Notification; +global using Listenarr.Application.Security; +global using Listenarr.Domain.Common; +global using Listenarr.Domain.Models; +global using Listenarr.Domain.Models.Configurations; +global using Listenarr.Infrastructure.SignalR; +global using Listenarr.Infrastructure.HostedServices.Audiobooks; +global using Listenarr.Infrastructure.HostedServices.Common; +global using Listenarr.Infrastructure.HostedServices.Downloads; +global using Listenarr.Infrastructure.HostedServices.Metadata; +global using Listenarr.Infrastructure.HostedServices.Search; diff --git a/tests/Mocks/Api/NzbgetApiMock.cs b/tests/Mocks/Api/NzbgetApiMock.cs index f3c10a633..9b3d30471 100644 --- a/tests/Mocks/Api/NzbgetApiMock.cs +++ b/tests/Mocks/Api/NzbgetApiMock.cs @@ -1,4 +1,3 @@ -using Listenarr.Domain.Common; using Listenarr.Tests.Common; namespace Listenarr.Tests.Mocks.Api diff --git a/tests/Mocks/Api/SabnzbdApiMock.cs b/tests/Mocks/Api/SabnzbdApiMock.cs index 84e7f1897..c23941e27 100644 --- a/tests/Mocks/Api/SabnzbdApiMock.cs +++ b/tests/Mocks/Api/SabnzbdApiMock.cs @@ -1,5 +1,4 @@ using System.Web; -using Listenarr.Domain.Common; using Listenarr.Tests.Common; namespace Listenarr.Tests.Mocks.Api diff --git a/tests/Mocks/Api/TransmissionApiMock.cs b/tests/Mocks/Api/TransmissionApiMock.cs index e2e0fc420..2b57e251c 100644 --- a/tests/Mocks/Api/TransmissionApiMock.cs +++ b/tests/Mocks/Api/TransmissionApiMock.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Listenarr.Domain.Common; using Listenarr.Tests.Common; namespace Listenarr.Tests.Mocks.Api diff --git a/tests/Mocks/DownloadClientAdapterMock.cs b/tests/Mocks/DownloadClientAdapterMock.cs index 6817e644e..e7691c280 100644 --- a/tests/Mocks/DownloadClientAdapterMock.cs +++ b/tests/Mocks/DownloadClientAdapterMock.cs @@ -1,7 +1,4 @@ -using Listenarr.Application.Interfaces; using Listenarr.Application.Interfaces.Repositories; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; namespace Listenarr.Tests.Mocks diff --git a/tests/Mocks/DownloadClientGatewayMock.cs b/tests/Mocks/DownloadClientGatewayMock.cs index 4d6168e15..68e5e2e2c 100644 --- a/tests/Mocks/DownloadClientGatewayMock.cs +++ b/tests/Mocks/DownloadClientGatewayMock.cs @@ -15,9 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Mocks { /// diff --git a/tests/Mocks/FfmpegServiceMock.cs b/tests/Mocks/FfmpegServiceMock.cs index 52805b22f..4ed44b458 100644 --- a/tests/Mocks/FfmpegServiceMock.cs +++ b/tests/Mocks/FfmpegServiceMock.cs @@ -1,5 +1,3 @@ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; using Listenarr.Tests.Common; diff --git a/tests/Mocks/ListenarrWebApplicationFactory.cs b/tests/Mocks/ListenarrWebApplicationFactory.cs index 8fbae2c07..ab15e920a 100644 --- a/tests/Mocks/ListenarrWebApplicationFactory.cs +++ b/tests/Mocks/ListenarrWebApplicationFactory.cs @@ -15,15 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Moq; using System.Collections.Concurrent; using System.Diagnostics; diff --git a/tests/Mocks/MetadataServiceMock.cs b/tests/Mocks/MetadataServiceMock.cs index d3415010e..dbc005b9a 100644 --- a/tests/Mocks/MetadataServiceMock.cs +++ b/tests/Mocks/MetadataServiceMock.cs @@ -1,6 +1,4 @@ using System.Text.RegularExpressions; -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; using Listenarr.Tests.Builders; namespace Listenarr.Tests.Mocks diff --git a/tests/Mocks/NoopHubBroadcaster.cs b/tests/Mocks/NoopHubBroadcaster.cs index f9ea1b170..95df31ec0 100644 --- a/tests/Mocks/NoopHubBroadcaster.cs +++ b/tests/Mocks/NoopHubBroadcaster.cs @@ -15,9 +15,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Mocks { // Minimal no-op broadcaster used as a safe fallback when the real @@ -29,5 +26,17 @@ public Task BroadcastQueueUpdateAsync(QueueSnapshot queueSnapshot) // Intentionally do nothing in tests or lightweight hosts return Task.CompletedTask; } + + public Task BroadcastAsync(string eventName, object payload, CancellationToken cancellationToken = default) + { + // Intentionally do nothing in tests or lightweight hosts + return Task.CompletedTask; + } + + public Task BroadcastAsync(RealtimeHubTarget target, string eventName, object payload, CancellationToken cancellationToken = default) + { + // Intentionally do nothing in tests or lightweight hosts + return Task.CompletedTask; + } } } diff --git a/tests/Mocks/StartupConfigServiceMock.cs b/tests/Mocks/StartupConfigServiceMock.cs index a355674df..ba8c6380a 100644 --- a/tests/Mocks/StartupConfigServiceMock.cs +++ b/tests/Mocks/StartupConfigServiceMock.cs @@ -1,7 +1,3 @@ -using Listenarr.Application.Interfaces; -using Listenarr.Domain.Common; -using Listenarr.Domain.Models; - namespace Listenarr.Tests.Mocks { ///