From 8c54c3799914e0930ea4cedaf33b5cc0ccba52b0 Mon Sep 17 00:00:00 2001 From: Robbie Davis Date: Fri, 17 Apr 2026 21:37:49 -0400 Subject: [PATCH 01/20] Upgrade .NET/Node, refactor repos & update CI Bump platform and toolchain versions and reorganize repository contracts/implementations. Workflows updated to use .NET 10 and Node 24, legacy ci.yml removed, and run-tests.yml enhanced with concurrency, expanded setup (node), layering enforcement checks (api/application -> infrastructure), async void detection, frontend lint/type-check/test steps and an API publish step. Project files updated: Directory.Build.props TargetFramework set to net10.0 and Dockerfile base/sdk images moved to .NET 10. Repository interfaces were moved out of listenarr.api into listenarr.application and corresponding EF implementations into listenarr.infrastructure (many files added/renamed). Directory.Packages.props removed an explicit Microsoft.AspNetCore.Authorization entry. Frontend package.json/lock were updated to bump several dependencies. Misc test and project file adjustments to align with these changes. --- .github/workflows/canary.yml | 8 +- .github/workflows/ci.yml | 90 --- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly.yml | 8 +- .github/workflows/release.yml | 4 +- .github/workflows/run-tests.yml | 98 ++- Directory.Build.props | 2 +- Directory.Packages.props | 1 - Dockerfile | 4 +- fe/package-lock.json | 667 ++++++++++-------- fe/package.json | 8 +- fe/vite.config.ts | 25 + global.json | 2 +- .../Controllers/LibraryController.cs | 3 +- listenarr.api/Dockerfile.runtime | 10 +- .../AppServiceRegistrationExtensions.cs | 4 +- listenarr.api/Listenarr.Api.csproj | 1 - listenarr.api/Program.cs | 12 +- .../Services/CompletedDownloadProcessor.cs | 4 +- .../Services/DownloadQueueService.cs | 2 +- listenarr.api/Services/FileFinalizer.cs | 2 +- listenarr.api/Services/LibraryAddService.cs | 3 +- listenarr.api/Services/RootFolderService.cs | 2 +- listenarr.api/Services/UserService.cs | 6 +- .../tools/discord-bot/package-lock.json | 8 +- listenarr.api/tools/discord-bot/package.json | 2 +- .../IApiConfigurationRepository.cs | 15 + .../IApplicationSettingsRepository.cs | 12 + .../Repositories/IAudiobookFileRepository.cs | 19 + .../IDownloadClientConfigurationRepository.cs | 15 + .../IDownloadProcessingJobRepository.cs | 2 +- .../Repositories/IDownloadRepository.cs | 4 +- .../Repositories/IHistoryRepository.cs | 23 + .../Repositories/IIndexerRepository.cs | 17 + .../IMonitoredAuthorRepository.cs | 15 + .../IMonitoredSeriesRepository.cs | 15 + .../Repositories/IMoveJobRepository.cs | 16 + .../IProcessExecutionLogRepository.cs | 13 + .../IRemotePathMappingRepository.cs | 15 + .../Repositories/IRootFolderRepository.cs | 4 +- .../Repositories/IUserRepository.cs | 16 + .../Repositories/IUserSessionRepository.cs | 15 + .../EfApiConfigurationRepository.cs | 57 ++ .../EfApplicationSettingsRepository.cs | 73 ++ .../Repositories/EfAudiobookFileRepository.cs | 73 ++ ...EfDownloadClientConfigurationRepository.cs | 56 ++ .../EfDownloadProcessingJobRepository.cs | 8 +- .../Repositories/EfDownloadRepository.cs | 44 +- .../Repositories/EfHistoryRepository.cs | 114 +++ .../Repositories/EfIndexerRepository.cs | 63 ++ .../EfMonitoredAuthorRepository.cs | 58 ++ .../EfMonitoredSeriesRepository.cs | 58 ++ .../Repositories/EfMoveJobRepository.cs | 47 ++ .../EfProcessExecutionLogRepository.cs | 36 + .../EfRemotePathMappingRepository.cs | 55 ++ .../Repositories/EfRootFolderRepository.cs | 24 +- .../Repositories/EfUserRepository.cs | 49 ++ .../Repositories/EfUserSessionRepository.cs | 67 ++ package-lock.json | 391 +++++++++- package.json | 5 +- ...DownloadQueueServiceReconciliationTests.cs | 2 +- .../EndToEndDownloadImportFlowTests.cs | 3 +- .../ForwardedHeadersTrustModelTests.cs | 15 +- ...ibraryController_DeleteImageSafetyTests.cs | 2 +- .../RootFolderServiceTests.cs | 3 +- .../TestCompletedDownloadProcessor.cs | 2 +- .../TestDownloadProcessingJobRepository.cs | 2 +- .../TestDownloadQueueService.cs | 2 +- .../TestDownloadRepository.cs | 2 +- .../Listenarr.Api.Tests/TestServiceFactory.cs | 18 +- tools/dbscan/DbScan.csproj | 2 +- 71 files changed, 2026 insertions(+), 499 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 listenarr.application/Repositories/IApiConfigurationRepository.cs create mode 100644 listenarr.application/Repositories/IApplicationSettingsRepository.cs create mode 100644 listenarr.application/Repositories/IAudiobookFileRepository.cs create mode 100644 listenarr.application/Repositories/IDownloadClientConfigurationRepository.cs rename {listenarr.api => listenarr.application}/Repositories/IDownloadProcessingJobRepository.cs (87%) rename {listenarr.api => listenarr.application}/Repositories/IDownloadRepository.cs (94%) create mode 100644 listenarr.application/Repositories/IHistoryRepository.cs create mode 100644 listenarr.application/Repositories/IIndexerRepository.cs create mode 100644 listenarr.application/Repositories/IMonitoredAuthorRepository.cs create mode 100644 listenarr.application/Repositories/IMonitoredSeriesRepository.cs create mode 100644 listenarr.application/Repositories/IMoveJobRepository.cs create mode 100644 listenarr.application/Repositories/IProcessExecutionLogRepository.cs create mode 100644 listenarr.application/Repositories/IRemotePathMappingRepository.cs rename {listenarr.api => listenarr.application}/Repositories/IRootFolderRepository.cs (90%) create mode 100644 listenarr.application/Repositories/IUserRepository.cs create mode 100644 listenarr.application/Repositories/IUserSessionRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfApiConfigurationRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfApplicationSettingsRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfAudiobookFileRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfDownloadClientConfigurationRepository.cs rename {listenarr.api => listenarr.infrastructure}/Repositories/EfDownloadProcessingJobRepository.cs (87%) rename {listenarr.api => listenarr.infrastructure}/Repositories/EfDownloadRepository.cs (84%) create mode 100644 listenarr.infrastructure/Repositories/EfHistoryRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfIndexerRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfMonitoredAuthorRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfMonitoredSeriesRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfMoveJobRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfProcessExecutionLogRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfRemotePathMappingRepository.cs rename {listenarr.api => listenarr.infrastructure}/Repositories/EfRootFolderRepository.cs (68%) create mode 100644 listenarr.infrastructure/Repositories/EfUserRepository.cs create mode 100644 listenarr.infrastructure/Repositories/EfUserSessionRepository.cs diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 988942fc..cdaa85e0 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -28,15 +28,15 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET 8 + - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - - name: Setup Node 20 + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Resolve target version id: resolve-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 2f5ff403..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: CI - -permissions: - contents: read - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - build-and-test: - runs-on: ubuntu-latest - env: - DOTNET_CLI_TELEMETRY_OPTOUT: "1" - DOTNET_NOLOGO: "1" - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Restore .NET packages - run: dotnet restore listenarr.slnx - - - name: Enforce layering rules - run: | - # Fail CI if listenarr.api source files reference listenarr.infrastructure namespaces. - # This is a lightweight safeguard; for stricter checks consider adding a Roslyn analyzer. - echo "Checking for layering violations (api -> infrastructure)..." - set -e - # Search only C# source files under listenarr.api for references to the infrastructure namespace. - if git grep -n --no-color "listenarr.infrastructure" -- "listenarr.api/**/*.cs" || git grep -n --no-color "Listenarr.Infrastructure" -- "listenarr.api/**/*.cs"; then - echo "Layering violation: listenarr.api references listenarr.infrastructure namespaces or symbols." >&2 - echo "Move contracts to listenarr.application / listenarr.domain and implementations to listenarr.infrastructure." >&2 - exit 1 - fi - echo "No api->infrastructure references found." - - - name: Install frontend deps - working-directory: fe - run: npm ci - - - name: Build frontend - working-directory: fe - run: npm run build --if-present - - - name: Frontend lint - working-directory: fe - run: | - echo "Running frontend linter (if configured)..." - # If a lint script exists it will run; --if-present prevents error when absent. - if npm run lint --silent --if-present; then - echo "Frontend lint passed (or no lint script present)." - else - echo "Frontend lint failed." >&2 - exit 1 - fi - - - name: Run DI assertions - run: dotnet test tests/Listenarr.Api.Tests/Listenarr.Api.Tests.csproj --no-build --filter FullyQualifiedName~DependencyInjectionTests --verbosity minimal - - - name: Build solution - run: dotnet build listenarr.slnx --no-restore --configuration Release - - - name: Run backend tests - run: dotnet test listenarr.slnx --no-build --verbosity normal - - - name: Run frontend tests - working-directory: fe - run: | - echo "Running frontend tests (if configured)..." - # Runs tests if test script exists. --if-present makes npm exit 0 when script is absent. - if npm run test --silent --if-present; then - echo "Frontend tests ran successfully." - else - echo "Frontend tests failed." >&2 - exit 1 - fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0d2c49c7..7f67ca46 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,7 +36,7 @@ jobs: if: matrix.language == 'csharp' uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Restore dependencies if: matrix.language == 'csharp' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5b4ad556..9f26843f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -28,15 +28,15 @@ jobs: with: fetch-depth: 0 - - name: Setup .NET 8 + - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - - name: Setup Node 20 + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Resolve target version id: resolve-version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6feace8..b5e573ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,12 +33,12 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - name: Resolve release and next versions id: resolve-version diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0ed4381f..c4dc5d94 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,31 +5,117 @@ on: branches: [ develop, main, canary, feature/* ] pull_request: branches: [ develop, main, canary ] + types: [ opened, synchronize, reopened ] permissions: contents: read +concurrency: + group: run-tests-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: unit-tests: runs-on: ubuntu-latest - strategy: - matrix: - dotnet: [8.0] + env: + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + DOTNET_NOLOGO: "1" + API_PROJECT: listenarr.api/Listenarr.Api.csproj steps: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Setup Node + uses: actions/setup-node@v4 with: - dotnet-version: ${{ matrix.dotnet }} + node-version: 24 + cache: 'npm' - name: Restore run: dotnet restore listenarr.slnx + - name: Enforce layering rules + run: | + set -e + VIOLATIONS=0 + + echo "Checking for layering violations (api -> infrastructure)..." + if git grep -n --no-color "listenarr.infrastructure" -- "listenarr.api/**/*.cs" ":(exclude)listenarr.api/Program.cs" || git grep -n --no-color "Listenarr.Infrastructure" -- "listenarr.api/**/*.cs" ":(exclude)listenarr.api/Program.cs"; then + echo "Layering violation: listenarr.api references listenarr.infrastructure." >&2 + VIOLATIONS=$((VIOLATIONS + 1)) + fi + + echo "Checking for layering violations (application -> infrastructure)..." + if git grep -n --no-color "listenarr.infrastructure" -- "listenarr.application/**/*.cs" || git grep -n --no-color "Listenarr.Infrastructure" -- "listenarr.application/**/*.cs"; then + echo "Layering violation: listenarr.application references listenarr.infrastructure." >&2 + echo "Define contracts in listenarr.application and implement them in listenarr.infrastructure." >&2 + VIOLATIONS=$((VIOLATIONS + 1)) + fi + + echo "Checking for async void in production code..." + if git grep -n --no-color "async void" -- "listenarr.api/**/*.cs" "listenarr.application/**/*.cs" "listenarr.infrastructure/**/*.cs" "listenarr.domain/**/*.cs"; then + echo "async void found in production code — use async Task instead to avoid swallowed exceptions." >&2 + VIOLATIONS=$((VIOLATIONS + 1)) + fi + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "$VIOLATIONS enforcement check(s) failed." >&2 + exit 1 + fi + echo "All enforcement checks passed." + + - name: Install frontend deps + working-directory: fe + run: npm ci + + - name: Build frontend + working-directory: fe + run: npm run build --if-present + + - name: Frontend lint + working-directory: fe + run: | + if npm run lint --silent --if-present; then + echo "Frontend lint passed (or no lint script present)." + else + echo "Frontend lint failed." >&2 + exit 1 + fi + + - name: Frontend type check + working-directory: fe + run: | + if npm run type-check --silent --if-present; then + echo "Frontend type check passed (or no type-check script present)." + else + echo "Frontend type check failed." >&2 + exit 1 + fi + + - name: Build solution + run: dotnet build listenarr.slnx --no-restore --configuration Release + - name: Run unit tests (with Test environment / disable Playwright) env: ASPNETCORE_ENVIRONMENT: Test Playwright__Enabled: 'false' run: | - dotnet test listenarr.slnx -c Release --no-restore --logger "console;verbosity=normal" + dotnet test listenarr.slnx -c Release --no-build --logger "console;verbosity=normal" + + - name: Run frontend tests + working-directory: fe + run: | + if npm run test --silent --if-present; then + echo "Frontend tests ran successfully." + else + echo "Frontend tests failed." >&2 + exit 1 + fi + + - name: Publish API (linux-x64) + run: dotnet publish ${{ env.API_PROJECT }} -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true -o listenarr.api/publish/linux-x64 diff --git a/Directory.Build.props b/Directory.Build.props index c7c5b474..4b247485 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable latest diff --git a/Directory.Packages.props b/Directory.Packages.props index c9069afb..79c5e497 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,6 @@ - diff --git a/Dockerfile b/Dockerfile index 56843df7..3e42006e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,11 @@ FROM golang:1.26-alpine AS gosu-builder ARG GOSU_VERSION=1.19 RUN CGO_ENABLED=0 go install github.com/tianon/gosu@${GOSU_VERSION} -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app EXPOSE 4545 -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["listenarr.api/Listenarr.Api.csproj", "listenarr.api/"] RUN dotnet restore "listenarr.api/Listenarr.Api.csproj" diff --git a/fe/package-lock.json b/fe/package-lock.json index fe7069b6..481fe9d1 100644 --- a/fe/package-lock.json +++ b/fe/package-lock.json @@ -41,12 +41,12 @@ "npm-run-all2": "^8.0.4", "patch-package": "^8.0.1", "prettier": "3.8.1", - "rollup-plugin-visualizer": "^6.0.11", - "start-server-and-test": "^2.1.5", + "rollup-plugin-visualizer": "^7.0.1", + "start-server-and-test": "^3.0.0", "typescript": "~6.0.0", - "vite": "^8.0.3", + "vite": "^8.0.7", "vite-plugin-vue-devtools": "^8.1.1", - "vitest": "^4.1.2", + "vitest": "^4.1.3", "vue-tsc": "^3.2.6" }, "engines": { @@ -823,7 +823,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -836,7 +835,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -848,7 +846,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1278,9 +1275,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, @@ -1342,9 +1339,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -1395,9 +1392,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -1412,9 +1409,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -1429,9 +1426,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -1446,9 +1443,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -1463,9 +1460,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -1480,9 +1477,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -1500,9 +1497,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -1520,9 +1517,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -1540,9 +1537,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -1560,9 +1557,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -1580,9 +1577,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -1600,9 +1597,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -1617,9 +1614,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -1627,16 +1624,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -1651,9 +1650,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -2144,16 +2143,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2162,13 +2161,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.2", + "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2199,9 +2198,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { @@ -2212,13 +2211,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.4", "pathe": "^2.0.3" }, "funding": { @@ -2226,14 +2225,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2242,9 +2241,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", "dev": true, "license": "MIT", "funding": { @@ -2252,13 +2251,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2969,23 +2968,26 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/axios/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -3802,13 +3804,16 @@ } }, "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/delayed-stream": { @@ -3845,13 +3850,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4374,22 +4372,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -4719,9 +4701,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -4796,13 +4778,6 @@ "node": ">= 6" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true, - "license": "MIT" - }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -4863,6 +4838,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5243,6 +5231,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -5428,9 +5429,9 @@ } }, "node_modules/joi": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", - "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz", + "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5440,7 +5441,7 @@ "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.0.0" + "@standard-schema/spec": "^1.1.0" }, "engines": { "node": ">= 20" @@ -6225,12 +6226,6 @@ "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6925,19 +6920,6 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "license": [ - "MIT", - "Apache2" - ], - "dependencies": { - "through": "~2.3" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7074,6 +7056,19 @@ "node": ">=4" } }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7150,22 +7145,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -7351,14 +7330,14 @@ "license": "MIT" }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -7367,50 +7346,50 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, "node_modules/rollup-plugin-visualizer": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.11.tgz", - "integrity": "sha512-TBwVHVY7buHjIKVLqr9scTVFwqZqMXINcCphPwIWKPDCOBIa+jCQfafvbjRJDZgXdq/A996Dy6yGJ/+/NtAXDQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz", + "integrity": "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==", "dev": true, "license": "MIT", "dependencies": { - "open": "^8.0.0", + "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", - "yargs": "^17.5.1" + "yargs": "^18.0.0" }, "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { - "rolldown": "1.x || ^1.0.0-beta", + "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "peerDependenciesMeta": { @@ -7422,19 +7401,86 @@ } } }, + "node_modules/rollup-plugin-visualizer/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rollup-plugin-visualizer/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", "dev": true, "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7453,6 +7499,103 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/rollup-plugin-visualizer/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -7774,19 +7917,6 @@ "node": ">=0.10.0" } }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -7821,20 +7951,20 @@ "license": "MIT" }, "node_modules/start-server-and-test": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz", - "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-3.0.2.tgz", + "integrity": "sha512-g6v4zPr1RRL5XxXJ+Wnk1GFLb+DGZLjFqse+5lNZ0X7m4SRMC6eOA+AXYboQDfNCEjpnTu0AGrvJb/JTUOg8dQ==", "dev": true, "license": "MIT", "dependencies": { - "arg": "^5.0.2", + "arg": "5.0.2", "bluebird": "3.7.2", "check-more-types": "2.24.0", "debug": "4.4.3", "execa": "5.1.1", "lazy-ass": "1.6.0", - "ps-tree": "1.2.0", - "wait-on": "9.0.4" + "tree-kill": "1.2.2", + "wait-on": "9.0.5" }, "bin": { "server-test": "src/bin/start.js", @@ -7842,7 +7972,7 @@ "start-test": "src/bin/start.js" }, "engines": { - "node": ">=16" + "node": "^22 || >=24" } }, "node_modules/start-server-and-test/node_modules/execa": { @@ -7899,16 +8029,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8515,16 +8635,16 @@ } }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -8542,7 +8662,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -8633,19 +8753,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vite-plugin-vue-devtools/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/vite-plugin-vue-devtools/node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -8769,19 +8876,19 @@ } }, "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -8809,10 +8916,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -8836,6 +8945,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -9072,15 +9187,15 @@ } }, "node_modules/wait-on": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", - "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.5.tgz", + "integrity": "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.13.5", - "joi": "^18.0.2", - "lodash": "^4.17.23", + "axios": "^1.15.0", + "joi": "^18.1.2", + "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, diff --git a/fe/package.json b/fe/package.json index 59b77c5f..47665666 100644 --- a/fe/package.json +++ b/fe/package.json @@ -60,12 +60,12 @@ "npm-run-all2": "^8.0.4", "patch-package": "^8.0.1", "prettier": "3.8.1", - "rollup-plugin-visualizer": "^6.0.11", - "start-server-and-test": "^2.1.5", + "rollup-plugin-visualizer": "^7.0.1", + "start-server-and-test": "^3.0.0", "typescript": "~6.0.0", - "vite": "^8.0.3", + "vite": "^8.0.7", "vite-plugin-vue-devtools": "^8.1.1", - "vitest": "^4.1.2", + "vitest": "^4.1.3", "vue-tsc": "^3.2.6" }, "overrides": { diff --git a/fe/vite.config.ts b/fe/vite.config.ts index c876f955..efae2b06 100644 --- a/fe/vite.config.ts +++ b/fe/vite.config.ts @@ -51,6 +51,21 @@ export default defineConfig(({ mode }) => ({ // default; adding this configure hook forces the header through. configure: (proxy) => { if (proxy && typeof proxy.on === 'function') { + proxy.on('error', (err, _req, res) => { + if ((err as NodeJS.ErrnoException).code === 'ECONNREFUSED') { + // API not ready yet — return 503 silently instead of logging to console + try { + if (res && typeof (res as import('http').ServerResponse).writeHead === 'function') { + const httpRes = res as import('http').ServerResponse + if (!httpRes.headersSent) { + httpRes.writeHead(503, { 'Content-Type': 'application/json' }) + httpRes.end(JSON.stringify({ message: 'API is starting, please retry.' })) + } + } + } catch { /* ignore */ } + return + } + }) proxy.on('proxyReq', (proxyReq, req) => { try { const origCookie = req && req.headers && (req.headers['cookie'] || req.headers.cookie) @@ -66,6 +81,16 @@ export default defineConfig(({ mode }) => ({ target: 'http://localhost:4545', changeOrigin: true, ws: true, + configure: (proxy) => { + if (proxy && typeof proxy.on === 'function') { + proxy.on('error', (err) => { + if ((err as NodeJS.ErrnoException).code !== 'ECONNREFUSED') { + console.error('[vite proxy /hubs]', err) + } + // ECONNREFUSED during startup — ignore silently + }) + } + } } } } diff --git a/global.json b/global.json index dad2db5e..e69d70fb 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "10.0.100", "rollForward": "latestMajor", "allowPrerelease": true } diff --git a/listenarr.api/Controllers/LibraryController.cs b/listenarr.api/Controllers/LibraryController.cs index d7eb03eb..d4fd91ca 100644 --- a/listenarr.api/Controllers/LibraryController.cs +++ b/listenarr.api/Controllers/LibraryController.cs @@ -4112,9 +4112,8 @@ private static string ComputeShortHash(string? input) if (string.IsNullOrEmpty(input)) return Guid.NewGuid().ToString("N").Substring(0, 12); - using var sha1 = SHA1.Create(); var bytes = Encoding.UTF8.GetBytes(input); - var hash = sha1.ComputeHash(bytes); + var hash = SHA1.HashData(bytes); // Return first 16 hex characters for a compact identifier return BitConverter.ToString(hash).Replace("-", "").Substring(0, 16).ToLowerInvariant(); } diff --git a/listenarr.api/Dockerfile.runtime b/listenarr.api/Dockerfile.runtime index b60961d0..b9f1ec8d 100644 --- a/listenarr.api/Dockerfile.runtime +++ b/listenarr.api/Dockerfile.runtime @@ -1,10 +1,10 @@ # Build gosu with a modern Go toolchain to avoid golang/stdlib CVEs present in # the Debian-packaged version (compiled with Go 1.19.x). -FROM golang:1.24-alpine AS gosu-builder +FROM golang:1.26-alpine AS gosu-builder ARG GOSU_VERSION=1.19 RUN CGO_ENABLED=0 go install github.com/tianon/gosu@${GOSU_VERSION} -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app EXPOSE 5656 @@ -18,11 +18,13 @@ ENV DOCKER_ENV=true # node_modules. RUN apt-get update \ && apt-get install -y curl ca-certificates gnupg --no-install-recommends \ - && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ && apt-get install -y nodejs --no-install-recommends \ - && npm install -g npm@10.9.8 --prefix /usr/local \ + && npm install -g npm@11.12.1 --prefix /usr/local \ && rm -rf /usr/lib/node_modules/npm \ && rm -f /usr/bin/npm /usr/bin/npx \ + && npm install --prefix /usr/local/lib/node_modules/npm --no-save --no-package-lock picomatch@4.0.4 \ + && npm install --prefix /usr/local/lib/node_modules/npm --no-save --no-package-lock brace-expansion@2.0.3 \ && rm -rf /var/lib/apt/lists/* # Use the gosu binary built above instead of the apt package. diff --git a/listenarr.api/Extensions/AppServiceRegistrationExtensions.cs b/listenarr.api/Extensions/AppServiceRegistrationExtensions.cs index 953b2909..e5eb7100 100644 --- a/listenarr.api/Extensions/AppServiceRegistrationExtensions.cs +++ b/listenarr.api/Extensions/AppServiceRegistrationExtensions.cs @@ -93,9 +93,9 @@ public static IServiceCollection AddListenarrAppServices(this IServiceCollection services.AddScoped(); // Repository for Download entity encapsulating EF operations used by application services - services.AddScoped(); + services.AddScoped(); // Repository for DownloadProcessingJob queries - services.AddScoped(); + services.AddScoped(); // Discord bot service for managing bot process services.AddSingleton(); diff --git a/listenarr.api/Listenarr.Api.csproj b/listenarr.api/Listenarr.Api.csproj index 3db9b56e..9ff908e5 100644 --- a/listenarr.api/Listenarr.Api.csproj +++ b/listenarr.api/Listenarr.Api.csproj @@ -25,7 +25,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/listenarr.api/Program.cs b/listenarr.api/Program.cs index 1ddb6bad..06608f94 100644 --- a/listenarr.api/Program.cs +++ b/listenarr.api/Program.cs @@ -275,11 +275,11 @@ ex is IOException ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("fc00::"), 7)); - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(IPAddress.Parse("fe80::"), 10)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse("fc00::"), 7)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse("fe80::"), 10)); }); // Add SignalR for real-time updates @@ -291,7 +291,7 @@ ex is IOException }); // RootFolder repository + service -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); // Migrator for legacy single-outputPath -> RootFolder migration builder.Services.AddScoped(); diff --git a/listenarr.api/Services/CompletedDownloadProcessor.cs b/listenarr.api/Services/CompletedDownloadProcessor.cs index 9b1639f7..3f1b1c88 100644 --- a/listenarr.api/Services/CompletedDownloadProcessor.cs +++ b/listenarr.api/Services/CompletedDownloadProcessor.cs @@ -11,7 +11,7 @@ namespace Listenarr.Api.Services { public class CompletedDownloadProcessor : ICompletedDownloadProcessor { - private readonly Listenarr.Api.Repositories.IDownloadRepository _downloadRepository; + private readonly Listenarr.Application.Repositories.IDownloadRepository _downloadRepository; private readonly IFileFinalizer _fileFinalizer; private readonly IConfigurationService _configurationService; private readonly IServiceScopeFactory _serviceScopeFactory; @@ -25,7 +25,7 @@ public class CompletedDownloadProcessor : ICompletedDownloadProcessor private readonly IAppMetricsService _metrics; public CompletedDownloadProcessor( - Listenarr.Api.Repositories.IDownloadRepository downloadRepository, + Listenarr.Application.Repositories.IDownloadRepository downloadRepository, IFileFinalizer fileFinalizer, IConfigurationService configurationService, IServiceScopeFactory serviceScopeFactory, diff --git a/listenarr.api/Services/DownloadQueueService.cs b/listenarr.api/Services/DownloadQueueService.cs index da046812..53282671 100644 --- a/listenarr.api/Services/DownloadQueueService.cs +++ b/listenarr.api/Services/DownloadQueueService.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Domain.Models; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; diff --git a/listenarr.api/Services/FileFinalizer.cs b/listenarr.api/Services/FileFinalizer.cs index 863ce0b7..295d6def 100644 --- a/listenarr.api/Services/FileFinalizer.cs +++ b/listenarr.api/Services/FileFinalizer.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Listenarr.Domain.Models; using Listenarr.Infrastructure.Models; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Microsoft.Extensions.Logging; namespace Listenarr.Api.Services diff --git a/listenarr.api/Services/LibraryAddService.cs b/listenarr.api/Services/LibraryAddService.cs index 8f8b45f2..c9ae8d8a 100644 --- a/listenarr.api/Services/LibraryAddService.cs +++ b/listenarr.api/Services/LibraryAddService.cs @@ -372,9 +372,8 @@ private static string ComputeShortHash(string? input) return Guid.NewGuid().ToString("N").Substring(0, 12); } - using var sha1 = SHA1.Create(); var bytes = Encoding.UTF8.GetBytes(input); - var hash = sha1.ComputeHash(bytes); + var hash = SHA1.HashData(bytes); return BitConverter.ToString(hash).Replace("-", "").Substring(0, 16).ToLowerInvariant(); } diff --git a/listenarr.api/Services/RootFolderService.cs b/listenarr.api/Services/RootFolderService.cs index 9b166f6c..e6bc3e4c 100644 --- a/listenarr.api/Services/RootFolderService.cs +++ b/listenarr.api/Services/RootFolderService.cs @@ -1,4 +1,4 @@ -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Infrastructure.Models; using Microsoft.EntityFrameworkCore; diff --git a/listenarr.api/Services/UserService.cs b/listenarr.api/Services/UserService.cs index d4bdcc64..625f99e7 100644 --- a/listenarr.api/Services/UserService.cs +++ b/listenarr.api/Services/UserService.cs @@ -115,8 +115,7 @@ private static string HashPassword(string password) var salt = new byte[16]; rng.GetBytes(salt); - using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); - var hash = pbkdf2.GetBytes(32); + var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, 32); return Convert.ToBase64String(salt) + ":" + Convert.ToBase64String(hash); } @@ -130,8 +129,7 @@ private static bool VerifyPassword(string password, string stored) var salt = Convert.FromBase64String(parts[0]); var hash = Convert.FromBase64String(parts[1]); - using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 100_000, HashAlgorithmName.SHA256); - var computed = pbkdf2.GetBytes(hash.Length); + var computed = Rfc2898DeriveBytes.Pbkdf2(password, salt, 100_000, HashAlgorithmName.SHA256, hash.Length); return CryptographicOperations.FixedTimeEquals(computed, hash); } catch (Exception caughtEx_1) when (caughtEx_1 is not OperationCanceledException && caughtEx_1 is not OutOfMemoryException && caughtEx_1 is not StackOverflowException) { diff --git a/listenarr.api/tools/discord-bot/package-lock.json b/listenarr.api/tools/discord-bot/package-lock.json index 1bb8e307..db4722f8 100644 --- a/listenarr.api/tools/discord-bot/package-lock.json +++ b/listenarr.api/tools/discord-bot/package-lock.json @@ -12,7 +12,7 @@ "discord.js": "^14.26.2", "fetch-cookie": "^3.2.0", "node-fetch": "^3.3.2", - "sanitize-html": "^2.17.2", + "sanitize-html": "^2.17.3", "tough-cookie": "^6.0.1" }, "engines": { @@ -701,9 +701,9 @@ "license": "MIT" }, "node_modules/sanitize-html": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.2.tgz", - "integrity": "sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz", + "integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==", "license": "MIT", "dependencies": { "deepmerge": "^4.2.2", diff --git a/listenarr.api/tools/discord-bot/package.json b/listenarr.api/tools/discord-bot/package.json index 333f26a0..b0714f11 100644 --- a/listenarr.api/tools/discord-bot/package.json +++ b/listenarr.api/tools/discord-bot/package.json @@ -16,7 +16,7 @@ "node-fetch": "^3.3.2", "fetch-cookie": "^3.2.0", "tough-cookie": "^6.0.1", - "sanitize-html": "^2.17.2" + "sanitize-html": "^2.17.3" }, "overrides": { "undici": "^6.24.1" diff --git a/listenarr.application/Repositories/IApiConfigurationRepository.cs b/listenarr.application/Repositories/IApiConfigurationRepository.cs new file mode 100644 index 00000000..d0e4a26f --- /dev/null +++ b/listenarr.application/Repositories/IApiConfigurationRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IApiConfigurationRepository + { + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(string id, CancellationToken ct = default); + Task SaveAsync(ApiConfiguration config, CancellationToken ct = default); + Task DeleteAsync(string id, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IApplicationSettingsRepository.cs b/listenarr.application/Repositories/IApplicationSettingsRepository.cs new file mode 100644 index 00000000..6e6579c8 --- /dev/null +++ b/listenarr.application/Repositories/IApplicationSettingsRepository.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IApplicationSettingsRepository + { + Task GetAsync(CancellationToken ct = default); + Task SaveAsync(ApplicationSettings settings, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IAudiobookFileRepository.cs b/listenarr.application/Repositories/IAudiobookFileRepository.cs new file mode 100644 index 00000000..ae2cd2d3 --- /dev/null +++ b/listenarr.application/Repositories/IAudiobookFileRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IAudiobookFileRepository + { + Task GetByIdAsync(int id, CancellationToken ct = default); + Task> GetByAudiobookIdAsync(int audiobookId, CancellationToken ct = default); + Task> GetMissingMetadataAsync(int max, CancellationToken ct = default); + Task AddAsync(AudiobookFile file, CancellationToken ct = default); + Task UpdateAsync(AudiobookFile file, CancellationToken ct = default); + Task DeleteByAudiobookIdAsync(int audiobookId, CancellationToken ct = default); + Task ExistsAtPathAsync(int audiobookId, string path, CancellationToken ct = default); + Task IsPathUsedByOtherAsync(int audiobookId, string path, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IDownloadClientConfigurationRepository.cs b/listenarr.application/Repositories/IDownloadClientConfigurationRepository.cs new file mode 100644 index 00000000..b88fde8a --- /dev/null +++ b/listenarr.application/Repositories/IDownloadClientConfigurationRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IDownloadClientConfigurationRepository + { + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(string id, CancellationToken ct = default); + Task SaveAsync(DownloadClientConfiguration config, CancellationToken ct = default); + Task DeleteAsync(string id, CancellationToken ct = default); + } +} diff --git a/listenarr.api/Repositories/IDownloadProcessingJobRepository.cs b/listenarr.application/Repositories/IDownloadProcessingJobRepository.cs similarity index 87% rename from listenarr.api/Repositories/IDownloadProcessingJobRepository.cs rename to listenarr.application/Repositories/IDownloadProcessingJobRepository.cs index 8469e28e..a8e1ec6d 100644 --- a/listenarr.api/Repositories/IDownloadProcessingJobRepository.cs +++ b/listenarr.application/Repositories/IDownloadProcessingJobRepository.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Listenarr.Api.Repositories +namespace Listenarr.Application.Repositories { public interface IDownloadProcessingJobRepository { diff --git a/listenarr.api/Repositories/IDownloadRepository.cs b/listenarr.application/Repositories/IDownloadRepository.cs similarity index 94% rename from listenarr.api/Repositories/IDownloadRepository.cs rename to listenarr.application/Repositories/IDownloadRepository.cs index a6a63823..9069c26b 100644 --- a/listenarr.api/Repositories/IDownloadRepository.cs +++ b/listenarr.application/Repositories/IDownloadRepository.cs @@ -1,8 +1,8 @@ -using System.Threading.Tasks; using System.Collections.Generic; +using System.Threading.Tasks; using Listenarr.Domain.Models; -namespace Listenarr.Api.Repositories +namespace Listenarr.Application.Repositories { public interface IDownloadRepository { diff --git a/listenarr.application/Repositories/IHistoryRepository.cs b/listenarr.application/Repositories/IHistoryRepository.cs new file mode 100644 index 00000000..d9fbd184 --- /dev/null +++ b/listenarr.application/Repositories/IHistoryRepository.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IHistoryRepository + { + Task> GetPagedAsync(int limit, int offset, CancellationToken ct = default); + Task CountAsync(CancellationToken ct = default); + Task> GetByAudiobookIdAsync(int audiobookId, CancellationToken ct = default); + Task> GetByEventTypeAsync(string eventType, int? limit = null, CancellationToken ct = default); + Task> GetBySourceAsync(string source, int? limit = null, CancellationToken ct = default); + Task> GetRecentAsync(int limit, CancellationToken ct = default); + Task AddAsync(History entry, CancellationToken ct = default); + Task UpdateAsync(History entry, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); + Task DeleteAllAsync(CancellationToken ct = default); + Task DeleteOlderThanAsync(DateTime cutoff, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IIndexerRepository.cs b/listenarr.application/Repositories/IIndexerRepository.cs new file mode 100644 index 00000000..f20b97d9 --- /dev/null +++ b/listenarr.application/Repositories/IIndexerRepository.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IIndexerRepository + { + Task GetByIdAsync(int id, CancellationToken ct = default); + Task> GetAllAsync(CancellationToken ct = default); + Task> GetEnabledAsync(bool isAutomaticSearch, CancellationToken ct = default); + Task AddAsync(Indexer indexer, CancellationToken ct = default); + Task UpdateAsync(Indexer indexer, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IMonitoredAuthorRepository.cs b/listenarr.application/Repositories/IMonitoredAuthorRepository.cs new file mode 100644 index 00000000..381264f3 --- /dev/null +++ b/listenarr.application/Repositories/IMonitoredAuthorRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IMonitoredAuthorRepository + { + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task UpsertAsync(MonitoredAuthor author, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IMonitoredSeriesRepository.cs b/listenarr.application/Repositories/IMonitoredSeriesRepository.cs new file mode 100644 index 00000000..442fc37c --- /dev/null +++ b/listenarr.application/Repositories/IMonitoredSeriesRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IMonitoredSeriesRepository + { + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task UpsertAsync(MonitoredSeries series, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IMoveJobRepository.cs b/listenarr.application/Repositories/IMoveJobRepository.cs new file mode 100644 index 00000000..f6f26401 --- /dev/null +++ b/listenarr.application/Repositories/IMoveJobRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IMoveJobRepository + { + Task GetByIdAsync(Guid id, CancellationToken ct = default); + Task> GetByStatusAsync(IEnumerable statuses, CancellationToken ct = default); + Task AddAsync(MoveJob job, CancellationToken ct = default); + Task UpdateAsync(MoveJob job, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IProcessExecutionLogRepository.cs b/listenarr.application/Repositories/IProcessExecutionLogRepository.cs new file mode 100644 index 00000000..ee1d75b9 --- /dev/null +++ b/listenarr.application/Repositories/IProcessExecutionLogRepository.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IProcessExecutionLogRepository + { + Task AddAsync(ProcessExecutionLog log, CancellationToken ct = default); + Task> GetRecentAsync(int limit, CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IRemotePathMappingRepository.cs b/listenarr.application/Repositories/IRemotePathMappingRepository.cs new file mode 100644 index 00000000..8e9ae826 --- /dev/null +++ b/listenarr.application/Repositories/IRemotePathMappingRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IRemotePathMappingRepository + { + Task> GetAllAsync(CancellationToken ct = default); + Task GetByIdAsync(int id, CancellationToken ct = default); + Task SaveAsync(RemotePathMapping mapping, CancellationToken ct = default); + Task DeleteAsync(int id, CancellationToken ct = default); + } +} diff --git a/listenarr.api/Repositories/IRootFolderRepository.cs b/listenarr.application/Repositories/IRootFolderRepository.cs similarity index 90% rename from listenarr.api/Repositories/IRootFolderRepository.cs rename to listenarr.application/Repositories/IRootFolderRepository.cs index 0eee0aeb..3aa95e8a 100644 --- a/listenarr.api/Repositories/IRootFolderRepository.cs +++ b/listenarr.application/Repositories/IRootFolderRepository.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Listenarr.Domain.Models; -namespace Listenarr.Api.Repositories +namespace Listenarr.Application.Repositories { public interface IRootFolderRepository { @@ -14,4 +14,4 @@ public interface IRootFolderRepository Task RemoveAsync(int id); Task GetDefaultAsync(); } -} \ No newline at end of file +} diff --git a/listenarr.application/Repositories/IUserRepository.cs b/listenarr.application/Repositories/IUserRepository.cs new file mode 100644 index 00000000..9d2ec722 --- /dev/null +++ b/listenarr.application/Repositories/IUserRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IUserRepository + { + Task GetByUsernameAsync(string username, CancellationToken ct = default); + Task AddAsync(User user, CancellationToken ct = default); + Task UpdateAsync(User user, CancellationToken ct = default); + Task> GetAdminUsersAsync(CancellationToken ct = default); + Task CountAsync(CancellationToken ct = default); + } +} diff --git a/listenarr.application/Repositories/IUserSessionRepository.cs b/listenarr.application/Repositories/IUserSessionRepository.cs new file mode 100644 index 00000000..e3b08398 --- /dev/null +++ b/listenarr.application/Repositories/IUserSessionRepository.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Domain.Models; + +namespace Listenarr.Application.Repositories +{ + public interface IUserSessionRepository + { + Task CreateAsync(UserSession session, CancellationToken ct = default); + Task GetByTokenHashAsync(string tokenHash, CancellationToken ct = default); + Task InvalidateAsync(string sessionToken, CancellationToken ct = default); + Task InvalidateAllForUserAsync(string username, CancellationToken ct = default); + Task CleanupExpiredAsync(CancellationToken ct = default); + } +} diff --git a/listenarr.infrastructure/Repositories/EfApiConfigurationRepository.cs b/listenarr.infrastructure/Repositories/EfApiConfigurationRepository.cs new file mode 100644 index 00000000..3f7e13d2 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfApiConfigurationRepository.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfApiConfigurationRepository : IApiConfigurationRepository + { + private readonly ListenArrDbContext _db; + + public EfApiConfigurationRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.ApiConfigurations.AsNoTracking().OrderBy(c => c.Priority).ToListAsync(ct); + } + + public async Task GetByIdAsync(string id, CancellationToken ct = default) + { + return await _db.ApiConfigurations.FirstOrDefaultAsync(c => c.Id == id, ct); + } + + public async Task SaveAsync(ApiConfiguration config, CancellationToken ct = default) + { + var existing = await _db.ApiConfigurations.FirstOrDefaultAsync(c => c.Id == config.Id, ct); + if (existing == null) + { + _db.ApiConfigurations.Add(config); + await _db.SaveChangesAsync(ct); + return config; + } + + _db.Entry(existing).CurrentValues.SetValues(config); + existing.HeadersJson = config.HeadersJson; + existing.ParametersJson = config.ParametersJson; + await _db.SaveChangesAsync(ct); + return existing; + } + + public async Task DeleteAsync(string id, CancellationToken ct = default) + { + var config = await _db.ApiConfigurations.FirstOrDefaultAsync(c => c.Id == id, ct); + if (config == null) return false; + _db.ApiConfigurations.Remove(config); + await _db.SaveChangesAsync(ct); + return true; + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfApplicationSettingsRepository.cs b/listenarr.infrastructure/Repositories/EfApplicationSettingsRepository.cs new file mode 100644 index 00000000..c0e1588f --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfApplicationSettingsRepository.cs @@ -0,0 +1,73 @@ +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfApplicationSettingsRepository : IApplicationSettingsRepository + { + private readonly ListenArrDbContext _db; + + public EfApplicationSettingsRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task GetAsync(CancellationToken ct = default) + { + return await _db.ApplicationSettings.AsNoTracking().FirstOrDefaultAsync(s => s.Id == 1, ct); + } + + public async Task SaveAsync(ApplicationSettings settings, CancellationToken ct = default) + { + settings.Id = 1; + + var existing = await _db.ApplicationSettings.FirstOrDefaultAsync(s => s.Id == 1, ct); + + if (existing == null) + { + _db.ApplicationSettings.Add(settings); + await _db.SaveChangesAsync(ct); + return settings; + } + + _db.Entry(existing).CurrentValues.SetValues(settings); + existing.AllowedFileExtensions = settings.AllowedFileExtensions; + existing.ImportBlacklistExtensions = settings.ImportBlacklistExtensions ?? new List(); + + if (settings.EnabledNotificationTriggers != null) + { + existing.EnabledNotificationTriggers = settings.EnabledNotificationTriggers; + _db.Entry(existing).Property(e => e.EnabledNotificationTriggers).IsModified = true; + } + else + { + existing.EnabledNotificationTriggers ??= new List(); + } + + if (settings.Webhooks != null) + { + existing.Webhooks = settings.Webhooks.Select(w => new WebhookConfiguration + { + Name = w.Name, + Url = w.Url, + Type = w.Type, + Triggers = w.Triggers?.ToList() ?? new List(), + IsEnabled = w.IsEnabled + }).ToList(); + _db.Entry(existing).Property(e => e.Webhooks).IsModified = true; + } + else + { + existing.Webhooks ??= new List(); + } + + _db.Update(existing); + await _db.SaveChangesAsync(ct); + return existing; + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfAudiobookFileRepository.cs b/listenarr.infrastructure/Repositories/EfAudiobookFileRepository.cs new file mode 100644 index 00000000..d0b0213a --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfAudiobookFileRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfAudiobookFileRepository : IAudiobookFileRepository + { + private readonly ListenArrDbContext _db; + + public EfAudiobookFileRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + return await _db.AudiobookFiles.FindAsync(new object[] { id }, ct); + } + + public async Task> GetByAudiobookIdAsync(int audiobookId, CancellationToken ct = default) + { + return await _db.AudiobookFiles + .AsNoTracking() + .Where(f => f.AudiobookId == audiobookId) + .ToListAsync(ct); + } + + public async Task> GetMissingMetadataAsync(int max, CancellationToken ct = default) + { + return await _db.AudiobookFiles + .AsNoTracking() + .Where(f => f.DurationSeconds == null || f.Format == null || f.SampleRate == null) + .Take(max) + .ToListAsync(ct); + } + + public async Task AddAsync(AudiobookFile file, CancellationToken ct = default) + { + _db.AudiobookFiles.Add(file); + await _db.SaveChangesAsync(ct); + return file; + } + + public async Task UpdateAsync(AudiobookFile file, CancellationToken ct = default) + { + _db.AudiobookFiles.Update(file); + await _db.SaveChangesAsync(ct); + } + + public async Task DeleteByAudiobookIdAsync(int audiobookId, CancellationToken ct = default) + { + var files = await _db.AudiobookFiles.Where(f => f.AudiobookId == audiobookId).ToListAsync(ct); + _db.AudiobookFiles.RemoveRange(files); + await _db.SaveChangesAsync(ct); + } + + public async Task ExistsAtPathAsync(int audiobookId, string path, CancellationToken ct = default) + { + return await _db.AudiobookFiles.AnyAsync(f => f.AudiobookId == audiobookId && f.Path == path, ct); + } + + public async Task IsPathUsedByOtherAsync(int audiobookId, string path, CancellationToken ct = default) + { + return await _db.AudiobookFiles.AnyAsync(f => f.AudiobookId != audiobookId && f.Path == path, ct); + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfDownloadClientConfigurationRepository.cs b/listenarr.infrastructure/Repositories/EfDownloadClientConfigurationRepository.cs new file mode 100644 index 00000000..239dcc2a --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfDownloadClientConfigurationRepository.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfDownloadClientConfigurationRepository : IDownloadClientConfigurationRepository + { + private readonly ListenArrDbContext _db; + + public EfDownloadClientConfigurationRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.DownloadClientConfigurations.AsNoTracking().OrderBy(c => c.Name).ToListAsync(ct); + } + + public async Task GetByIdAsync(string id, CancellationToken ct = default) + { + return await _db.DownloadClientConfigurations.FirstOrDefaultAsync(c => c.Id == id, ct); + } + + public async Task SaveAsync(DownloadClientConfiguration config, CancellationToken ct = default) + { + var existing = await _db.DownloadClientConfigurations.FirstOrDefaultAsync(c => c.Id == config.Id, ct); + if (existing == null) + { + _db.DownloadClientConfigurations.Add(config); + await _db.SaveChangesAsync(ct); + return config; + } + + _db.Entry(existing).CurrentValues.SetValues(config); + existing.SettingsJson = config.SettingsJson; + await _db.SaveChangesAsync(ct); + return existing; + } + + public async Task DeleteAsync(string id, CancellationToken ct = default) + { + var config = await _db.DownloadClientConfigurations.FirstOrDefaultAsync(c => c.Id == id, ct); + if (config == null) return false; + _db.DownloadClientConfigurations.Remove(config); + await _db.SaveChangesAsync(ct); + return true; + } + } +} diff --git a/listenarr.api/Repositories/EfDownloadProcessingJobRepository.cs b/listenarr.infrastructure/Repositories/EfDownloadProcessingJobRepository.cs similarity index 87% rename from listenarr.api/Repositories/EfDownloadProcessingJobRepository.cs rename to listenarr.infrastructure/Repositories/EfDownloadProcessingJobRepository.cs index d10c900a..7cae7a94 100644 --- a/listenarr.api/Repositories/EfDownloadProcessingJobRepository.cs +++ b/listenarr.infrastructure/Repositories/EfDownloadProcessingJobRepository.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; using Listenarr.Infrastructure.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Listenarr.Api.Repositories +namespace Listenarr.Infrastructure.Repositories { public class EfDownloadProcessingJobRepository : IDownloadProcessingJobRepository { @@ -24,7 +26,7 @@ public async Task> GetPendingDownloadIdsAsync(IEnumerable c var ids = (completedDownloadIds ?? Array.Empty()).ToList(); if (!ids.Any()) return new List(); - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.DownloadProcessingJobs .Where(j => ids.Contains(j.DownloadId) && (j.Status == ProcessingJobStatus.Pending || j.Status == ProcessingJobStatus.Processing || j.Status == ProcessingJobStatus.Retry)) .Select(j => j.DownloadId) @@ -37,7 +39,7 @@ public async Task> GetAllJobDownloadIdsAsync(IEnumerable co var ids = (completedDownloadIds ?? Array.Empty()).ToList(); if (!ids.Any()) return new List(); - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.DownloadProcessingJobs .Where(j => ids.Contains(j.DownloadId)) .Select(j => j.DownloadId) diff --git a/listenarr.api/Repositories/EfDownloadRepository.cs b/listenarr.infrastructure/Repositories/EfDownloadRepository.cs similarity index 84% rename from listenarr.api/Repositories/EfDownloadRepository.cs rename to listenarr.infrastructure/Repositories/EfDownloadRepository.cs index 16293ff4..e38f1356 100644 --- a/listenarr.api/Repositories/EfDownloadRepository.cs +++ b/listenarr.infrastructure/Repositories/EfDownloadRepository.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Listenarr.Infrastructure.Models; +using Listenarr.Application.Repositories; using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Listenarr.Api.Repositories +namespace Listenarr.Infrastructure.Repositories { public class EfDownloadRepository : IDownloadRepository { @@ -22,30 +23,30 @@ public EfDownloadRepository(IDbContextFactory dbFactory, ILo public async Task AddAsync(Download download) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); ctx.Downloads.Add(download); await ctx.SaveChangesAsync(); } public async Task FindAsync(string id) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.Downloads.FindAsync(id); } public async Task UpdateAsync(Download download) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); ctx.Downloads.Update(download); await ctx.SaveChangesAsync(); } public async Task UpdateMetadataAsync(string id, string key, object? value) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); var d = await ctx.Downloads.FindAsync(id); if (d == null) return; - if (d.Metadata == null) d.Metadata = new System.Collections.Generic.Dictionary(); + if (d.Metadata == null) d.Metadata = new Dictionary(); d.Metadata[key] = value ?? string.Empty; ctx.Downloads.Update(d); await ctx.SaveChangesAsync(); @@ -53,7 +54,7 @@ public async Task UpdateMetadataAsync(string id, string key, object? value) public async Task RemoveAsync(string id) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); var d = await ctx.Downloads.FindAsync(id); if (d == null) return; ctx.Downloads.Remove(d); @@ -62,13 +63,13 @@ public async Task RemoveAsync(string id) public async Task> GetAllAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.Downloads.AsNoTracking().ToListAsync(); } public async Task> GetQueueDisplayCandidatesAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); var ddl = await ctx.Downloads .AsNoTracking() .Where(d => d.DownloadClientId == "DDL" && d.Status != DownloadStatus.Moved) @@ -86,7 +87,7 @@ public async Task> GetQueueDisplayCandidatesAsync() public async Task> GetQueueMatchingCandidatesAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.Downloads .AsNoTracking() .Where(d => d.DownloadClientId != "DDL" && d.Status != DownloadStatus.Failed) @@ -96,7 +97,7 @@ public async Task> GetQueueMatchingCandidatesAsync() public async Task> GetKnownClientItemIdsAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); var metadataEntries = await ctx.Downloads .AsNoTracking() .Select(d => d.Metadata) @@ -105,20 +106,11 @@ public async Task> GetKnownClientItemIdsAsync() var ids = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var metadata in metadataEntries) { - if (metadata == null) - { - continue; - } - + if (metadata == null) continue; if (TryGetMetadataString(metadata, "ClientDownloadId", out var clientDownloadId)) - { ids.Add(clientDownloadId); - } - if (TryGetMetadataString(metadata, "TorrentHash", out var torrentHash)) - { ids.Add(torrentHash); - } } return ids.ToList(); @@ -126,7 +118,7 @@ public async Task> GetKnownClientItemIdsAsync() public async Task> GetByClientAsync(string clientId) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.Downloads .AsNoTracking() .Where(d => d.DownloadClientId == clientId) @@ -137,7 +129,7 @@ public async Task> GetByIdsAsync(IEnumerable ids) { var idSet = ids?.ToList() ?? new List(); if (!idSet.Any()) return new List(); - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.Downloads .AsNoTracking() .Where(d => idSet.Contains(d.Id)) @@ -147,12 +139,8 @@ public async Task> GetByIdsAsync(IEnumerable ids) private static bool TryGetMetadataString(Dictionary? metadata, string key, out string value) { value = string.Empty; - if (metadata == null || !metadata.TryGetValue(key, out var raw) || raw == null) - { return false; - } - value = raw.ToString() ?? string.Empty; return !string.IsNullOrWhiteSpace(value); } diff --git a/listenarr.infrastructure/Repositories/EfHistoryRepository.cs b/listenarr.infrastructure/Repositories/EfHistoryRepository.cs new file mode 100644 index 00000000..33065f9b --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfHistoryRepository.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfHistoryRepository : Listenarr.Application.Repositories.IHistoryRepository + { + private readonly ListenArrDbContext _db; + + public EfHistoryRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetPagedAsync(int limit, int offset, CancellationToken ct = default) + { + return await _db.History + .AsNoTracking() + .OrderByDescending(h => h.Timestamp) + .Skip(offset) + .Take(limit) + .ToListAsync(ct); + } + + public async Task CountAsync(CancellationToken ct = default) + { + return await _db.History.CountAsync(ct); + } + + public async Task> GetByAudiobookIdAsync(int audiobookId, CancellationToken ct = default) + { + return await _db.History + .AsNoTracking() + .Where(h => h.AudiobookId == audiobookId) + .OrderByDescending(h => h.Timestamp) + .ToListAsync(ct); + } + + public async Task> GetByEventTypeAsync(string eventType, int? limit = null, CancellationToken ct = default) + { + var query = _db.History + .AsNoTracking() + .Where(h => h.EventType == eventType) + .OrderByDescending(h => h.Timestamp); + + return limit.HasValue + ? await query.Take(limit.Value).ToListAsync(ct) + : await query.ToListAsync(ct); + } + + public async Task> GetBySourceAsync(string source, int? limit = null, CancellationToken ct = default) + { + var query = _db.History + .AsNoTracking() + .Where(h => h.Source == source) + .OrderByDescending(h => h.Timestamp); + + return limit.HasValue + ? await query.Take(limit.Value).ToListAsync(ct) + : await query.ToListAsync(ct); + } + + public async Task> GetRecentAsync(int limit, CancellationToken ct = default) + { + return await _db.History + .AsNoTracking() + .OrderByDescending(h => h.Timestamp) + .Take(limit) + .ToListAsync(ct); + } + + public async Task AddAsync(History entry, CancellationToken ct = default) + { + _db.History.Add(entry); + await _db.SaveChangesAsync(ct); + return entry; + } + + public async Task UpdateAsync(History entry, CancellationToken ct = default) + { + _db.History.Update(entry); + await _db.SaveChangesAsync(ct); + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var entry = await _db.History.FindAsync(new object[] { id }, ct); + if (entry == null) return false; + _db.History.Remove(entry); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task DeleteAllAsync(CancellationToken ct = default) + { + _db.History.RemoveRange(_db.History); + await _db.SaveChangesAsync(ct); + } + + public async Task DeleteOlderThanAsync(DateTime cutoff, CancellationToken ct = default) + { + var old = await _db.History.Where(h => h.Timestamp < cutoff).ToListAsync(ct); + _db.History.RemoveRange(old); + await _db.SaveChangesAsync(ct); + return old.Count; + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfIndexerRepository.cs b/listenarr.infrastructure/Repositories/EfIndexerRepository.cs new file mode 100644 index 00000000..e0a46122 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfIndexerRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfIndexerRepository : IIndexerRepository + { + private readonly ListenArrDbContext _db; + + public EfIndexerRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + return await _db.Indexers.FindAsync(new object[] { id }, ct); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.Indexers.AsNoTracking().ToListAsync(ct); + } + + public async Task> GetEnabledAsync(bool isAutomaticSearch, CancellationToken ct = default) + { + return await _db.Indexers + .AsNoTracking() + .Where(i => i.IsEnabled && (isAutomaticSearch ? i.EnableAutomaticSearch : i.EnableInteractiveSearch)) + .OrderBy(i => i.Priority) + .ToListAsync(ct); + } + + public async Task AddAsync(Indexer indexer, CancellationToken ct = default) + { + _db.Indexers.Add(indexer); + await _db.SaveChangesAsync(ct); + return indexer; + } + + public async Task UpdateAsync(Indexer indexer, CancellationToken ct = default) + { + var existing = await _db.Indexers.FindAsync(new object[] { indexer.Id }, ct); + if (existing == null) throw new InvalidOperationException($"Indexer {indexer.Id} not found."); + _db.Entry(existing).CurrentValues.SetValues(indexer); + await _db.SaveChangesAsync(ct); + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var indexer = await _db.Indexers.FindAsync(new object[] { id }, ct); + if (indexer == null) return; + _db.Indexers.Remove(indexer); + await _db.SaveChangesAsync(ct); + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfMonitoredAuthorRepository.cs b/listenarr.infrastructure/Repositories/EfMonitoredAuthorRepository.cs new file mode 100644 index 00000000..8e0e0512 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfMonitoredAuthorRepository.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfMonitoredAuthorRepository : IMonitoredAuthorRepository + { + private readonly ListenArrDbContext _db; + + public EfMonitoredAuthorRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.MonitoredAuthors.AsNoTracking().ToListAsync(ct); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + return await _db.MonitoredAuthors.FindAsync(new object[] { id }, ct); + } + + public async Task UpsertAsync(MonitoredAuthor author, CancellationToken ct = default) + { + var existing = author.Id > 0 + ? await _db.MonitoredAuthors.FindAsync(new object[] { author.Id }, ct) + : null; + + if (existing == null) + { + _db.MonitoredAuthors.Add(author); + await _db.SaveChangesAsync(ct); + return author; + } + + _db.Entry(existing).CurrentValues.SetValues(author); + await _db.SaveChangesAsync(ct); + return existing; + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var author = await _db.MonitoredAuthors.FindAsync(new object[] { id }, ct); + if (author == null) return false; + _db.MonitoredAuthors.Remove(author); + await _db.SaveChangesAsync(ct); + return true; + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfMonitoredSeriesRepository.cs b/listenarr.infrastructure/Repositories/EfMonitoredSeriesRepository.cs new file mode 100644 index 00000000..7f846660 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfMonitoredSeriesRepository.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfMonitoredSeriesRepository : IMonitoredSeriesRepository + { + private readonly ListenArrDbContext _db; + + public EfMonitoredSeriesRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.MonitoredSeries.AsNoTracking().ToListAsync(ct); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + return await _db.MonitoredSeries.FindAsync(new object[] { id }, ct); + } + + public async Task UpsertAsync(MonitoredSeries series, CancellationToken ct = default) + { + var existing = series.Id > 0 + ? await _db.MonitoredSeries.FindAsync(new object[] { series.Id }, ct) + : null; + + if (existing == null) + { + _db.MonitoredSeries.Add(series); + await _db.SaveChangesAsync(ct); + return series; + } + + _db.Entry(existing).CurrentValues.SetValues(series); + await _db.SaveChangesAsync(ct); + return existing; + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var series = await _db.MonitoredSeries.FindAsync(new object[] { id }, ct); + if (series == null) return false; + _db.MonitoredSeries.Remove(series); + await _db.SaveChangesAsync(ct); + return true; + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfMoveJobRepository.cs b/listenarr.infrastructure/Repositories/EfMoveJobRepository.cs new file mode 100644 index 00000000..1a94956f --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfMoveJobRepository.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfMoveJobRepository : IMoveJobRepository + { + private readonly ListenArrDbContext _db; + + public EfMoveJobRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task GetByIdAsync(Guid id, CancellationToken ct = default) + { + return await _db.MoveJobs.FindAsync(new object[] { id }, ct); + } + + public async Task> GetByStatusAsync(IEnumerable statuses, CancellationToken ct = default) + { + return await _db.MoveJobs + .AsNoTracking() + .Where(j => statuses.Contains(j.Status)) + .ToListAsync(ct); + } + + public async Task AddAsync(MoveJob job, CancellationToken ct = default) + { + _db.MoveJobs.Add(job); + await _db.SaveChangesAsync(ct); + return job; + } + + public async Task UpdateAsync(MoveJob job, CancellationToken ct = default) + { + _db.MoveJobs.Update(job); + await _db.SaveChangesAsync(ct); + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfProcessExecutionLogRepository.cs b/listenarr.infrastructure/Repositories/EfProcessExecutionLogRepository.cs new file mode 100644 index 00000000..93575901 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfProcessExecutionLogRepository.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfProcessExecutionLogRepository : IProcessExecutionLogRepository + { + private readonly ListenArrDbContext _db; + + public EfProcessExecutionLogRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task AddAsync(ProcessExecutionLog log, CancellationToken ct = default) + { + _db.ProcessExecutionLogs.Add(log); + await _db.SaveChangesAsync(ct); + } + + public async Task> GetRecentAsync(int limit, CancellationToken ct = default) + { + return await _db.ProcessExecutionLogs + .AsNoTracking() + .OrderByDescending(l => l.Timestamp) + .Take(limit) + .ToListAsync(ct); + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfRemotePathMappingRepository.cs b/listenarr.infrastructure/Repositories/EfRemotePathMappingRepository.cs new file mode 100644 index 00000000..194353fe --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfRemotePathMappingRepository.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfRemotePathMappingRepository : IRemotePathMappingRepository + { + private readonly ListenArrDbContext _db; + + public EfRemotePathMappingRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task> GetAllAsync(CancellationToken ct = default) + { + return await _db.RemotePathMappings.AsNoTracking().ToListAsync(ct); + } + + public async Task GetByIdAsync(int id, CancellationToken ct = default) + { + return await _db.RemotePathMappings.FindAsync(new object[] { id }, ct); + } + + public async Task SaveAsync(RemotePathMapping mapping, CancellationToken ct = default) + { + var existing = await _db.RemotePathMappings.FindAsync(new object[] { mapping.Id }, ct); + if (existing == null) + { + _db.RemotePathMappings.Add(mapping); + } + else + { + _db.Entry(existing).CurrentValues.SetValues(mapping); + } + await _db.SaveChangesAsync(ct); + return existing ?? mapping; + } + + public async Task DeleteAsync(int id, CancellationToken ct = default) + { + var mapping = await _db.RemotePathMappings.FindAsync(new object[] { id }, ct); + if (mapping == null) return false; + _db.RemotePathMappings.Remove(mapping); + await _db.SaveChangesAsync(ct); + return true; + } + } +} diff --git a/listenarr.api/Repositories/EfRootFolderRepository.cs b/listenarr.infrastructure/Repositories/EfRootFolderRepository.cs similarity index 68% rename from listenarr.api/Repositories/EfRootFolderRepository.cs rename to listenarr.infrastructure/Repositories/EfRootFolderRepository.cs index 6b90a7d0..0258e960 100644 --- a/listenarr.api/Repositories/EfRootFolderRepository.cs +++ b/listenarr.infrastructure/Repositories/EfRootFolderRepository.cs @@ -1,12 +1,14 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Listenarr.Application.Repositories; using Listenarr.Domain.Models; using Listenarr.Infrastructure.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Listenarr.Api.Repositories +namespace Listenarr.Infrastructure.Repositories { public class EfRootFolderRepository : IRootFolderRepository { @@ -15,38 +17,38 @@ public class EfRootFolderRepository : IRootFolderRepository public EfRootFolderRepository(IDbContextFactory dbFactory, ILogger logger) { - _dbFactory = dbFactory; - _logger = logger; + _dbFactory = dbFactory ?? throw new ArgumentNullException(nameof(dbFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task AddAsync(RootFolder root) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); ctx.RootFolders.Add(root); await ctx.SaveChangesAsync(); } public async Task> GetAllAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.RootFolders.OrderBy(r => r.Name).ToListAsync(); } public async Task GetByIdAsync(int id) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.RootFolders.FindAsync(id); } public async Task GetByPathAsync(string path) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.RootFolders.FirstOrDefaultAsync(r => r.Path == path); } public async Task RemoveAsync(int id) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); var r = await ctx.RootFolders.FindAsync(id); if (r == null) return; ctx.RootFolders.Remove(r); @@ -55,15 +57,15 @@ public async Task RemoveAsync(int id) public async Task UpdateAsync(RootFolder root) { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); ctx.RootFolders.Update(root); await ctx.SaveChangesAsync(); } public async Task GetDefaultAsync() { - var ctx = await _dbFactory.CreateDbContextAsync(); + await using var ctx = await _dbFactory.CreateDbContextAsync(); return await ctx.RootFolders.FirstOrDefaultAsync(r => r.IsDefault); } } -} \ No newline at end of file +} diff --git a/listenarr.infrastructure/Repositories/EfUserRepository.cs b/listenarr.infrastructure/Repositories/EfUserRepository.cs new file mode 100644 index 00000000..9a2c707d --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfUserRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfUserRepository : IUserRepository + { + private readonly ListenArrDbContext _db; + + public EfUserRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task GetByUsernameAsync(string username, CancellationToken ct = default) + { + return await _db.Users.FirstOrDefaultAsync(u => u.Username == username, ct); + } + + public async Task AddAsync(User user, CancellationToken ct = default) + { + _db.Users.Add(user); + await _db.SaveChangesAsync(ct); + return user; + } + + public async Task UpdateAsync(User user, CancellationToken ct = default) + { + _db.Users.Update(user); + await _db.SaveChangesAsync(ct); + } + + public async Task> GetAdminUsersAsync(CancellationToken ct = default) + { + return await _db.Users.Where(u => u.IsAdmin).ToListAsync(ct); + } + + public async Task CountAsync(CancellationToken ct = default) + { + return await _db.Users.CountAsync(ct); + } + } +} diff --git a/listenarr.infrastructure/Repositories/EfUserSessionRepository.cs b/listenarr.infrastructure/Repositories/EfUserSessionRepository.cs new file mode 100644 index 00000000..2ee71736 --- /dev/null +++ b/listenarr.infrastructure/Repositories/EfUserSessionRepository.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Listenarr.Application.Repositories; +using Listenarr.Domain.Models; +using Listenarr.Infrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace Listenarr.Infrastructure.Repositories +{ + public class EfUserSessionRepository : IUserSessionRepository + { + private readonly ListenArrDbContext _db; + + public EfUserSessionRepository(ListenArrDbContext db) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + } + + public async Task CreateAsync(UserSession session, CancellationToken ct = default) + { + _db.UserSessions.Add(session); + await _db.SaveChangesAsync(ct); + return session; + } + + public async Task GetByTokenHashAsync(string tokenHash, CancellationToken ct = default) + { + var session = await _db.UserSessions.SingleOrDefaultAsync(s => s.TokenHash == tokenHash, ct); + if (session == null) return null; + + if (session.ExpiresAt < DateTime.UtcNow) + { + _db.UserSessions.Remove(session); + await _db.SaveChangesAsync(ct); + return null; + } + + session.LastAccessed = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return session; + } + + public async Task InvalidateAsync(string sessionToken, CancellationToken ct = default) + { + var session = await _db.UserSessions.SingleOrDefaultAsync(s => s.TokenHash == sessionToken, ct); + if (session == null) return; + _db.UserSessions.Remove(session); + await _db.SaveChangesAsync(ct); + } + + public async Task InvalidateAllForUserAsync(string username, CancellationToken ct = default) + { + var sessions = await _db.UserSessions.Where(s => s.Username == username).ToListAsync(ct); + _db.UserSessions.RemoveRange(sessions); + await _db.SaveChangesAsync(ct); + } + + public async Task CleanupExpiredAsync(CancellationToken ct = default) + { + var expired = await _db.UserSessions.Where(s => s.ExpiresAt < DateTime.UtcNow).ToListAsync(ct); + _db.UserSessions.RemoveRange(expired); + await _db.SaveChangesAsync(ct); + return expired.Count; + } + } +} diff --git a/package-lock.json b/package-lock.json index 2d48975b..7d95a060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,64 @@ "name": "listenarr", "version": "0.2.71", "dependencies": { - "concurrently": "^9.2.1" + "concurrently": "^9.2.1", + "wait-on": "^8.0.3" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -35,6 +90,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -95,6 +180,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", @@ -119,12 +216,80 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -134,6 +299,51 @@ "node": ">=6" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -143,6 +353,55 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -152,6 +411,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -161,6 +459,78 @@ "node": ">=8" } }, + "node_modules/joi": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.1.2.tgz", + "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -247,6 +617,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index df6c8c8f..8c98c196 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "pretest": "npm run version:sync", "dev": "concurrently \"npm run dev:api\" \"npm run dev:web\" --names \"API,WEB\" --prefix-colors \"blue,green\"", "dev:api": "cd listenarr.api && dotnet run", - "dev:web": "cd fe && npm run dev", + "dev:web": "wait-on http-get://localhost:4545/api/v1/system/health -t 120000 -i 1000 --httpTimeout 3000 && cd fe && npm run dev", "build": "npm run build:api && npm run build:web", "build:api": "cd listenarr.api && dotnet build -c Release", "build:web": "cd fe && npm run build", @@ -19,6 +19,7 @@ "test": "cd fe && npm run test:unit" }, "dependencies": { - "concurrently": "^9.2.1" + "concurrently": "^9.2.1", + "wait-on": "^8.0.3" } } diff --git a/tests/Listenarr.Api.Tests/DownloadQueueServiceReconciliationTests.cs b/tests/Listenarr.Api.Tests/DownloadQueueServiceReconciliationTests.cs index d06e35be..96234bbc 100644 --- a/tests/Listenarr.Api.Tests/DownloadQueueServiceReconciliationTests.cs +++ b/tests/Listenarr.Api.Tests/DownloadQueueServiceReconciliationTests.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Api.Services; using Listenarr.Domain.Models; using Listenarr.Infrastructure.Models; diff --git a/tests/Listenarr.Api.Tests/EndToEndDownloadImportFlowTests.cs b/tests/Listenarr.Api.Tests/EndToEndDownloadImportFlowTests.cs index 5067a129..4dbc494e 100644 --- a/tests/Listenarr.Api.Tests/EndToEndDownloadImportFlowTests.cs +++ b/tests/Listenarr.Api.Tests/EndToEndDownloadImportFlowTests.cs @@ -6,7 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Listenarr.Api.Hubs; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; +using Listenarr.Infrastructure.Repositories; using Listenarr.Api.Services; using Listenarr.Domain.Models; using Listenarr.Infrastructure.Models; diff --git a/tests/Listenarr.Api.Tests/ForwardedHeadersTrustModelTests.cs b/tests/Listenarr.Api.Tests/ForwardedHeadersTrustModelTests.cs index ebe7323f..356de7df 100644 --- a/tests/Listenarr.Api.Tests/ForwardedHeadersTrustModelTests.cs +++ b/tests/Listenarr.Api.Tests/ForwardedHeadersTrustModelTests.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -using HttpOverridesIPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Listenarr.Api.Tests { @@ -27,16 +26,16 @@ public void ForwardedHeadersOptions_TrustsCommonPrivateProxyNetworks() Assert.True(options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto)); Assert.True(options.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost)); - Assert.Contains(options.KnownNetworks, network => Matches(network, "10.0.0.0", 8)); - Assert.Contains(options.KnownNetworks, network => Matches(network, "172.16.0.0", 12)); - Assert.Contains(options.KnownNetworks, network => Matches(network, "192.168.0.0", 16)); - Assert.Contains(options.KnownNetworks, network => Matches(network, "fc00::", 7)); - Assert.Contains(options.KnownNetworks, network => Matches(network, "fe80::", 10)); + Assert.Contains(options.KnownIPNetworks, network => Matches(network, "10.0.0.0", 8)); + Assert.Contains(options.KnownIPNetworks, network => Matches(network, "172.16.0.0", 12)); + Assert.Contains(options.KnownIPNetworks, network => Matches(network, "192.168.0.0", 16)); + Assert.Contains(options.KnownIPNetworks, network => Matches(network, "fc00::", 7)); + Assert.Contains(options.KnownIPNetworks, network => Matches(network, "fe80::", 10)); } - private static bool Matches(HttpOverridesIPNetwork network, string prefix, int prefixLength) + private static bool Matches(System.Net.IPNetwork network, string prefix, int prefixLength) { - return network.Prefix.Equals(IPAddress.Parse(prefix)) && network.PrefixLength == prefixLength; + return network.BaseAddress.Equals(IPAddress.Parse(prefix)) && network.PrefixLength == prefixLength; } } } diff --git a/tests/Listenarr.Api.Tests/LibraryController_DeleteImageSafetyTests.cs b/tests/Listenarr.Api.Tests/LibraryController_DeleteImageSafetyTests.cs index bc2e7d59..32e1c185 100644 --- a/tests/Listenarr.Api.Tests/LibraryController_DeleteImageSafetyTests.cs +++ b/tests/Listenarr.Api.Tests/LibraryController_DeleteImageSafetyTests.cs @@ -7,7 +7,7 @@ using Listenarr.Api.Controllers; using Listenarr.Api.Services; using Listenarr.Infrastructure.Models; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; diff --git a/tests/Listenarr.Api.Tests/RootFolderServiceTests.cs b/tests/Listenarr.Api.Tests/RootFolderServiceTests.cs index 971dd488..77c8d0fa 100644 --- a/tests/Listenarr.Api.Tests/RootFolderServiceTests.cs +++ b/tests/Listenarr.Api.Tests/RootFolderServiceTests.cs @@ -7,7 +7,8 @@ using Moq; using Xunit.Sdk; using Listenarr.Infrastructure.Models; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; +using Listenarr.Infrastructure.Repositories; using Listenarr.Api.Services; using Listenarr.Domain.Models; using Listenarr.Domain.Utils; diff --git a/tests/Listenarr.Api.Tests/TestCompletedDownloadProcessor.cs b/tests/Listenarr.Api.Tests/TestCompletedDownloadProcessor.cs index 5696714b..265091ca 100644 --- a/tests/Listenarr.Api.Tests/TestCompletedDownloadProcessor.cs +++ b/tests/Listenarr.Api.Tests/TestCompletedDownloadProcessor.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Domain.Models; namespace Listenarr.Api.Tests diff --git a/tests/Listenarr.Api.Tests/TestDownloadProcessingJobRepository.cs b/tests/Listenarr.Api.Tests/TestDownloadProcessingJobRepository.cs index fa127225..ce77f850 100644 --- a/tests/Listenarr.Api.Tests/TestDownloadProcessingJobRepository.cs +++ b/tests/Listenarr.Api.Tests/TestDownloadProcessingJobRepository.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Infrastructure.Models; using Microsoft.EntityFrameworkCore; diff --git a/tests/Listenarr.Api.Tests/TestDownloadQueueService.cs b/tests/Listenarr.Api.Tests/TestDownloadQueueService.cs index 25dd5be1..b25920b8 100644 --- a/tests/Listenarr.Api.Tests/TestDownloadQueueService.cs +++ b/tests/Listenarr.Api.Tests/TestDownloadQueueService.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Listenarr.Api.Services; using Listenarr.Domain.Models; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Microsoft.Extensions.Logging; namespace Listenarr.Api.Tests diff --git a/tests/Listenarr.Api.Tests/TestDownloadRepository.cs b/tests/Listenarr.Api.Tests/TestDownloadRepository.cs index 36be06c4..bef58747 100644 --- a/tests/Listenarr.Api.Tests/TestDownloadRepository.cs +++ b/tests/Listenarr.Api.Tests/TestDownloadRepository.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Listenarr.Api.Repositories; +using Listenarr.Application.Repositories; using Listenarr.Domain.Models; using Microsoft.EntityFrameworkCore; using Listenarr.Infrastructure.Models; diff --git a/tests/Listenarr.Api.Tests/TestServiceFactory.cs b/tests/Listenarr.Api.Tests/TestServiceFactory.cs index 65d88b19..8f62da79 100644 --- a/tests/Listenarr.Api.Tests/TestServiceFactory.cs +++ b/tests/Listenarr.Api.Tests/TestServiceFactory.cs @@ -73,13 +73,13 @@ public static ServiceProvider BuildServiceProvider(Action? c // Provide a test-friendly IDownloadRepository so tests that resolve DownloadService // from the root provider don't need to register it explicitly. Prefer an existing // ListenArrDbContext if present, otherwise fall back to an in-memory test repo. - services.AddSingleton(sp => + services.AddSingleton(sp => { var dbFactory = sp.GetService>(); if (dbFactory != null) { - var logger = sp.GetRequiredService>(); - return new Listenarr.Api.Repositories.EfDownloadRepository(dbFactory, logger); + var logger = sp.GetRequiredService>(); + return new Listenarr.Infrastructure.Repositories.EfDownloadRepository(dbFactory, logger); } var db = sp.GetService(); @@ -87,13 +87,13 @@ public static ServiceProvider BuildServiceProvider(Action? c }); // Provide a test-friendly IDownloadProcessingJobRepository for tests. - services.AddSingleton(sp => + services.AddSingleton(sp => { var dbFactory = sp.GetService>(); if (dbFactory != null) { - var logger = sp.GetRequiredService>(); - return new Listenarr.Api.Repositories.EfDownloadProcessingJobRepository(dbFactory, logger); + var logger = sp.GetRequiredService>(); + return new Listenarr.Infrastructure.Repositories.EfDownloadProcessingJobRepository(dbFactory, logger); } var db = sp.GetService(); @@ -107,7 +107,7 @@ public static ServiceProvider BuildServiceProvider(Action? c var import = sp.GetService(); if (import != null) { - var downloadRepo = sp.GetRequiredService(); + var downloadRepo = sp.GetRequiredService(); var scopeFactory = sp.GetRequiredService(); var logger = sp.GetRequiredService>(); return new Listenarr.Api.Services.FileFinalizer(import, downloadRepo, scopeFactory, logger); @@ -133,7 +133,7 @@ public static ServiceProvider BuildServiceProvider(Action? c // Provide a test-friendly IDownloadQueueService so DownloadService can be resolved services.AddSingleton(sp => { - var downloadRepo = sp.GetService(); + var downloadRepo = sp.GetService(); var clientGateway = sp.GetService(); var config = sp.GetService(); var logger = sp.GetService>(); @@ -152,7 +152,7 @@ public static ServiceProvider BuildServiceProvider(Action? c // Provide a test-friendly ICompletedDownloadProcessor so DownloadService can be resolved in tests. services.AddSingleton(sp => { - var downloadRepo = sp.GetService(); + var downloadRepo = sp.GetService(); var fileFinalizer = sp.GetService(); var config = sp.GetService(); var scopeFactory = sp.GetService(); diff --git a/tools/dbscan/DbScan.csproj b/tools/dbscan/DbScan.csproj index 73fd110a..4f69063e 100644 --- a/tools/dbscan/DbScan.csproj +++ b/tools/dbscan/DbScan.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net10.0 enable enable From 90127e1268de47db2e1832daa6d6dddf1d803db2 Mon Sep 17 00:00:00 2001 From: Robbie Davis Date: Sun, 19 Apr 2026 10:49:17 -0400 Subject: [PATCH 02/20] Use repository interfaces instead of DbContext Replace direct ListenArrDbContext access with repository abstractions across the API to decouple data access and improve testability. Controllers (AdminMetadataController, DownloadsController, HistoryController, IndexersController, and others) now use repository interfaces (e.g. IDownloadRepository, IIndexerRepository, IAudiobookFileRepository, IHistoryRepository) and call repository methods instead of EF Core DbContext operations. Added new application/infrastructure pieces (IDownloadHistoryRepository, IDatabaseConnectionProvider, QueueStats, EfDatabaseConnectionProvider) and updated service registration accordingly. Also: bump package versions (Microsoft.AspNetCore.Mvc.Testing -> 10.0.6; Serilog.AspNetCore -> 10.0.0; Serilog.Sinks.File -> 7.0.0). Frontend fixes include improved typings and test mocks (use vi.mocked, typed SearchResult/RootFolder), added v-for keys, minor refactors (var -> let), normalized genre typing, and removal of some unused UI helpers. A number of tests were updated to match the refactor. --- Directory.Packages.props | 6 +- .../EditAudiobookModal.moveOptions.spec.ts | 6 +- fe/src/__tests__/LibraryImportFooter.spec.ts | 11 +- .../LibraryImportSearchModal.spec.ts | 3 +- fe/src/__tests__/libraryImport.store.spec.ts | 8 +- .../components/feedback/ManualImportModal.vue | 10 +- .../settings/RootFolderFormModal.vue | 2 +- fe/src/stores/libraryImport.ts | 13 +- fe/src/views/activity/ActivityView.vue | 30 -- fe/src/views/library/CollectionView.vue | 4 - .../Controllers/AdminMetadataController.cs | 39 +-- .../Controllers/DownloadsController.cs | 54 ++-- .../Controllers/HistoryController.cs | 94 ++---- .../Controllers/IndexersController.cs | 53 ++-- .../Controllers/LibraryController.cs | 292 +++++++---------- .../Controllers/ProwlarrCompatController.cs | 61 ++-- .../Controllers/RootFoldersController.cs | 20 +- .../AppServiceRegistrationExtensions.cs | 5 - .../Extensions/ServiceProviderExtensions.cs | 80 ----- .../ServiceRegistrationExtensions.cs | 67 ---- listenarr.api/Program.cs | 12 +- listenarr.api/Services/AudioFileService.cs | 89 ++---- listenarr.api/Services/AudiobookDtoFactory.cs | 3 +- .../Services/AudiobookMatchingService.cs | 12 +- .../Services/AuthorMonitoringService.cs | 62 +--- .../Services/AutomaticSearchService.cs | 75 +++-- .../CompletedDownloadHandlingService.cs | 32 +- .../Services/CompletedDownloadProcessor.cs | 130 +------- .../Services/ConfigurationService.cs | 297 +++--------------- .../Services/DownloadHashRetrievalService.cs | 6 +- .../Services/DownloadMonitorService.cs | 211 ++++--------- .../DownloadProcessingBackgroundService.cs | 68 ++-- .../DownloadProcessingQueueService.cs | 146 ++------- listenarr.api/Services/DownloadService.cs | 131 ++------ .../Services/DownloadStateMachine.cs | 6 +- .../Services/DownloadValidationPipeline.cs | 6 +- listenarr.api/Services/FileFinalizer.cs | 38 --- .../Services/FileProcessingHandler.cs | 7 +- .../IDownloadProcessingQueueService.cs | 14 - listenarr.api/Services/ImportService.cs | 41 +-- listenarr.api/Services/LibraryAddService.cs | 14 +- .../Services/MetadataRescanService.cs | 28 +- .../Services/MoveBackgroundService.cs | 32 +- listenarr.api/Services/MoveQueueService.cs | 35 +-- listenarr.api/Services/NzbUrlResolver.cs | 19 +- .../Services/ProcessExecutionStore.cs | 12 +- .../Services/QualityProfileService.cs | 95 +----- .../Services/RemotePathMappingService.cs | 112 +++---- listenarr.api/Services/RenameService.cs | 21 +- listenarr.api/Services/RootFolderService.cs | 167 +++------- .../Services/ScanBackgroundService.cs | 55 ++-- .../Services/Scoring/CompositeScorer.cs | 3 +- .../Services/Scoring/SearchResultScorer.cs | 12 +- .../Services/Search/AsinCandidateCollector.cs | 1 - .../Services/Search/AsinSearchHandler.cs | 1 - .../Search/Filters/AudiobookOnlyFilter.cs | 1 - .../Search/Filters/ISearchResultFilter.cs | 1 - .../Search/Filters/KindleEditionFilter.cs | 1 - .../Filters/MissingInformationFilter.cs | 1 - .../Search/Filters/ProductLikeTitleFilter.cs | 1 - .../Search/Filters/PromotionalTitleFilter.cs | 1 - .../Filters/SearchResultFilterPipeline.cs | 1 - .../Services/Search/MetadataConverters.cs | 1 - .../Services/Search/MetadataMerger.cs | 1 - .../InternetArchiveSearchProvider.cs | 1 - .../Providers/MyAnonamouseSearchProvider.cs | 13 +- .../Providers/TorznabNewznabSearchProvider.cs | 1 - .../Services/Search/SearchValidation.cs | 1 - .../Strategies/AudibleMetadataStrategy.cs | 1 - .../Search/Strategies/AudnexusStrategy.cs | 1 - .../Search/Strategies/IMetadataStrategy.cs | 1 - .../Strategies/MetadataStrategyCoordinator.cs | 1 - listenarr.api/Services/SearchService.cs | 67 ++-- .../Services/SeriesMonitoringService.cs | 62 +--- listenarr.api/Services/SessionService.cs | 71 +---- listenarr.api/Services/StartupDbNormalizer.cs | 41 +-- .../UnmatchedScanBackgroundService.cs | 15 +- listenarr.api/Services/UserService.cs | 25 +- .../Repositories/IAudiobookFileRepository.cs | 3 + .../IDownloadHistoryRepository.cs | 22 ++ .../IDownloadProcessingJobRepository.cs | 15 + .../Repositories/IDownloadRepository.cs | 1 + .../Repositories/IIndexerRepository.cs | 1 + .../IMonitoredAuthorRepository.cs | 2 + .../IMonitoredSeriesRepository.cs | 3 + .../IRemotePathMappingRepository.cs | 1 + .../Repositories/IRootFolderRepository.cs | 6 + .../Repositories/IUserSessionRepository.cs | 1 + .../Services/IAudiobookRepository.cs | 8 +- .../Services/IDatabaseConnectionProvider.cs | 10 + listenarr.domain/Models/QueueStats.cs | 16 + ...astructureServiceRegistrationExtensions.cs | 85 ++++- .../Repositories/DownloadHistoryRepository.cs | 3 +- .../EfApplicationSettingsRepository.cs | 38 +-- .../Repositories/EfAudiobookFileRepository.cs | 25 ++ .../EfDownloadProcessingJobRepository.cs | 140 +++++++++ .../Repositories/EfDownloadRepository.cs | 9 + .../Repositories/EfIndexerRepository.cs | 5 + .../EfMonitoredAuthorRepository.cs | 14 + .../EfMonitoredSeriesRepository.cs | 14 + .../EfRemotePathMappingRepository.cs | 9 + .../Repositories/EfRootFolderRepository.cs | 73 +++++ .../Repositories/EfUserSessionRepository.cs | 14 + .../Services/AudiobookRepository.cs | 54 ++++ .../Services/EfDatabaseConnectionProvider.cs | 26 ++ .../AudioFileServiceTests.cs | 9 + ...oFileService_UpdateAudiobookFieldsTests.cs | 3 + .../AudiobookDtoFactoryTests.cs | 4 +- .../AuthorMonitoringServiceTests.cs | 14 +- .../CompletedDownloadProcessorTests.cs | 24 +- .../ConfigurationServiceTests.cs | 40 +-- .../DownloadMonitorFinalizationTests.cs | 10 +- ...nloadMonitorPipelineClientCoverageTests.cs | 1 + .../DownloadMonitorServiceTests.cs | 2 + .../DownloadNaming_AudiobookMetadataTests.cs | 2 +- ...rocessingBackgroundServiceRecoveryTests.cs | 3 + .../DownloadProcessingQueueServiceTests.cs | 3 + .../DownloadProcessingTests.cs | 4 +- ...ownloadProcessing_FileMissingRetryTests.cs | 3 + .../DownloadProcessing_NoDoubleMoveTests.cs | 2 +- .../DownloadService_ImportTests.cs | 55 ++-- .../DownloadsControllerTests.cs | 14 +- .../EndToEndDownloadImportFlowTests.cs | 5 +- .../ImportServiceHardlinkTests.cs | 8 +- .../Listenarr.Api.Tests/ImportServiceTests.cs | 55 +++- .../Listenarr.Api.Tests/IndexersAuthTests.cs | 2 +- .../IndexersControllerProwlarrImportTests.cs | 7 +- .../IndexersControllerTests.cs | 2 +- .../IndexersNewznabAuthTests.cs | 6 +- .../IndexersNewznabParsingTests.cs | 7 +- .../IndexersPersistedAuthTests.cs | 2 +- .../LibraryController_AddToLibraryTests.cs | 37 ++- .../LibraryController_BasePathTests.cs | 27 +- .../LibraryController_BulkUpdateTests.cs | 14 +- ...LibraryController_DeleteFilesystemTests.cs | 19 +- ...ibraryController_DeleteImageSafetyTests.cs | 15 +- ...yController_LibraryListSlimPayloadTests.cs | 25 +- .../LibraryController_MoveTests.cs | 23 +- .../LibraryController_QualityCutoffTests.cs | 29 +- ...ryController_ScanPathConfigFailureTests.cs | 9 +- ...braryController_ScanPathValidationTests.cs | 16 +- .../LibraryController_UpdateAudiobookTests.cs | 15 +- ...aryController_WantedFlagRegressionTests.cs | 51 ++- .../MetadataIntegrationTests.cs | 3 + .../MoveBackgroundServiceTests.cs | 2 + .../MoveBackgroundService_BroadcastTests.cs | 4 +- .../MoveBackgroundService_FailureTests.cs | 2 + ...groundService_FilePathPreservationTests.cs | 6 +- .../MoveQueueServiceTests.cs | 14 +- .../MyAnonamouseCookieTests.cs | 31 +- .../ProwlarrCompatControllerTests.cs | 14 +- .../QualityProfileScoringTests.cs | 13 +- .../QualityScoringTests.cs | 5 +- .../Listenarr.Api.Tests/RenameServiceTests.cs | 7 +- .../RootFolderServiceTests.cs | 30 +- .../RootFoldersControllerTests.cs | 22 +- .../SearchServiceFixesTests.cs | 5 +- .../SearchServiceScoringTests.cs | 5 +- .../SearchServiceSortingTests.cs | 8 +- .../SeriesMonitoringServiceTests.cs | 7 +- .../TestDownloadProcessingJobRepository.cs | 121 +++++-- .../TestDownloadRepository.cs | 9 + .../Listenarr.Api.Tests/TestServiceFactory.cs | 15 + 163 files changed, 2008 insertions(+), 2641 deletions(-) delete mode 100644 listenarr.api/Extensions/ServiceProviderExtensions.cs create mode 100644 listenarr.application/Repositories/IDownloadHistoryRepository.cs create mode 100644 listenarr.application/Services/IDatabaseConnectionProvider.cs create mode 100644 listenarr.domain/Models/QueueStats.cs create mode 100644 listenarr.infrastructure/Services/EfDatabaseConnectionProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 79c5e497..bfe42e3f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,7 +12,7 @@ - + @@ -24,8 +24,8 @@ - - + + diff --git a/fe/src/__tests__/EditAudiobookModal.moveOptions.spec.ts b/fe/src/__tests__/EditAudiobookModal.moveOptions.spec.ts index 347b5ebf..f8a8b886 100644 --- a/fe/src/__tests__/EditAudiobookModal.moveOptions.spec.ts +++ b/fe/src/__tests__/EditAudiobookModal.moveOptions.spec.ts @@ -241,8 +241,8 @@ describe('EditAudiobookModal move options', () => { it('hydrates current metadata immediately and renders person fields as tags', async () => { const { apiService } = await import('@/services/api') - ;(apiService.getQualityProfiles as any).mockImplementation(() => new Promise(() => {})) - ;(apiService.getAudiobook as any).mockResolvedValue({ + vi.mocked(apiService.getQualityProfiles).mockImplementation(() => new Promise(() => {})) + vi.mocked(apiService.getAudiobook).mockResolvedValue({ ...audiobook, subtitle: 'Existing Subtitle', narrators: ['Narrator One', 'Narrator Two'], @@ -313,7 +313,7 @@ describe('EditAudiobookModal move options', () => { it('rehydrates unchanged metadata when the same audiobook receives fuller data', async () => { const { apiService } = await import('@/services/api') - ;(apiService.getAudiobook as any).mockResolvedValue({ + vi.mocked(apiService.getAudiobook).mockResolvedValue({ ...audiobook, description: 'Loaded from refreshed detail payload', publishedDate: '2024-03-01', diff --git a/fe/src/__tests__/LibraryImportFooter.spec.ts b/fe/src/__tests__/LibraryImportFooter.spec.ts index 88e8da0e..0babf1e4 100644 --- a/fe/src/__tests__/LibraryImportFooter.spec.ts +++ b/fe/src/__tests__/LibraryImportFooter.spec.ts @@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import LibraryImportFooter from '@/components/domain/audiobook/LibraryImportFooter.vue' import { useLibraryImportStore } from '@/stores/libraryImport' +import type { SearchResult, RootFolder } from '@/types' const success = vi.fn() const error = vi.fn() @@ -33,7 +34,7 @@ describe('LibraryImportFooter', () => { folderName: 'Book 1', format: 'MP3', fileCount: 1, - selectedMatch: { title: 'Book 1', authors: [] } as any, + selectedMatch: { title: 'Book 1', authors: [] } as unknown as SearchResult, hasSearched: true, isSearching: false, selected: true, @@ -47,14 +48,14 @@ describe('LibraryImportFooter', () => { folderName: 'Book 2', format: 'MP3', fileCount: 1, - selectedMatch: { title: 'Book 2', authors: [] } as any, + selectedMatch: { title: 'Book 2', authors: [] } as unknown as SearchResult, hasSearched: true, isSearching: false, selected: true, }, - } as any + } - ;(store as any).importSelected = vi.fn( + vi.spyOn(store, 'importSelected').mockImplementation( () => new Promise<{ imported: number; errors: string[] }>((resolve) => { resolveImport = resolve @@ -63,7 +64,7 @@ describe('LibraryImportFooter', () => { const wrapper = mount(LibraryImportFooter, { props: { - folders: [{ id: 1, path: 'D:\\library' }] as any, + folders: [{ id: 1, path: 'D:\\library' }] as unknown as RootFolder[], }, global: { plugins: [pinia], diff --git a/fe/src/__tests__/LibraryImportSearchModal.spec.ts b/fe/src/__tests__/LibraryImportSearchModal.spec.ts index 896f2b53..1d1cc2b5 100644 --- a/fe/src/__tests__/LibraryImportSearchModal.spec.ts +++ b/fe/src/__tests__/LibraryImportSearchModal.spec.ts @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { apiService } from '@/services/api' +import type { SearchResult } from '@/types' import LibraryImportSearchModal from '@/components/domain/audiobook/LibraryImportSearchModal.vue' const getProtectedImageSrc = vi.fn(() => 'https://example.com/protected.jpg') @@ -20,7 +21,7 @@ describe('LibraryImportSearchModal', () => { title: 'Alchemised', imageUrl: '/api/v1/images/B000APXZHK', authors: [{ name: 'SenLinYu' }], - } as any, + } as unknown as SearchResult, ]) }) diff --git a/fe/src/__tests__/libraryImport.store.spec.ts b/fe/src/__tests__/libraryImport.store.spec.ts index 0c1fd78e..a0b3106d 100644 --- a/fe/src/__tests__/libraryImport.store.spec.ts +++ b/fe/src/__tests__/libraryImport.store.spec.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createPinia, setActivePinia } from 'pinia' +import type { SearchResult } from '@/types' const startManualImport = vi.fn() const addToLibrary = vi.fn() @@ -70,13 +71,14 @@ describe('library import store', () => { selectedMatch: { title: 'Ordered Book', authors: [], - } as any, + } as unknown as SearchResult, hasSearched: true, isSearching: false, selected: true, }, - } as any + } + store.action = 'move' await store.importSelected('D:\\library') expect(addToLibrary).toHaveBeenCalledTimes(1) @@ -161,7 +163,7 @@ describe('library import store', () => { isSearching: false, selected: false, }, - } as any + } store.startProcessing() await new Promise((resolve) => setTimeout(resolve, 0)) diff --git a/fe/src/components/feedback/ManualImportModal.vue b/fe/src/components/feedback/ManualImportModal.vue index 25086f9a..4f0dac42 100644 --- a/fe/src/components/feedback/ManualImportModal.vue +++ b/fe/src/components/feedback/ManualImportModal.vue @@ -69,14 +69,14 @@
-
{{ field.label }}
+
{{ field.label }}
-
+
{{ field.display(it) @@ -122,7 +122,7 @@ :disabled="selectedCount === 0 || loading" > - +