Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a0a6a3d
chore: migrate "add new studio" lookup and add flow to tanstack query…
Mothball7205 Jun 19, 2026
1a414a1
Chore: Bump postcss from 8.5.10 to 8.5.11 (#299)
dependabot[bot] Jun 19, 2026
c529a10
Chore: Bump ghcr.io/devcontainers/features/node from 1.7.1 to 2.1.0 (…
dependabot[bot] Jun 23, 2026
04f2b83
chore: bump swashbuckle (#321)
plz12345 Jul 12, 2026
0736790
Chore: Cloudflare cookie test expiring and reddening CI (#325)
plz12345 Jul 15, 2026
ae2248f
test: add MountCheck coverage
plz12345 Jul 15, 2026
c2e4645
fix: cache the mount list instead of enumerating it per lookup
plz12345 Jul 15, 2026
7ec3325
fix: stop re-querying root folders once per movie
plz12345 Jul 15, 2026
09f0744
perf: walk the path once per mount lookup, not once per mount
plz12345 Jul 15, 2026
dd1c29e
chore: address SonarQube findings in disk provider
plz12345 Jul 15, 2026
7a66a5f
fix: multi-brand studio token parsing for cross-posted scenes (#320)
v8eta Jul 16, 2026
3018588
Fixed: Movie file quality missing from the paged movie/scene index
plz12345 Jul 16, 2026
cfdf27d
Fixed: Unbounded pageSize on the paged movie endpoint
plz12345 Jul 16, 2026
154f63d
Fixed: Scene title replacement eating codec tokens from simpleRelease…
plz12345 Jul 16, 2026
1ef2ed2
fix: Check Health taking 24h+ on large libraries (#324)
plz12345 Jul 17, 2026
d027b4f
Fixed: Movie file quality missing from the paged movie/scene index (#…
plz12345 Jul 17, 2026
f11a731
Chore: Bump ws from 7.5.10 to 7.5.12 (#328)
dependabot[bot] Jul 18, 2026
954c64a
Chore: Bump qs from 6.15.1 to 6.15.2 (#292)
dependabot[bot] Jul 18, 2026
00d5522
Chore: Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7…
dependabot[bot] Jul 18, 2026
f8f0faf
Chore: Bump postcss from 8.5.11 to 8.5.12 (#318)
dependabot[bot] Jul 18, 2026
39be234
Chore: Bump @babel/core from 7.29.0 to 7.29.6 (#311)
dependabot[bot] Jul 18, 2026
874970f
Chore: Bump js-yaml from 4.1.1 to 4.3.0 (#319)
dependabot[bot] Jul 18, 2026
39b1db6
Chore: derive base release version from git tag instead of hardcoded …
plz12345 Jul 18, 2026
95f357d
chore: add agents.md (#331)
plz12345 Jul 18, 2026
4ca92df
chore: ignore ImageSharp major updates in Dependabot (#334)
plz12345 Jul 20, 2026
b482645
chore: Bump the all-nuget-updates group with 30 updates (#335)
dependabot[bot] Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "Whisparr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"ghcr.io/devcontainers/features/node:2": {
"nodeGypDependencies": true,
"version": "20",
"nvmVersion": "latest"
Expand Down
5 changes: 4 additions & 1 deletion .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ inputs:
version:
description: "Version number to build"
required: true
assembly_version:
description: "Assembly version (major.minor.patch.build) to stamp"
required: true
framework:
description: ".net framework used for the build"
required: true
Expand Down Expand Up @@ -38,7 +41,7 @@ runs:
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "WHISPARR_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV"
echo "BRANCH=${{ inputs.branch }}" >> "$GITHUB_ENV"
echo "WHISPARR_ASSEMBLY_VERSION=${{ env.WHISPARR_ASSEMBLY_VERSION }}" >> "$GITHUB_ENV"
echo "WHISPARR_ASSEMBLY_VERSION=${{ inputs.assembly_version }}" >> "$GITHUB_ENV"

if [ "$RUNNER_OS" == "Windows" ]; then
echo "NUGET_PACKAGES=D:\nuget\packages" >> "$GITHUB_ENV"
Expand Down
7 changes: 5 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ updates:
interval: "daily"
open-pull-requests-limit: 1
commit-message:
prefix: "Chore"
prefix: "chore"
groups:
all-npm-updates:
patterns:
Expand All @@ -24,8 +24,11 @@ updates:
interval: "daily"
open-pull-requests-limit: 1
commit-message:
prefix: "Chore"
prefix: "chore"
groups:
all-nuget-updates:
patterns:
- "*"
ignore:
- dependency-name: "SixLabors.ImageSharp"
update-types: ["version-update:semver-major"]
34 changes: 31 additions & 3 deletions .github/workflows/build_v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ env:
FRAMEWORK: net10.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
WHISPARR_MAJOR_VERSION: 3
VERSION: 3.3.4
WHISPARR_ASSEMBLY_VERSION: 3.3.4.${{ github.run_number }}
# Base version now comes from the newest base tag (vX.Y.Z) reachable from the
# building branch; this is only the fallback for the seed window before any
# base tag exists. See prepare job below.
VERSION_FALLBACK: 3.3.4

jobs:
prepare:
Expand All @@ -34,20 +36,45 @@ jobs:
framework: ${{ steps.variables.outputs.framework }}
major_version: ${{ steps.variables.outputs.major_version }}
version: ${{ steps.variables.outputs.version }}
assembly_version: ${{ steps.variables.outputs.assembly_version }}
branch: ${{ steps.variables.outputs.branch }}
steps:
- name: Check out
uses: actions/checkout@v5
with:
fetch-depth: 0 # fetches tags; needed to read the version marker

- name: Setup Environment Variables
id: variables
shell: bash
run: |
# Base version = highest base tag (strictly vX.Y.Z) reachable from THIS
# branch's history. --merged keeps it reachability-scoped so eros-develop
# and eros each resolve their own base; the strict regex rejects both the
# CI-minted -develop.N/-release.N tags and the legacy 4-part vX.Y.Z.B tags
# inherited from upstream (which glob-based matching cannot separate).
# `|| true` keeps the default bash `-eo pipefail` shell from aborting the
# step when no base tag matches yet (seed window) or when `head` closes the
# pipe early (SIGPIPE).
BASE_TAG=$(git tag --merged HEAD --sort=-v:refname 2>/dev/null \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
if [ -n "$BASE_TAG" ]; then
BASE="${BASE_TAG#v}"
echo "Base version ${BASE} (from tag ${BASE_TAG})"
else
BASE="${{ env.VERSION_FALLBACK }}"
echo "Base version ${BASE} (from VERSION_FALLBACK; no vX.Y.Z tag reachable)"
fi

if [[ "${{ github.ref_name }}" == "eros-develop" ]]; then
SEMVER_LABEL="develop"
else
SEMVER_LABEL="release"
fi
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.WHISPARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
echo "version=${{ env.VERSION }}-${SEMVER_LABEL}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "version=${BASE}-${SEMVER_LABEL}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "assembly_version=${BASE}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "branch=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_OUTPUT"

backend:
Expand Down Expand Up @@ -99,6 +126,7 @@ jobs:
with:
branch: ${{ needs.prepare.outputs.branch }}
version: ${{ needs.prepare.outputs.version }}
assembly_version: ${{ needs.prepare.outputs.assembly_version }}
framework: ${{ needs.prepare.outputs.framework }}
runtime: ${{ matrix.runtime }}
package_tests: ${{ matrix.package_tests }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ src/**/[Oo]bj/
.continue
.vscode
.claude
CLAUDE.md

# Build results
*_i.c
Expand Down
148 changes: 148 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Whisparr — repo map for agents & contributors

This file is an **architecture map**: where things live and where new code goes.
It deliberately does **not** restate conventions that are documented elsewhere —
see [Conventions live elsewhere](#conventions-live-elsewhere) for those.

`CLAUDE.md` is a symlink to this file.

## What this is

Whisparr is a Servarr-family PVR — an adult movie/scene collection manager — and
a **fork of Radarr**, which is why backend folders are named `NzbDrone.*`
(inherited Sonarr/Radarr lineage). It's a **C# .NET 10** backend plus a
**React 19 / Redux** frontend, bundled into a single application served over
ASP.NET Core with SignalR for live updates and SQLite/PostgreSQL (via Dapper)
for storage.

- Repo: `Whisparr/Whisparr-Eros`. Default / PR-target branch: **`eros-develop`**
(never `eros`).
- Issues for both `Whisparr/Whisparr` and `Whisparr/Whisparr-Eros` are tracked
on `Whisparr/Whisparr` — link fixes as `Fixes whisparr/whisparr#NNNN`.

## The naming quirk (read this first)

Folders under `src/` keep the legacy `NzbDrone.*` prefix, but the csproj,
assemblies, and (in newer projects) namespaces map to `Whisparr.*`. When you
search, the folder and the assembly name often differ:

| Folder (`src/…`) | Assembly / project | Responsibility |
| -------------------- | -------------------- | --------------------------------------- |
| `NzbDrone.Core` | `Whisparr.Core` | All business logic (~40 domain areas) |
| `Whisparr.Api.V3` | `Whisparr.Api.V3` | REST API — controllers + resource DTOs |
| `Whisparr.Http` | `Whisparr.Http` | HTTP framework, base controllers, REST/ |
| `NzbDrone.SignalR` | `Whisparr.SignalR` | Real-time push to the UI |
| `NzbDrone.Host` | `Whisparr.Host` | Bootstrap, startup, DI wiring |
| `NzbDrone.Common` | `Whisparr.Common` | Cross-cutting infra (DI, disk, http, …) |
| `NzbDrone` / `.Console` | `Whisparr` / `.Console` | Executable entry points |
| `NzbDrone.Mono` / `.Windows` | `Whisparr.Mono` / `.Windows` | Platform-specific impls |
| `NzbDrone.Update` | `Whisparr.Update` | Self-updater |

Test projects follow the same rule: `NzbDrone.Core.Test` →
`Whisparr.Core.Test`, etc.

## Top-level layout

- `src/` — **all** C# source and test projects. Solution: `src/Whisparr.sln`;
shared MSBuild config: `src/Directory.Build.props` / `.targets` (output paths,
warnings-as-errors, multi-RID).
- `frontend/` — React/Redux SPA; Webpack 5 + Babel + `ts-loader`
(`frontend/build/webpack.config.js`). Mixed JSX + TypeScript (migration in
progress), CSS Modules via PostCSS.
- `.github/workflows/` — CI (`build_v3.yml` main build, `sonarqube.yml`,
`deploy.yml`, `api_docs.yml`).
- `distribution/` — packaging assets (Debian, Docker). `schemas/`, `scripts/`,
`Logo/` — supporting assets.
- `_output/` / `_tests/` / `_temp/` — **build artifacts, not source.** `en.json`
and other files appear here as copies; the source-of-truth is under `src/`.

## Where things go (the cheat-sheet)

Each item names a representative path — follow the existing neighbors.

- **DB migration** → a `[Migration(N)]` class deriving `NzbDroneMigrationBase`,
overriding `MainDbUpgrade()`, in `src/NzbDrone.Core/Datastore/Migration/`
(FluentMigrator). File naming: `NNN_snake_case_description.cs`. Numbers are
**non-contiguous** — high 200s are inherited Radarr history, Whisparr-specific
ones sit in the 000–025 range; use the next free number. Migrations are
auto-discovered and run in order.
- **Command / event** → `src/NzbDrone.Core/Messaging/`. Commands derive
`Command`, executed by an `IExecute<TCommand>` handler; events implement
`IEvent`, handled by `IHandle<TEvent>` / `IHandleAsync<TEvent>` via
`EventAggregator`. No manual subscription.
- **DI registration** → usually **none needed.** DryIoc scans assemblies
(`WithNzbDroneRules()` + `AutoAddServices`) and auto-wires interfaces
(singleton) to implementations. Just define the interface + class. Entry
point: `src/NzbDrone.Host/Bootstrap.cs`.
- **New provider** (indexer / download client / notification / import list /
metadata source) → the **ThingiProvider** pattern in
`src/NzbDrone.Core/ThingiProvider/`. Add a subfolder, subclass the type's
`*Base<TSettings>`, and add a `*Settings` config-contract class. The factory
and provider controller expose it automatically. Canonical full example:
`src/NzbDrone.Core/Indexers/Newznab/`. Type roots: `Indexers/`,
`Download/` (+ `Download/Clients/`), `Notifications/`, `ImportLists/`,
`MetadataSource/`.
- **New API surface** → a `*Controller : RestControllerWithSignalR<TResource,
TModel>` under `src/Whisparr.Api.V3/`, decorated `[V3ApiController]`, with a
paired `*Resource` DTO carrying static `MapToResource` / `ToModel` mappers.
Base classes and routing live in `src/Whisparr.Http/REST/`. Controllers often
also implement `IHandle<…Event>` to push SignalR updates. `openapi.json` is
generated in this project (`docs.sh`).
- **Parser change** → `src/NzbDrone.Core/Parser/` (`Parser.cs`,
`QualityParser.cs`, `LanguageParser.cs`, `ReleaseGroupParser.cs`,
`ParsingService.cs`). **This is the most test-guarded area in the repo.** Any
change must add/adjust `[TestCase(...)]` rows in
`src/NzbDrone.Core.Test/ParserTests/` (`ParserFixture.cs`,
`ParseMovieTitleFixture.cs`, `QualityParserFixture.cs`, `StudioFixture.cs`,
…) covering the specific tokens touched. A parser change with no new parser
case is a defect, not a nit.
- **User-facing string** → add a key to
`src/NzbDrone.Core/Localization/Core/en.json` (flat, `PascalCase`,
alphabetical; `{placeholder}` interpolation). Backend:
`_localizationService.GetLocalizedString("Key")`. Frontend:
`translate('Key')`. Do **not** hand-edit other languages — Weblate owns them;
log-message translations aren't accepted.

## Tests

- NUnit, `[TestFixture]` classes named `*Fixture.cs`, in the sibling
`NzbDrone.*.Test` project mirroring the source tree.
- Base classes: `TestBase` / `TestBase<TSubject>`
(`src/NzbDrone.Test.Common/TestBase.cs`) → `CoreTest` / `CoreTest<TSubject>`
and `DbTest` (`src/NzbDrone.Core.Test/Framework/`). Auto-mocking `Mocker` /
`Subject`; Moq (AutoMoq) + FluentAssertions + NBuilder.
- **80% coverage on new backend code** (enforced on PRs).
- `NzbDrone.Integration.Test` and `NzbDrone.Automation.Test` (Selenium) are
separate projects / test categories.
- Targeted run (excluding heavy categories):
```
dotnet test src/NzbDrone.Core.Test/Whisparr.Core.Test.csproj \
--filter "FullyQualifiedName~ParserTests&TestCategory!=IntegrationTest&TestCategory!=AutomationTest&TestCategory!=ManualTest"
```

## Build & run

- Frontend: `yarn install`, then `yarn start` (watch) or `yarn build`. Node 20
+ Yarn via `corepack enable`.
- Backend: `dotnet msbuild -restore src/Whisparr.sln -p:Configuration=Debug -p:Platform=Posix -t:PublishAllRids`
(or `Platform=Windows`), or the orchestrator `build.sh`. Output in `_output/`;
app runs at `http://localhost:7878`.
- .NET SDK is pinned in `global.json` (10.0.x). API docs: `docs.sh`.

## Conventions live elsewhere

This map covers *structure*. The rules are already documented — don't duplicate,
consult:

- **`CONTRIBUTING.md`** — dev setup, branch/PR rules (PR only to `eros-develop`,
rebase not merge, meaningful feature-branch names, one fix per PR), commit
prefixes (`New:` / `Fix:` / `Chore:`, conventional `feat:`/`fix:`/… also
tolerated), testing, and the translation workflow.
- **`.github/PULL_REQUEST_TEMPLATE.md`** — required DB-migration note
(`YES - NNNN` | `NO`), Tests & Translation-Keys checkboxes, SFW screenshots,
`Fixes whisparr/whisparr#NNNN`.
- **`.editorconfig`** + **`src/stylecop.json`** — C# style (4-space indent,
`var` preferred, `_camelCase` private fields, `System.*` usings first, final
newline). **`eslint.config.js`** + **`frontend/.prettierrc.json`** — frontend.
- CI enforces the above (`build_v3.yml`, `sonarqube.yml`); `yarn lint --fix` /
`yarn stylelint-windows --fix` before committing frontend changes.
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Setup guides, [FAQ](Whisparr/Whisparr-Eros/wiki/FAQ), the more information we ha

Whisparr is written in C# (backend) and JS (frontend). The backend is built on the .NET10 framework, while the frontend utilizes Reactjs.

For a map of the repository layout and where new code belongs (backend project structure, migrations, providers, the parser, tests, and localization), see [AGENTS.md](AGENTS.md).

## Tools required

- Visual Studio 2022 or higher is recommended (<https://www.visualstudio.com/vs/>). The community version is free and works (<https://www.visualstudio.com/downloads/>).
Expand Down Expand Up @@ -95,6 +97,9 @@ dotnet msbuild -restore src/Whisparr.sln -p:Configuration=Debug -p:Platform=Posi
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2022 and WebStorm

> AI coding assistants are welcome as a tool, but you remain responsible for what you submit. Please understand and test any code before opening a PR. Submissions that appear to be unreviewed, machine-generated output — untested, broken, or unsafe — are unlikely to be merged. We've seen enough of these to treat them with caution, so make sure you can explain and stand behind your changes.
{.is-warning}

## Pull Requesting

- Only make pull requests to `eros-develop`, never `eros`, if you make a PR to `eros` we will comment on it and close it
Expand Down
Loading
Loading