From 367c2fdec68ff35dd6811997f4264620d012b885 Mon Sep 17 00:00:00 2001 From: enoch85 Date: Wed, 1 Jul 2026 22:23:55 +0000 Subject: [PATCH 1/2] chore: ban em/en dashes and convert existing usages to hyphens Replaces every em dash (U+2014) and en dash (U+2013) with a plain ASCII hyphen across TypeScript/TSX and CSS source (comments, log and UI strings, tests), and adds the convention to AGENTS.md and project-notes.instructions.md so new code keeps to a single ASCII dash. Purely mechanical in source; no behavior change. Markdown prose was intentionally left out of scope. --- .../project-notes.instructions.md | 7 +++ AGENTS.md | 1 + .../migration-tests/migrations.spec.ts | 12 ++--- .../migration-tests/upgrade-from-1x.spec.ts | 2 +- ...622081794-NormalizeRuleSectionOperators.ts | 6 +-- apps/server/src/main.ts | 4 +- .../actions/servarr-tag.service.spec.ts | 16 +++--- .../modules/actions/servarr-tag.service.ts | 18 +++---- .../actions/sonarr-action-handler.spec.ts | 12 ++--- .../modules/actions/sonarr-action-handler.ts | 8 +-- .../download-client-api.controller.ts | 2 +- .../download-client-api.service.spec.ts | 2 +- .../download-client-api.service.ts | 10 ++-- .../download-client.interface.ts | 6 +-- .../helpers/qbittorrent.helper.ts | 6 +-- .../external-api/external-api.service.spec.ts | 6 +-- apps/server/src/modules/api/lib/cache.ts | 4 +- apps/server/src/modules/api/lib/httpRetry.ts | 4 +- .../src/modules/api/lib/plextvApi.spec.ts | 2 +- apps/server/src/modules/api/lib/plextvApi.ts | 2 +- .../media-server/emby/emby-adapter.service.ts | 26 +++++----- .../api/media-server/emby/emby.mapper.spec.ts | 2 +- .../api/media-server/emby/emby.mapper.ts | 2 +- .../jellyfin/jellyfin-adapter.service.spec.ts | 4 +- .../jellyfin/jellyfin-adapter.service.ts | 34 ++++++------- .../jellyfin/jellyfin.constants.ts | 2 +- .../media-server-id.utils.spec.ts | 2 +- .../api/media-server/media-server-id.utils.ts | 4 +- .../api/media-server/media-server.factory.ts | 2 +- .../media-server/media-server.interface.ts | 12 ++--- .../plex/plex-adapter.service.spec.ts | 2 +- .../media-server/plex/plex-adapter.service.ts | 4 +- .../api/plex-api/plex-api.constants.ts | 2 +- .../api/plex-api/plex-api.service.spec.ts | 4 +- .../modules/api/plex-api/plex-api.service.ts | 32 ++++++------ .../api/seerr-api/seerr-api.service.spec.ts | 2 +- .../api/seerr-api/seerr-api.service.ts | 10 ++-- .../servarr-api/common/servarr-api.service.ts | 4 +- .../servarr-api/helpers/radarr.helper.spec.ts | 2 +- .../api/servarr-api/helpers/radarr.helper.ts | 8 +-- .../servarr-api/helpers/sonarr.helper.spec.ts | 2 +- .../api/servarr-api/helpers/sonarr.helper.ts | 4 +- .../interfaces/servarr.interface.ts | 2 +- .../streamystats-api.service.spec.ts | 2 +- .../streamystats-api.service.ts | 4 +- .../collections/collection-handler.spec.ts | 2 +- .../modules/collections/collection-handler.ts | 14 +++--- .../collections/collection-poster.service.ts | 14 +++--- .../collection-worker.server.spec.ts | 2 +- .../collections/collection-worker.service.ts | 4 +- .../collections/collections.service.spec.ts | 6 +-- .../collections/collections.service.ts | 50 +++++++++---------- .../entities/collection.entities.ts | 2 +- .../recently-handled-media.service.ts | 4 +- .../modules/collections/sort-utils.spec.ts | 6 +-- .../modules/metadata/metadata.service.spec.ts | 6 +-- .../src/modules/metadata/metadata.service.ts | 18 +++---- .../providers/tvdb-metadata.provider.spec.ts | 6 +-- .../notifications/agents/webhookUrl.ts | 4 +- .../notifications/notifications.service.ts | 6 +-- .../overlay-processor.service.spec.ts | 8 +-- .../overlays/overlay-processor.service.ts | 6 +-- .../overlays/overlay-render.service.ts | 2 +- .../modules/overlays/overlay-task.service.ts | 2 +- .../providers/emby-overlay.provider.ts | 2 +- .../jellyfin-overlay.provider.spec.ts | 2 +- .../providers/jellyfin-overlay.provider.ts | 4 +- .../providers/overlay-provider.factory.ts | 2 +- .../providers/overlay-provider.interface.ts | 8 +-- .../providers/overlay-provider.module.ts | 2 +- .../providers/plex-overlay.provider.ts | 2 +- .../rules/constants/constants.service.ts | 4 +- .../rules/constants/rules.constants.ts | 6 +-- .../rules/getter/emby-getter.service.ts | 8 +-- .../getter/getter-property-coverage.spec.ts | 6 +-- .../modules/rules/getter/getter.service.ts | 2 +- .../rules/getter/jellyfin-getter.service.ts | 4 +- .../rules/getter/plex-getter.service.spec.ts | 4 +- .../rules/getter/plex-getter.service.ts | 8 +-- .../rules/getter/radarr-getter.service.ts | 2 +- .../rules/getter/seerr-getter.service.spec.ts | 4 +- .../rules/getter/seerr-getter.service.ts | 8 +-- .../getter/sonarr-getter.service.spec.ts | 4 +- .../rules/getter/sonarr-getter.service.ts | 6 +-- .../streamystats-getter.service.spec.ts | 8 +-- .../getter/streamystats-getter.service.ts | 10 ++-- .../rules/getter/tautulli-getter.service.ts | 2 +- .../modules/rules/helpers/arr-lookup-cache.ts | 6 +-- .../rules/helpers/exclusion-cascade.helper.ts | 2 +- .../rules/helpers/rule-property.helper.ts | 4 +- .../rules/helpers/rule.comparator.service.ts | 8 +-- .../rules/helpers/yaml.service.spec.ts | 2 +- .../rules.service.deleteRuleGroup.spec.ts | 2 +- ...vice.exclusion-scoping.integration.spec.ts | 8 +-- .../rules/rules.service.exclusions.spec.ts | 6 +-- .../server/src/modules/rules/rules.service.ts | 16 +++--- .../tasks/exclusion-corrector.service.ts | 2 +- .../rule-executor-job-manager.service.ts | 4 +- .../rules/tasks/rule-executor.service.spec.ts | 6 +-- .../rules/tasks/rule-executor.service.ts | 14 +++--- ...rator.service.executeRulesWithData.spec.ts | 8 +-- .../src/modules/settings/dto's/setting.dto.ts | 2 +- .../settings/media-server-switch.service.ts | 2 +- .../settings/rule-migration.service.spec.ts | 2 +- .../settings/rule-migration.service.ts | 12 ++--- .../settings/settings-operations.service.ts | 6 +-- .../storage-metrics.controller.ts | 2 +- .../storage-metrics.service.ts | 6 +-- .../modules/tasks/execution-lock.service.ts | 2 +- apps/server/src/utils/secretMasking.ts | 2 +- apps/server/src/utils/sharp.ts | 4 +- apps/server/test/rules-test-matrix.e2e.ts | 2 +- apps/ui/src/api/settings.ts | 2 +- .../ui/src/components/AddModal/index.spec.tsx | 6 +-- apps/ui/src/components/AddModal/index.tsx | 6 +-- .../StreamystatsStatsPanel/index.tsx | 4 +- .../MediaCard/maintainerrStatus.spec.ts | 4 +- .../components/Common/Poster/PosterCard.tsx | 2 +- .../components/Login/Emby/EmbyLoginButton.tsx | 4 +- .../OverlayEditor/OverlayCanvas.spec.tsx | 4 +- .../OverlayEditor/OverlayCanvas.tsx | 6 +-- .../OverlayEditor/PropertiesPanel.tsx | 4 +- .../OverlayEditor/ResourceField.tsx | 4 +- .../Rule/RuleCreator/RuleInput/index.tsx | 2 +- .../AddModal/CollectionPosterPicker.spec.tsx | 4 +- .../AddModal/CollectionPosterPicker.tsx | 6 +-- .../Rules/RuleGroup/AddModal/index.spec.tsx | 2 +- .../Rules/RuleGroup/AddModal/index.tsx | 12 ++--- .../Settings/DownloadClient/index.tsx | 2 +- .../components/Settings/Overlays/index.tsx | 2 +- .../components/Settings/Plex/index.spec.tsx | 10 ++-- .../ui/src/components/Settings/Plex/index.tsx | 10 ++-- .../Servarr/ExclusionTagSettings.spec.tsx | 2 +- .../Settings/Servarr/ExclusionTagSettings.tsx | 2 +- .../StorageMetrics/StorageUsageBar.tsx | 2 +- .../components/StorageMetrics/index.spec.tsx | 2 +- .../src/components/StorageMetrics/index.tsx | 10 ++-- apps/ui/src/hooks/useLockBodyScroll.spec.tsx | 4 +- apps/ui/src/hooks/useLockBodyScroll.ts | 2 +- .../src/pages/OverlayTemplateEditorPage.tsx | 22 ++++---- apps/ui/src/router.tsx | 6 +-- apps/ui/src/utils/PlexAuth.ts | 2 +- apps/ui/src/utils/formatBytes.ts | 2 +- apps/ui/styles/globals.css | 6 +-- .../contracts/src/media-server/features.ts | 2 +- .../contracts/src/media-server/sort-utils.ts | 6 +-- .../contracts/src/overlays/overlay-element.ts | 6 +-- .../src/overlays/overlay-provider-dtos.ts | 2 +- .../src/overlays/overlay-template.ts | 10 ++-- .../download-client/downloadClientSetting.ts | 2 +- .../contracts/src/settings/servarr/arrTag.ts | 4 +- .../contracts/src/streamystats/watchlists.ts | 2 +- packages/contracts/src/uploads/index.ts | 4 +- 153 files changed, 454 insertions(+), 446 deletions(-) diff --git a/.github/instructions/project-notes.instructions.md b/.github/instructions/project-notes.instructions.md index c55538f95..6f9c8ae9a 100644 --- a/.github/instructions/project-notes.instructions.md +++ b/.github/instructions/project-notes.instructions.md @@ -41,6 +41,13 @@ conversational chat. rather than `.replace(/\/+$/, '')`. Rationale: explicit, no hidden engine cost, no surprises on empty input or unicode. +- **Never use em or en dashes; always use a plain hyphen `-`.** This applies to + every committed artifact: code, comments, log and UI strings, tests, commit + messages, and docs. Do not paste `—` (U+2014) or `–` (U+2013); type `-` + (U+002D). Disable any "smart dash" autocorrect in your editor. Rationale: + a single ASCII dash keeps logs, diffs, and greps consistent and copy-paste + safe. To find stragglers: `git ls-files | xargs grep -lP '[\x{2013}\x{2014}]'`. + - **Prefer the codebase's existing idiom over adding a dependency.** When a feature request or issue names a specific library (e.g. `p-limit` for bounded concurrency), check first whether the repo already solves it. It does for diff --git a/AGENTS.md b/AGENTS.md index 62c66c2e2..3e83e8398 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -215,6 +215,7 @@ yarn workspace @maintainerr/contracts build - `BREAKING CHANGE` footer or `!` (e.g. `feat!:`) → major release - **Import Organization**: Prefer absolute imports, group by type (external, internal, relative) - **String Handling**: Avoid regex for simple prefix/suffix/substring checks or single-character trimming. Prefer string primitives such as `endsWith`, `startsWith`, `slice`, `substring`, or direct character inspection to reduce unnecessary regex risk; see [OWASP ReDoS guidance](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS). Example: `fix: avoid regex backtracking in disk path normalization (#2526)`. +- **Punctuation**: Never use em or en dashes in any committed artifact (code, comments, log/UI strings, tests, commit messages, docs). Always type a plain ASCII hyphen `-` (U+002D), never `—` (U+2014) or `–` (U+2013). See the convention in [project-notes.instructions.md](.github/instructions/project-notes.instructions.md). ### TypeScript Guidelines diff --git a/apps/server/src/database/migration-tests/migrations.spec.ts b/apps/server/src/database/migration-tests/migrations.spec.ts index 6fa55594e..7bf4dc455 100644 --- a/apps/server/src/database/migration-tests/migrations.spec.ts +++ b/apps/server/src/database/migration-tests/migrations.spec.ts @@ -2,20 +2,20 @@ import * as fs from 'fs'; import * as path from 'path'; import { DataSource, MigrationInterface } from 'typeorm'; -// Generic migration matrix — we don't test each migration individually. These +// Generic migration matrix - we don't test each migration individually. These // confirm TypeORM behaves and that migrations comply with typeorm_instructions.txt // (generated from the entities, never hand-waived): -// 1. The whole chain applies in order on a fresh DB, each recorded once — +// 1. The whole chain applies in order on a fresh DB, each recorded once - // proving every migration is structurally valid SQL that TypeORM accepts. // 2. The schema migration this PR adds reproduces its entity columns EXACTLY // (type + NOT NULL + default). A hand-edited migration that drifts from the -// entity definition fails here — the in-jest stand-in for `migration:generate` +// entity definition fails here - the in-jest stand-in for `migration:generate` // reporting "No changes". (The repo-wide entity-vs-schema diff stays a manual // release step: TypeORM's metadata builder can't run under @swc/jest, which // reflects the codebase's `T | null` columns as `Object` and rejects the // build. A new migration adds its columns to test 2.) // 3. That migration's up() carries TypeORM's SQLite create-temporary-table -// rebuild — the fingerprint of `migration:generate`. Matching columns (2) +// rebuild - the fingerprint of `migration:generate`. Matching columns (2) // can be reproduced by a hand-written `ALTER TABLE ADD COLUMN`; the rebuild // cannot, so its absence flags a hand-waived migration. // 4. The newest migration's down() is symmetric. @@ -116,7 +116,7 @@ describe('database migrations', () => { const src = fs.readFileSync(path.join(MIGRATIONS_DIR, newest.file), 'utf8'); // SQLite can't ALTER most columns in place, so `migration:generate` always // emits a full create-temporary-table / copy / drop / rename rebuild for the - // changed tables. A hand-written ALTER shortcut lacks it — this is the + // changed tables. A hand-written ALTER shortcut lacks it - this is the // cheapest signal the migration was generated rather than authored. expect(src).toContain('CREATE TABLE "temporary_collection"'); expect(src).toContain('CREATE TABLE "temporary_settings"'); @@ -124,7 +124,7 @@ describe('database migrations', () => { // We don't revert the whole chain: several pre-existing migrations have // non-reversible down() paths (production only ever migrates up). We do confirm - // the newest migration's down() is symmetric — the regression this catches when + // the newest migration's down() is symmetric - the regression this catches when // a migration is added. it('revert the newest migration cleanly (symmetric down)', async () => { const ds = await makeDS(all.map((m) => m.cls)).initialize(); diff --git a/apps/server/src/database/migration-tests/upgrade-from-1x.spec.ts b/apps/server/src/database/migration-tests/upgrade-from-1x.spec.ts index 58b7b6397..45b146741 100644 --- a/apps/server/src/database/migration-tests/upgrade-from-1x.spec.ts +++ b/apps/server/src/database/migration-tests/upgrade-from-1x.spec.ts @@ -83,7 +83,7 @@ describe('upgrade from v1.7.1 to current', () => { await insertRule(4, 1, '1'); // explicit operator -> untouched await ds.destroy(); - // --- Phase 3: upgrade — register ALL migrations, run the pending set --- + // --- Phase 3: upgrade - register ALL migrations, run the pending set --- ds = makeDS(all.map((m) => m.cls)); await ds.initialize(); const phase3 = await ds.runMigrations(); diff --git a/apps/server/src/database/migrations/1779622081794-NormalizeRuleSectionOperators.ts b/apps/server/src/database/migrations/1779622081794-NormalizeRuleSectionOperators.ts index c6a1ce34f..95a931c8c 100644 --- a/apps/server/src/database/migrations/1779622081794-NormalizeRuleSectionOperators.ts +++ b/apps/server/src/database/migrations/1779622081794-NormalizeRuleSectionOperators.ts @@ -12,8 +12,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * To change behaviour as little as possible, this migration writes back the * value each rule *already evaluates as today*, so no existing rule changes * how it matches: - * - first rule of a later section -> "0" (AND) — the section-combine default - * - any other (within-section) rule -> "1" (OR) — the within-section default + * - first rule of a later section -> "0" (AND) - the section-combine default + * - any other (within-section) rule -> "1" (OR) - the within-section default * - the first rule of a group keeps null; its operator is forced to null at * runtime regardless, so it is left untouched. * Making these explicit also stops the engine from re-inferring the value and @@ -21,7 +21,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * * Rules are ordered by id, which is how they are (re)written on every save and * how the comparator iterates them, so the first id seen for a new section is - * that section's first rule — the same boundary the comparator uses. + * that section's first rule - the same boundary the comparator uses. * * Operators are written as strings ("0"/"1") to match the values the UI * persists; the comparator coerces with `+`, so string and numeric operators diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 5d1dc0086..f3483128f 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -11,8 +11,8 @@ import { MaintainerrLogger } from './modules/logging/logs.service'; import { installStdioPipeGuards } from './modules/logging/winston/stdioPipeGuard'; import { isSharpAvailable, SHARP_UNAVAILABLE_MESSAGE } from './utils/sharp'; -// Pre-bootstrap guard so the console.warn/console.error calls below — and any -// other write before LogsModule loads — cannot crash the process on a broken +// Pre-bootstrap guard so the console.warn/console.error calls below - and any +// other write before LogsModule loads - cannot crash the process on a broken // stdio pipe. The logging module re-installs these (idempotent) for // defence-in-depth. installStdioPipeGuards(); diff --git a/apps/server/src/modules/actions/servarr-tag.service.spec.ts b/apps/server/src/modules/actions/servarr-tag.service.spec.ts index 297beaaf8..3d2d77db4 100644 --- a/apps/server/src/modules/actions/servarr-tag.service.spec.ts +++ b/apps/server/src/modules/actions/servarr-tag.service.spec.ts @@ -31,7 +31,7 @@ describe('ServarrTagService', () => { // By default every media-server id resolves to a tmdb/tvdb candidate; the // *arr lookup (mocked per test) decides whether it matches an entity. The - // candidate id is irrelevant here — the lookup mock ignores it. + // candidate id is irrelevant here - the lookup mock ignores it. metadataService.resolveLookupCandidatesForService.mockImplementation( async (_mediaServerId, service) => [ { providerKey: service === 'radarr' ? 'tmdb' : 'tvdb', id: 100 }, @@ -39,7 +39,7 @@ describe('ServarrTagService', () => { ); }); - describe('Behavior A — membership tagging', () => { + describe('Behavior A - membership tagging', () => { it('tags newly added movies in Radarr with the collection title', async () => { const radarr = mockRadarrApi(servarrService, logger); jest @@ -71,7 +71,7 @@ describe('ServarrTagService', () => { ); }); - it('uses the current (renamed) group name as the tag — no stale old-label removal', async () => { + it('uses the current (renamed) group name as the tag - no stale old-label removal', async () => { const radarr = mockRadarrApi(servarrService, logger); jest .spyOn(radarr, 'getMovieByTmdbId') @@ -92,7 +92,7 @@ describe('ServarrTagService', () => { ); // Only the current name is ensured/applied; the old (renamed-from) label is - // intentionally not chased here (documented edge case — re-tagged on churn). + // intentionally not chased here (documented edge case - re-tagged on churn). expect(radarr.ensureTag).toHaveBeenCalledTimes(1); expect(radarr.ensureTag).toHaveBeenCalledWith('renamed-group'); expect(radarr.setMovieTags).toHaveBeenCalledWith([10], 5, 'add'); @@ -135,7 +135,7 @@ describe('ServarrTagService', () => { [], ); - // Both titles normalize to 'my-group', so both resolve to the same tag id — + // Both titles normalize to 'my-group', so both resolve to the same tag id - // an untag from one then a re-add from the other converges on the same tag. expect(ensureTag.mock.calls.every((c) => c[0] === 'my-group')).toBe(true); expect(radarr.setMovieTags).toHaveBeenCalledWith([10], 5, 'add'); @@ -249,7 +249,7 @@ describe('ServarrTagService', () => { expect(radarr.setMovieTags).not.toHaveBeenCalled(); }); - it('skips untaggable types (season/episode) — Sonarr has no per-season tag', async () => { + it('skips untaggable types (season/episode) - Sonarr has no per-season tag', async () => { const sonarr = mockSonarrApi(servarrService, logger); const collection = createCollection({ type: 'season', @@ -380,7 +380,7 @@ describe('ServarrTagService', () => { await service.syncMembershipTags(collection, added, []); - // Every distinct item is tagged exactly once — nothing dropped or doubled. + // Every distinct item is tagged exactly once - nothing dropped or doubled. const addCalls = setMovieTags.mock.calls.filter((c) => c[2] === 'add'); const taggedIds = addCalls.flatMap((c) => c[0]); expect(taggedIds).toHaveLength(total); @@ -395,7 +395,7 @@ describe('ServarrTagService', () => { }, 30_000); }); - describe('Behavior B — exclusion tagging', () => { + describe('Behavior B - exclusion tagging', () => { const movieTarget = { mediaServerId: 'movie-1', type: 'movie' as const }; it('does nothing when exclusion tagging is disabled', async () => { diff --git a/apps/server/src/modules/actions/servarr-tag.service.ts b/apps/server/src/modules/actions/servarr-tag.service.ts index 6a7196178..e1925e25c 100644 --- a/apps/server/src/modules/actions/servarr-tag.service.ts +++ b/apps/server/src/modules/actions/servarr-tag.service.ts @@ -39,7 +39,7 @@ const RESOLVE_CONCURRENCY = 5; const EDITOR_BATCH_SIZE = 100; /** - * Applies/removes Radarr & Sonarr tags as a side effect of Maintainerr state — + * Applies/removes Radarr & Sonarr tags as a side effect of Maintainerr state - * NOT an action slot. Two triggers share this plumbing: * * - **Membership (Behavior A):** while a `tagInArr` collection holds an item, the @@ -55,7 +55,7 @@ const EDITOR_BATCH_SIZE = 100; * contract (undefined = transient → skip & retry; null = confirmed-not-tracked → * nothing to do). A transient blip therefore never strips a tag. * - * v1 is gated to movie (Radarr) and show (Sonarr) — Sonarr has no per-season tag, + * v1 is gated to movie (Radarr) and show (Sonarr) - Sonarr has no per-season tag, * so season/episode collections are skipped with a debug log. * * Known edge cases (v1 behaviour): @@ -83,7 +83,7 @@ export class ServarrTagService { } /** - * Behavior A — reconcile *arr tags for the items that just entered/left a + * Behavior A - reconcile *arr tags for the items that just entered/left a * collection this run. `added`/`removed` are the executor's rule-scope deltas * (manual / co-owned members are already excluded from `removed`), each * carrying the item's cached provider ids. The tag label is the collection @@ -199,7 +199,7 @@ export class ServarrTagService { }; } - /** True if either *arr has exclusion tagging on — lets callers skip the + /** True if either *arr has exclusion tagging on - lets callers skip the * collection lookup entirely when both are off. */ public anyExclusionTaggingEnabled(): boolean { return ( @@ -234,7 +234,7 @@ export class ServarrTagService { } /** - * Behavior B — apply the protective exclusion tag to the excluded item's *arr + * Behavior B - apply the protective exclusion tag to the excluded item's *arr * entity. No-ops unless exclusion tagging is enabled. The caller resolves the * instance: a collection-scoped exclusion uses its rule group's collection, a * global one the single configured instance (skipped when ambiguous). Adding on @@ -251,10 +251,10 @@ export class ServarrTagService { } /** - * Behavior B — remove the protective exclusion tag on un-exclude. This is + * Behavior B - remove the protective exclusion tag on un-exclude. This is * conservative on purpose (Zipties' "second dnd source" pain): it only runs * when the user opts in via `_untag_on_unexclude`, and even then only - * ever touches the configured label — never the user's other tags. With the + * ever touches the configured label - never the user's other tags. With the * default (opt-in OFF) a manually-set "dnd" is never stripped by Maintainerr. */ public async removeExclusionTag( @@ -318,7 +318,7 @@ export class ServarrTagService { const arrId = await this.resolveArrId(client, service, target); if (arrId == null) { // undefined = transient (retried on the next exclude/un-exclude), - // null = the item isn't tracked in *arr — nothing to tag either way. + // null = the item isn't tracked in *arr - nothing to tag either way. return; } @@ -401,7 +401,7 @@ export class ServarrTagService { * - null when the item is confirmed not tracked in *arr (no candidates, or * *arr returned an empty match), * - undefined when the lookup transiently failed (so callers skip & retry, - * never untagging on a blip) — per the #3125 getter contract. + * never untagging on a blip) - per the #3125 getter contract. * The item's cached tmdb/tvdb are passed as resolution fallbacks. */ private async resolveArrId( diff --git a/apps/server/src/modules/actions/sonarr-action-handler.spec.ts b/apps/server/src/modules/actions/sonarr-action-handler.spec.ts index 99d5dc35d..fbd134ba5 100644 --- a/apps/server/src/modules/actions/sonarr-action-handler.spec.ts +++ b/apps/server/src/modules/actions/sonarr-action-handler.spec.ts @@ -244,7 +244,7 @@ describe('SonarrActionHandler', () => { ); // A transient Sonarr lookup failure (getSeriesByTvdbId → undefined) must NOT - // be read as "not in Sonarr" and trigger a media-server delete — fail closed + // be read as "not in Sonarr" and trigger a media-server delete - fail closed // so the item stays in the collection and is retried next run. (#3125) it('fails closed (no delete, no action) when the Sonarr lookup fails transiently for a DELETE action', async () => { const collection = createCollection({ @@ -366,7 +366,7 @@ describe('SonarrActionHandler', () => { // empty-show cleanup must resolve the series from the (uncached) Sonarr // client on every run, never from a memo that could hold a pre-deletion // snapshot. The rule-evaluation memo is intentionally never threaded into - // this path — if a future refactor did so, the second run below would re-use + // this path - if a future refactor did so, the second run below would re-use // the stale "still has files" series and wrongly skip the deletion. it('resolves the series fresh from the client each run, never via a memo', async () => { const collection = createCollection({ @@ -617,7 +617,7 @@ describe('SonarrActionHandler', () => { // Regression for issues #2757 / #2891. Sonarr carries every TVDB season on // the series, including ones the user never downloaded; those stay // monitored forever. The no-Seerr fallback must not require every season to - // be unmonitored — an ended show with zero episode files is deletable even + // be unmonitored - an ended show with zero episode files is deletable even // when later (never-downloaded) seasons are still monitored. it('should delete ended show with no episode files when Seerr is not configured even if later seasons remain monitored', async () => { const collection = createCollection({ @@ -807,7 +807,7 @@ describe('SonarrActionHandler', () => { }); // A season that is still monitored AND holds files means the user is not - // done with the show — it must not be unmonitored. + // done with the show - it must not be unmonitored. it('should not unmonitor show when a monitored season still has files', async () => { const collection = createCollection({ arrAction: ServarrAction.UNMONITOR_SHOW_IF_EMPTY, @@ -851,7 +851,7 @@ describe('SonarrActionHandler', () => { }); // season.statistics is optional in Sonarr's response. A monitored season - // with no statistics has an unknown file count — it must be treated as + // with no statistics has an unknown file count - it must be treated as // still having content, never assumed empty. it('should not unmonitor show when a monitored season has no statistics', async () => { const collection = createCollection({ @@ -1757,7 +1757,7 @@ describe('SonarrActionHandler', () => { await sonarrActionHandler.handleAction(collection, collectionMedia); - // Coverage could not be proven, so nothing is removed — but the delete + // Coverage could not be proven, so nothing is removed - but the delete // itself still runs (cleanup is best-effort). expect(mockedSonarrApi.unmonitorSeasons).toHaveBeenCalled(); expect(downloadClient.removeDownloads).toHaveBeenCalledWith([]); diff --git a/apps/server/src/modules/actions/sonarr-action-handler.ts b/apps/server/src/modules/actions/sonarr-action-handler.ts index c814d1322..a0efc9b1f 100644 --- a/apps/server/src/modules/actions/sonarr-action-handler.ts +++ b/apps/server/src/modules/actions/sonarr-action-handler.ts @@ -437,7 +437,7 @@ export class SonarrActionHandler { /** * The torrents a season/episode delete fully covers: those whose every backed * episode is in the deleted set. A season/series pack also backs episodes that - * are kept, so it is excluded. Keyed on Sonarr's episodeId — this is the only + * are kept, so it is excluded. Keyed on Sonarr's episodeId - this is the only * safeguard for a lone pack, since removeDownloads' cross-seed guard protects * torrents that share a content path, not one torrent backing several wanted * episodes. Fails closed: returns [] whenever coverage cannot be proven. @@ -632,7 +632,7 @@ export class SonarrActionHandler { // episode files; `ended` confirms no further episodes are coming. We do // NOT additionally require every season to be unmonitored: Sonarr carries // every TVDB season on the series, including ones the user never - // downloaded, and those stay monitored forever — which would block + // downloaded, and those stay monitored forever - which would block // deletion of a genuinely empty, ended show indefinitely (issue #2757 / // #2891: e.g. a show where the user only ever had seasons 1-4). if (series.status !== 'ended') { @@ -705,13 +705,13 @@ export class SonarrActionHandler { // 'monitored': the show counts as empty once no season is still both // monitored AND holding files. Seasons the user never downloaded stay // monitored on the series object indefinitely (Sonarr carries every TVDB - // season) and have zero files — they must not count as monitored content, + // season) and have zero files - they must not count as monitored content, // or a genuinely finished show could never be unmonitored (#2757 / #2891). // // A monitored season is only treated as empty when Sonarr *explicitly* // reports zero files. season.statistics is optional; if it's absent the // file count is unknown, so the season is treated as still having content - // (conservative — never unmonitor a show on an assumption). + // (conservative - never unmonitor a show on an assumption). return series.seasons.every( (season) => !season.monitored || season.statistics?.episodeFileCount === 0, diff --git a/apps/server/src/modules/api/download-client-api/download-client-api.controller.ts b/apps/server/src/modules/api/download-client-api/download-client-api.controller.ts index 1ed3a4ef2..deea079af 100644 --- a/apps/server/src/modules/api/download-client-api/download-client-api.controller.ts +++ b/apps/server/src/modules/api/download-client-api/download-client-api.controller.ts @@ -1,6 +1,6 @@ import { Controller } from '@nestjs/common'; -// The download client has no direct HTTP surface of its own — it is consumed +// The download client has no direct HTTP surface of its own - it is consumed // internally by the action handlers and configured/tested via the settings // controller. @Controller('api/download-client') diff --git a/apps/server/src/modules/api/download-client-api/download-client-api.service.spec.ts b/apps/server/src/modules/api/download-client-api/download-client-api.service.spec.ts index 3798d8c1d..c8cbc7624 100644 --- a/apps/server/src/modules/api/download-client-api/download-client-api.service.spec.ts +++ b/apps/server/src/modules/api/download-client-api/download-client-api.service.spec.ts @@ -12,7 +12,7 @@ const apiMock = { }; // The service builds its client through the factory, which constructs a -// QbittorrentApi — mock that so the factory returns our stub. +// QbittorrentApi - mock that so the factory returns our stub. jest.mock('./helpers/qbittorrent.helper', () => ({ QbittorrentApi: jest.fn().mockImplementation(() => apiMock), })); diff --git a/apps/server/src/modules/api/download-client-api/download-client-api.service.ts b/apps/server/src/modules/api/download-client-api/download-client-api.service.ts index e3c30f23f..ec0a2ba82 100644 --- a/apps/server/src/modules/api/download-client-api/download-client-api.service.ts +++ b/apps/server/src/modules/api/download-client-api/download-client-api.service.ts @@ -12,10 +12,10 @@ import { // blocks the caller. Bad credentials are NOT this case (they return HTTP 200 // "Fails." and are handled at login), so "Invalid API key" (the shared util's // 401/403 message) is misleading. The reliable fix is whitelisting Maintainerr's -// IP — it and qBittorrent commonly run on different (Docker) IPs — so lead with +// IP - it and qBittorrent commonly run on different (Docker) IPs - so lead with // that and only mention proxy/host validation as a secondary cause. const DOWNLOAD_CLIENT_FORBIDDEN_MESSAGE = - 'The download client accepted the login but returned 403 Forbidden — a ' + + 'The download client accepted the login but returned 403 Forbidden - a ' + 'qBittorrent Web UI security restriction, not a wrong username or password. ' + 'In qBittorrent → Options → Web UI → Security, add Maintainerr’s IP or ' + 'subnet to “Bypass authentication for clients in whitelisted IP subnets” ' + @@ -120,9 +120,9 @@ export class DownloadClientApiService { /** * Remove the completed downloads identified by the given download-client ids - * (Radarr/Sonarr `downloadId`s — for a torrent client these are infohashes). + * (Radarr/Sonarr `downloadId`s - for a torrent client these are infohashes). * No-op when no download client is configured. Best-effort: each download is - * handled independently and a failure never throws into the caller — the + * handled independently and a failure never throws into the caller - the * media has already been deleted from the *arr at this point, so this cleanup * is a side effect. * @@ -159,7 +159,7 @@ export class DownloadClientApiService { const torrent = await this.api.getTorrentByHash(hash); if (!torrent) { // Not in the download client (already removed, a different client, or - // a manual import that never had a download) — nothing to clean up. + // a manual import that never had a download) - nothing to clean up. continue; } diff --git a/apps/server/src/modules/api/download-client-api/download-client.interface.ts b/apps/server/src/modules/api/download-client-api/download-client.interface.ts index 77f20f399..539426c6b 100644 --- a/apps/server/src/modules/api/download-client-api/download-client.interface.ts +++ b/apps/server/src/modules/api/download-client-api/download-client.interface.ts @@ -21,9 +21,9 @@ export interface DownloadClientTorrent { /** * Whether the client's OWN seeding goal (its ratio / seed-time limit) is met, * decided entirely by the client: - * - `true` — goal reached, safe to remove - * - `false` — a limit exists but isn't reached yet, keep seeding - * - `null` — the client enforces no limit, so the caller applies its + * - `true` - goal reached, safe to remove + * - `false` - a limit exists but isn't reached yet, keep seeding + * - `null` - the client enforces no limit, so the caller applies its * fallback ratio instead */ reachedSeedingGoal: boolean | null; diff --git a/apps/server/src/modules/api/download-client-api/helpers/qbittorrent.helper.ts b/apps/server/src/modules/api/download-client-api/helpers/qbittorrent.helper.ts index 9064d72a8..a1b4e9352 100644 --- a/apps/server/src/modules/api/download-client-api/helpers/qbittorrent.helper.ts +++ b/apps/server/src/modules/api/download-client-api/helpers/qbittorrent.helper.ts @@ -57,7 +57,7 @@ const toDownloadClientTorrent = ( }; /** - * Thin client for the qBittorrent WebUI API (v2, qBittorrent 4.1+) — the + * Thin client for the qBittorrent WebUI API (v2, qBittorrent 4.1+) - the * qBittorrent implementation of the backend-agnostic `DownloadClient` contract. * * qBittorrent uses cookie/session auth: `POST /api/v2/auth/login` issues a `SID` @@ -86,7 +86,7 @@ export class QbittorrentApi // qBittorrent's WebUI wants a `Referer` matching the host (its login is the // only CSRF-exempt endpoint). Deliberately do NOT send `Origin`: qBittorrent // treats a request whose Origin doesn't match its own as cross-site and - // rejects it with 403 on every endpoint except login — which breaks + // rejects it with 403 on every endpoint except login - which breaks // reverse-proxy / scheme-mismatch setups (the mature qbittorrent-api client // sends Referer only, for the same reason). The SID cookie carries the auth. super(`${url}/api/v2`, {}, logger, { @@ -167,7 +167,7 @@ export class QbittorrentApi // On a normal login qBittorrent issues an SID cookie to use on subsequent // requests. When the WebUI bypasses authentication (e.g. "Bypass // authentication for clients on localhost"/whitelisted subnets) it returns - // "Ok." with no cookie — that is still a valid, authenticated session, so + // "Ok." with no cookie - that is still a valid, authenticated session, so // capture the cookie when present but never require it. const sid = this.extractSid(response.headers['set-cookie']); if (sid) { diff --git a/apps/server/src/modules/api/external-api/external-api.service.spec.ts b/apps/server/src/modules/api/external-api/external-api.service.spec.ts index 496e96a99..e8048678e 100644 --- a/apps/server/src/modules/api/external-api/external-api.service.spec.ts +++ b/apps/server/src/modules/api/external-api/external-api.service.spec.ts @@ -78,7 +78,7 @@ describe('ExternalApiService', () => { return { service, cache }; }; - it('does not cache Buffer responses — second call hits the network again', async () => { + it('does not cache Buffer responses - second call hits the network again', async () => { const { service } = createServiceWithCache(); const validObject = { data: 'ok' }; @@ -96,7 +96,7 @@ describe('ExternalApiService', () => { expect(getFn).toHaveBeenCalledTimes(2); }); - it('does not cache null responses — second call hits the network again', async () => { + it('does not cache null responses - second call hits the network again', async () => { const { service } = createServiceWithCache(); const validObject = { items: [] }; @@ -113,7 +113,7 @@ describe('ExternalApiService', () => { expect(getFn).toHaveBeenCalledTimes(2); }); - it('caches valid object responses — second call does not hit the network', async () => { + it('caches valid object responses - second call does not hit the network', async () => { const { service } = createServiceWithCache(); const validObject = { items: [1, 2, 3] }; diff --git a/apps/server/src/modules/api/lib/cache.ts b/apps/server/src/modules/api/lib/cache.ts index cb8c55500..8e6bc9c9f 100644 --- a/apps/server/src/modules/api/lib/cache.ts +++ b/apps/server/src/modules/api/lib/cache.ts @@ -81,7 +81,7 @@ class CacheManager { plexguid: new Cache('plexguid', 'Plex GUID', 'plexguid'), // Holds the leaf watch-history map built by PlexApiService.prefetchWatchHistory. // Persistent so the map survives flushAll() between rule groups in the same - // cron window; useClones is off because the value is a large Map — + // cron window; useClones is off because the value is a large Map - // getWatchHistory returns copies of the per-item arrays instead. plexwatchhistory: new Cache( 'plexwatchhistory', @@ -97,7 +97,7 @@ class CacheManager { seerr: new Cache('seerr', 'Seerr API', 'seerr'), // Holds the run-scoped request index built by SeerrApiService.getRequestsForMedia // (one bulk /request sweep grouped by tmdbId). useClones is off because the - // value is a Map — per-item reads copy the per-title array out. Unlike + // value is a Map - per-item reads copy the per-title array out. Unlike // plexwatchhistory this is NOT persistent: request data changes between runs, // so flushAll() at each rule-group start rebuilds it (freshness over reuse). // Long TTL so a single long run can't expire it mid-sweep. diff --git a/apps/server/src/modules/api/lib/httpRetry.ts b/apps/server/src/modules/api/lib/httpRetry.ts index e397bd8fc..c1f74b586 100644 --- a/apps/server/src/modules/api/lib/httpRetry.ts +++ b/apps/server/src/modules/api/lib/httpRetry.ts @@ -2,8 +2,8 @@ import { type AxiosInstance } from 'axios'; import axiosRetry from 'axios-retry'; /** - * Apply Maintainerr's standard transient-failure retry policy — 3 attempts - * with exponential backoff — to an Axios instance. One home for the policy so + * Apply Maintainerr's standard transient-failure retry policy - 3 attempts + * with exponential backoff - to an Axios instance. One home for the policy so * every outbound HTTP client (Plex, Emby, the Jellyfin SDK, external-api) * retries identically. */ diff --git a/apps/server/src/modules/api/lib/plextvApi.spec.ts b/apps/server/src/modules/api/lib/plextvApi.spec.ts index 7260ce68b..7ac3ef008 100644 --- a/apps/server/src/modules/api/lib/plextvApi.spec.ts +++ b/apps/server/src/modules/api/lib/plextvApi.spec.ts @@ -51,7 +51,7 @@ describe('PlexTvApi.validateToken', () => { }); // plex.tv returns 422 {"error":"Invalid token"} for a bad token on this - // endpoint (verified live) — not 401 — so 422 must count as invalid too. + // endpoint (verified live) - not 401 - so 422 must count as invalid too. it.each([422, 401, 403])( 'returns invalid when plex.tv answers %i', async (status) => { diff --git a/apps/server/src/modules/api/lib/plextvApi.ts b/apps/server/src/modules/api/lib/plextvApi.ts index 05f044d6c..48262cf11 100644 --- a/apps/server/src/modules/api/lib/plextvApi.ts +++ b/apps/server/src/modules/api/lib/plextvApi.ts @@ -185,7 +185,7 @@ export class PlexTvApi extends ExternalApiService { public async getDevices(clientIdentifier: string): Promise { try { // v2 /api/v2/resources returns owned servers that legacy v1 /api/resources - // omits. It requires X-Plex-Client-Identifier — use the same id the UI + // omits. It requires X-Plex-Client-Identifier - use the same id the UI // authenticates with so plex.tv sees a consistent client. const resources = await this.get( '/api/v2/resources?includeHttps=1', diff --git a/apps/server/src/modules/api/media-server/emby/emby-adapter.service.ts b/apps/server/src/modules/api/media-server/emby/emby-adapter.service.ts index 6ecb20c2c..7098d7320 100644 --- a/apps/server/src/modules/api/media-server/emby/emby-adapter.service.ts +++ b/apps/server/src/modules/api/media-server/emby/emby-adapter.service.ts @@ -89,7 +89,7 @@ export class EmbyAdapterService implements IMediaServerService { if (!url || !apiKey) { this.logger.debug( - 'Emby settings incomplete — skipping initialize (url or api_key missing)', + 'Emby settings incomplete - skipping initialize (url or api_key missing)', ); this.initialized = false; this.http = undefined; @@ -482,7 +482,7 @@ export class EmbyAdapterService implements IMediaServerService { /** * User IDs of every user with `IsFavorite=true` on this item. Mirrors - * `JellyfinAdapterService.getItemFavoritedBy` (per-user fan-out — Emby + * `JellyfinAdapterService.getItemFavoritedBy` (per-user fan-out - Emby * has no central favorites endpoint). */ async getItemFavoritedBy(itemId: string): Promise { @@ -497,7 +497,7 @@ export class EmbyAdapterService implements IMediaServerService { ); if (data.UserData?.IsFavorite) favoritedBy.push(user.id); } catch { - // user may lack visibility on this item — skip silently + // user may lack visibility on this item - skip silently } } return favoritedBy; @@ -541,7 +541,7 @@ export class EmbyAdapterService implements IMediaServerService { * Users who watched at least one episode under `parentId` (season or show). * Mirrors `JellyfinAdapterService.getDescendantEpisodeWatchers`. One * /Items request per user, each scoped to that user with `IsPlayed=true` - * + `Limit=1` — we only need to know whether any played episode exists. + * + `Limit=1` - we only need to know whether any played episode exists. */ async getDescendantEpisodeWatchers(parentId: string): Promise { if (!this.http) return []; @@ -608,7 +608,7 @@ export class EmbyAdapterService implements IMediaServerService { // documented path for Emby. if (!this.embyUserId) { this.logger.warn( - 'Emby getRecentlyAdded requires a configured user ID — none set', + 'Emby getRecentlyAdded requires a configured user ID - none set', ); return []; } @@ -728,7 +728,7 @@ export class EmbyAdapterService implements IMediaServerService { async prefetchWatchHistory(): Promise { // Emby has no central watch-history endpoint (history is per-user), so // there is nothing to bulk prefetch. Gated by - // supportsFeature(CENTRAL_WATCH_HISTORY) which is false for Emby — callers + // supportsFeature(CENTRAL_WATCH_HISTORY) which is false for Emby - callers // shouldn't reach here. throw new Error( 'Bulk watch-history prefetch is not supported on Emby (per-user history)', @@ -765,7 +765,7 @@ export class EmbyAdapterService implements IMediaServerService { ); } } catch { - // Some users may not have access to this item — skip silently. + // Some users may not have access to this item - skip silently. } } @@ -798,7 +798,7 @@ export class EmbyAdapterService implements IMediaServerService { if (!item) continue; // A collection can track an episode at any level, so protect the // episode and its season and series. ParentId is intentionally - // omitted — for Emby movies it is the library folder, not a + // omitted - for Emby movies it is the library folder, not a // collectable ancestor. Movies only carry Id. if (item.Id) playing.add(item.Id); if (item.SeasonId) playing.add(item.SeasonId); @@ -828,7 +828,7 @@ export class EmbyAdapterService implements IMediaServerService { try { // User-scoped read: Emby resolves the BoxSet query against a user's // library view, so an unscoped read can miss or 404. Pass the user via the - // UserId query param on the literal /Items path — functionally the same as + // UserId query param on the literal /Items path - functionally the same as // /Users/{id}/Items, and the param idiom already used elsewhere here. (A // user value interpolated into the request path is a CodeQL SSRF sink; a // query param is not.) @@ -938,7 +938,7 @@ export class EmbyAdapterService implements IMediaServerService { // Create with one item: Emby's create-collection endpoint throws HTTP 500 // ("Sequence contains no elements" in CollectionManager) when creating an // empty collection under a library folder, so it needs at least one item - // (#3075 — the regression from #3001's empty-create). The rest are added + // (#3075 - the regression from #3001's empty-create). The rest are added // afterwards via addBatchToCollection; re-adding this item there is an // idempotent no-op (collection membership is a set). const { data } = await this.http.post( @@ -1167,7 +1167,7 @@ export class EmbyAdapterService implements IMediaServerService { // Emby exposes DisplayOrder = PremiereDate | SortName on a BoxSet (via // ItemUpdateService) but no item-move/reorder endpoint, so an explicit // ordered list of item IDs can't be expressed. Gated by - // supportsFeature(COLLECTION_SORT) which is false for Emby — callers + // supportsFeature(COLLECTION_SORT) which is false for Emby - callers // shouldn't reach here. throw new Error( 'Collection sort is not supported on Emby (no item-move API)', @@ -1220,7 +1220,7 @@ export class EmbyAdapterService implements IMediaServerService { error, 'Connection failed', ); - // A 500 here is raised inside Emby's own image handler — most often the + // A 500 here is raised inside Emby's own image handler - most often the // library's "Save artwork into media folders" setting (per library, not // global) makes Emby write the poster next to the media file, and that // path is read-only (e.g. a movie library mounted read-only while the TV @@ -1233,7 +1233,7 @@ export class EmbyAdapterService implements IMediaServerService { typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data); - if (body) detail = ` — ${body.slice(0, 500)}`; + if (body) detail = ` - ${body.slice(0, 500)}`; } throw new Error( `Failed to upload Emby collection image: ${message}${detail}`, diff --git a/apps/server/src/modules/api/media-server/emby/emby.mapper.spec.ts b/apps/server/src/modules/api/media-server/emby/emby.mapper.spec.ts index 3a2ca8174..a02c5a523 100644 --- a/apps/server/src/modules/api/media-server/emby/emby.mapper.spec.ts +++ b/apps/server/src/modules/api/media-server/emby/emby.mapper.spec.ts @@ -6,7 +6,7 @@ import type { EmbyBaseItemDto, EmbyUserDto } from './emby.types'; * Maintainerr's MediaItem contract. Emby's API and Jellyfin's API share the * same .NET-derived BaseItemDto field shape (Jellyfin forked Emby in 2018), * so the synthetic fixtures below mirror the ones in jellyfin.mapper.spec.ts - * — they assert how the mapper transforms a known input, not what Emby + * - they assert how the mapper transforms a known input, not what Emby * returns over the wire. */ describe('EmbyMapper', () => { diff --git a/apps/server/src/modules/api/media-server/emby/emby.mapper.ts b/apps/server/src/modules/api/media-server/emby/emby.mapper.ts index 05e0f5127..96c8710ea 100644 --- a/apps/server/src/modules/api/media-server/emby/emby.mapper.ts +++ b/apps/server/src/modules/api/media-server/emby/emby.mapper.ts @@ -236,7 +236,7 @@ export class EmbyMapper { updatedAt: extras.DateLastSaved ? new Date(extras.DateLastSaved) : undefined, - // Emby has no native smart collections — only manual BoxSets and the + // Emby has no native smart collections - only manual BoxSets and the // TheMovieDb-driven "Automatic Creation of Collections" (movie franchise // grouping, not filter rules). Always false, matching the Jellyfin mapper. smart: false, diff --git a/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.spec.ts b/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.spec.ts index fa273d68a..ff9ba61c3 100644 --- a/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.spec.ts +++ b/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.spec.ts @@ -772,7 +772,7 @@ describe('JellyfinAdapterService', () => { it('rejects blank Jellyfin item ids before calling the API', async () => { await expect(service.refreshItemMetadata(' ')).rejects.toThrow( - 'refreshItemMetadata called with empty itemId — aborting metadata refresh request', + 'refreshItemMetadata called with empty itemId - aborting metadata refresh request', ); expect(jellyfinApiMocks.refreshItem).not.toHaveBeenCalled(); @@ -1516,7 +1516,7 @@ describe('JellyfinAdapterService', () => { isLocked: true, }), ); - // Collections are created empty — item ids must never be sent on create + // Collections are created empty - item ids must never be sent on create // (they go in the query string → HTTP 414 at scale); items are added via // addBatchToCollection. expect(collectionApiMocks.createCollection).not.toHaveBeenCalledWith( diff --git a/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.ts b/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.ts index 0bf61e101..20d3e03c4 100644 --- a/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.ts +++ b/apps/server/src/modules/api/media-server/jellyfin/jellyfin-adapter.service.ts @@ -408,7 +408,7 @@ export class JellyfinAdapterService implements IMediaServerService { * Pick a single random media item (movie or series, configurable) from the * given section ids. When no section ids are provided, picks across all * supported libraries. Uses Jellyfin's native `ItemSortBy.Random` so the - * server does the randomisation — no client-side sampling needed. + * server does the randomisation - no client-side sampling needed. */ async findRandomItem( sectionIds: string[] | undefined, @@ -419,7 +419,7 @@ export class JellyfinAdapterService implements IMediaServerService { try { const userId = await this.getUserId(); // The overlay editor UI passes a single section key; for "all sections" - // the caller omits the param. Anything else is unsupported — the + // the caller omits the param. Anything else is unsupported - the // recursive getItems call spans the selected parent or the whole server. const parentId = sectionIds?.[0]; const response = await getItemsApi(this.api).getItems({ @@ -456,7 +456,7 @@ export class JellyfinAdapterService implements IMediaServerService { /** * Download the raw bytes of a specific image for an item. The `imageType` - * is the caller's choice — `Primary` for movie/show posters, `Thumb` for + * is the caller's choice - `Primary` for movie/show posters, `Thumb` for * episode title-card stills. Forces JPEG so callers can rely on a known * Content-Type (the overlay editor's /poster proxy hard-codes image/jpeg; * the render pipeline also emits JPEG). Returns null when the item has no @@ -488,7 +488,7 @@ export class JellyfinAdapterService implements IMediaServerService { /** * Replace the given image type on an item. Sends the image as a - * base64-encoded string body — the Jellyfin server (at least through the + * base64-encoded string body - the Jellyfin server (at least through the * versions this project targets) rejects raw binary payloads on this * endpoint with a 500, despite the OpenAPI description hinting at * `image/*` binary. Base64 is the empirically-verified working path; see @@ -654,11 +654,11 @@ export class JellyfinAdapterService implements IMediaServerService { error instanceof AxiosError ? error.response?.status : undefined; if (status === 404) { this.logger.debug( - 'Jellyfin /System/Info/Storage not available — server is older than 10.11', + 'Jellyfin /System/Info/Storage not available - server is older than 10.11', ); } else if (status === 401 || status === 403) { this.logger.debug( - 'Jellyfin /System/Info/Storage denied — the configured user is not an administrator', + 'Jellyfin /System/Info/Storage denied - the configured user is not an administrator', ); } else { this.logger.debug(error); @@ -887,7 +887,7 @@ export class JellyfinAdapterService implements IMediaServerService { /** * Confirm a Jellyfin item is still present. Distinguishes "definitely - * gone" (200 with empty Items, or 404) from "could not check" — the + * gone" (200 with empty Items, or 404) from "could not check" - the * latter throws so revert callers don't drop their state on a blip. * An uninitialised adapter is treated as inconclusive (throws) for the * same reason: callers must never delete the only restore-from-overlay @@ -1045,7 +1045,7 @@ export class JellyfinAdapterService implements IMediaServerService { async prefetchWatchHistory(): Promise { // Jellyfin has no central watch-history endpoint (history is per-user), so // there is nothing to bulk prefetch. Gated by - // supportsFeature(CENTRAL_WATCH_HISTORY) which is false for Jellyfin — + // supportsFeature(CENTRAL_WATCH_HISTORY) which is false for Jellyfin - // callers shouldn't reach here. throw new Error( 'Bulk watch-history prefetch is not supported on Jellyfin (per-user history)', @@ -1116,7 +1116,7 @@ export class JellyfinAdapterService implements IMediaServerService { if (!item) continue; // A collection can track an episode at any level, so protect the // episode and its season and series. ParentId is intentionally - // omitted — for Jellyfin movies it is the library folder, not a + // omitted - for Jellyfin movies it is the library folder, not a // collectable ancestor. Movies only carry Id. if (item.Id) playing.add(item.Id); if (item.SeasonId) playing.add(item.SeasonId); @@ -1136,7 +1136,7 @@ export class JellyfinAdapterService implements IMediaServerService { * Jellyfin's Series Played flag is an all-or-nothing aggregate, so the * show-level watch history degenerates to sw_allEpisodesSeenBy (#2559). * One getItems call per user (batched via mapUsersBatched, shared with - * getAllUserItemData) — O(users), not O(users × episodes). + * getAllUserItemData) - O(users), not O(users × episodes). */ async getDescendantEpisodeWatchers(parentId: string): Promise { if (!this.api) return []; @@ -1160,7 +1160,7 @@ export class JellyfinAdapterService implements IMediaServerService { // Ignore unaired placeholders (mirrors #2624). excludeLocationTypes: [LocationType.Virtual], enableUserData: true, - // Minimize payload — we only need UserData per episode. + // Minimize payload - we only need UserData per episode. fields: [], }) ).data.Items ?? [], @@ -1312,7 +1312,7 @@ export class JellyfinAdapterService implements IMediaServerService { try { // Use getItems with enableUserData instead of the dedicated - // getItemUserData endpoint — the latter does not reliably return + // getItemUserData endpoint - the latter does not reliably return // per-user data when authenticating with an API key on all // Jellyfin versions. const response = await getItemsApi(this.api).getItems({ @@ -1489,7 +1489,7 @@ export class JellyfinAdapterService implements IMediaServerService { if (await this.getCollection(collectionId).then(Boolean)) { this.logger.error(`Failed to delete collection ${collectionId}`); this.logger.debug(error); - // Throw before the cache invalidation below — the collection still + // Throw before the cache invalidation below - the collection still // exists, so cached entries are still valid. throw error; } @@ -2040,13 +2040,13 @@ export class JellyfinAdapterService implements IMediaServerService { async deleteFromDisk(itemId: string): Promise { if (!this.api) { throw new Error( - 'Jellyfin API not initialized — cannot delete item from disk', + 'Jellyfin API not initialized - cannot delete item from disk', ); } if (!itemId || itemId.trim() === '') { throw new Error( - 'deleteFromDisk called with empty itemId — aborting to prevent unintended deletion', + 'deleteFromDisk called with empty itemId - aborting to prevent unintended deletion', ); } @@ -2081,13 +2081,13 @@ export class JellyfinAdapterService implements IMediaServerService { async refreshItemMetadata(itemId: string): Promise { if (!this.api) { throw new Error( - 'Jellyfin API not initialized — cannot refresh item metadata', + 'Jellyfin API not initialized - cannot refresh item metadata', ); } if (isBlankMediaServerId(itemId)) { throw new Error( - 'refreshItemMetadata called with empty itemId — aborting metadata refresh request', + 'refreshItemMetadata called with empty itemId - aborting metadata refresh request', ); } diff --git a/apps/server/src/modules/api/media-server/jellyfin/jellyfin.constants.ts b/apps/server/src/modules/api/media-server/jellyfin/jellyfin.constants.ts index 220cb582a..084cda673 100644 --- a/apps/server/src/modules/api/media-server/jellyfin/jellyfin.constants.ts +++ b/apps/server/src/modules/api/media-server/jellyfin/jellyfin.constants.ts @@ -18,7 +18,7 @@ export const JELLYFIN_BATCH_SIZE = { } as const; /** - * Default query options for every library-scoped getItems() call — i.e. + * Default query options for every library-scoped getItems() call - i.e. * any call whose parentId is a library (or no parentId for a global recursive * search) and that expects to surface real media items. * diff --git a/apps/server/src/modules/api/media-server/media-server-id.utils.spec.ts b/apps/server/src/modules/api/media-server/media-server-id.utils.spec.ts index 5c8eb5509..9f16b5add 100644 --- a/apps/server/src/modules/api/media-server/media-server-id.utils.spec.ts +++ b/apps/server/src/modules/api/media-server/media-server-id.utils.spec.ts @@ -132,7 +132,7 @@ describe('media-server-id.utils', () => { // #2853: malformed strings used to slip through because the filter only // rejected the all-zero Guid and Plex-shaped numeric IDs. Anything else - // — truncated UUIDs, non-hex garbage, fully-dashed but wrong-length — must + // - truncated UUIDs, non-hex garbage, fully-dashed but wrong-length - must // now be rejected before Maintainerr sends it to Jellyfin's refresh queue. it.each([ 'abc', diff --git a/apps/server/src/modules/api/media-server/media-server-id.utils.ts b/apps/server/src/modules/api/media-server/media-server-id.utils.ts index e4e7be80b..956165437 100644 --- a/apps/server/src/modules/api/media-server/media-server-id.utils.ts +++ b/apps/server/src/modules/api/media-server/media-server-id.utils.ts @@ -86,8 +86,8 @@ export function shouldRefreshMetadataItemId( } // Jellyfin Ids are always either 32-char unbroken hex or a 36-char dashed - // UUID. Anything else — truncated strings, garbage from migrations, the - // all-zero "empty Guid" — is rejected before the refresh request is issued + // UUID. Anything else - truncated strings, garbage from migrations, the + // all-zero "empty Guid" - is rejected before the refresh request is issued // so Jellyfin doesn't end up parsing the route as `Guid.Empty` and spamming // `ProviderManager.StartProcessingRefreshQueue` with "Guid can't be empty" // errors (#2853). The previous filter only rejected the all-zero Guid and diff --git a/apps/server/src/modules/api/media-server/media-server.factory.ts b/apps/server/src/modules/api/media-server/media-server.factory.ts index 7297249fe..7a614d010 100644 --- a/apps/server/src/modules/api/media-server/media-server.factory.ts +++ b/apps/server/src/modules/api/media-server/media-server.factory.ts @@ -283,7 +283,7 @@ export class MediaServerFactory { return adapter; } - // Connection is dead — force re-initialization + // Connection is dead - force re-initialization this.logger.debug( 'Media server unreachable during pre-job check, attempting re-initialization', ); diff --git a/apps/server/src/modules/api/media-server/media-server.interface.ts b/apps/server/src/modules/api/media-server/media-server.interface.ts index 7455ee5b3..211150eb9 100644 --- a/apps/server/src/modules/api/media-server/media-server.interface.ts +++ b/apps/server/src/modules/api/media-server/media-server.interface.ts @@ -95,7 +95,7 @@ export interface IMediaServerService { /** * Compute per-library size on disk by enumerating items. Potentially slow - * — meant to be called on demand. Returns a map of library id → bytes. + * - meant to be called on demand. Returns a map of library id → bytes. * Libraries missing from the map could not be sized. */ computeLibraryStorageSizes(): Promise>; @@ -166,7 +166,7 @@ export interface IMediaServerService { * HTTP requests. * * Gated by MediaServerFeature.CENTRAL_WATCH_HISTORY (a centrally queryable - * history endpoint). Throws if not supported — callers must check + * history endpoint). Throws if not supported - callers must check * supportsFeature() first; when unsupported, evaluation uses per-item queries. */ prefetchWatchHistory(abortSignal?: AbortSignal): Promise; @@ -206,7 +206,7 @@ export interface IMediaServerService { * an active streaming session. The collection worker uses this to defer * handling of in-use media to the next run (deletion is the case that * matters; the occasional non-destructive action is deferred too rather - * than scoped — a deliberate simplification). + * than scoped - a deliberate simplification). * * For hierarchical media the set includes every level a collection might * track: a playing episode contributes its own id plus its season and show @@ -214,7 +214,7 @@ export interface IMediaServerService { * protected. * * Best-effort: returns an empty set when nothing is playing and, after the - * HTTP client's own retries, when the lookup could not be completed — so a + * HTTP client's own retries, when the lookup could not be completed - so a * session outage degrades to the pre-existing behaviour (handle as usual) * rather than blocking the run. The worker reads this once at the start of a * run, so media that starts playing mid-run isn't protected until the next @@ -332,7 +332,7 @@ export interface IMediaServerService { * Set the primary poster image on a collection on the media server. * * Maintainerr is one writer among several (Kometa, Posterizarr, manual - * uploads). This is a single write — last writer wins. Unlike per-item + * uploads). This is a single write - last writer wins. Unlike per-item * overlays (which re-apply on cron because they carry day-counter state), * collection posters carry no per-cycle state, so callers should write * only when the source bytes change (user upload, collection re-create); @@ -387,7 +387,7 @@ export interface IMediaServerService { /** * Ask the media server to re-fetch metadata for a specific item from its * own configured agents. This is a best-effort, fire-and-forget operation - * on the server side — the call returns quickly while the server works async. + * on the server side - the call returns quickly while the server works async. */ refreshItemMetadata(itemId: string): Promise; } diff --git a/apps/server/src/modules/api/media-server/plex/plex-adapter.service.spec.ts b/apps/server/src/modules/api/media-server/plex/plex-adapter.service.spec.ts index 0afdb99fe..c2020d7f5 100644 --- a/apps/server/src/modules/api/media-server/plex/plex-adapter.service.spec.ts +++ b/apps/server/src/modules/api/media-server/plex/plex-adapter.service.spec.ts @@ -120,7 +120,7 @@ describe('PlexAdapterService', () => { it('should reject blank item ids before calling PlexApiService', async () => { await expect(service.refreshItemMetadata(' ')).rejects.toThrow( - 'refreshItemMetadata called with empty itemId — aborting metadata refresh request', + 'refreshItemMetadata called with empty itemId - aborting metadata refresh request', ); expect(plexApi.refreshMediaMetadata).not.toHaveBeenCalled(); diff --git a/apps/server/src/modules/api/media-server/plex/plex-adapter.service.ts b/apps/server/src/modules/api/media-server/plex/plex-adapter.service.ts index 85a7f2f39..19fc94c7b 100644 --- a/apps/server/src/modules/api/media-server/plex/plex-adapter.service.ts +++ b/apps/server/src/modules/api/media-server/plex/plex-adapter.service.ts @@ -777,7 +777,7 @@ export class PlexAdapterService implements IMediaServerService { async deleteFromDisk(itemId: string): Promise { if (!itemId || itemId.trim() === '') { throw new Error( - 'deleteFromDisk called with empty itemId — aborting to prevent unintended deletion', + 'deleteFromDisk called with empty itemId - aborting to prevent unintended deletion', ); } @@ -815,7 +815,7 @@ export class PlexAdapterService implements IMediaServerService { async refreshItemMetadata(itemId: string): Promise { if (isBlankMediaServerId(itemId)) { throw new Error( - 'refreshItemMetadata called with empty itemId — aborting metadata refresh request', + 'refreshItemMetadata called with empty itemId - aborting metadata refresh request', ); } diff --git a/apps/server/src/modules/api/plex-api/plex-api.constants.ts b/apps/server/src/modules/api/plex-api/plex-api.constants.ts index 1c48d7810..62c0acb5e 100644 --- a/apps/server/src/modules/api/plex-api/plex-api.constants.ts +++ b/apps/server/src/modules/api/plex-api/plex-api.constants.ts @@ -9,7 +9,7 @@ export const PLEX_PAGE_SIZE = { export const PLEX_REQUEST_TIMEOUT_MS = 30_000; // Key in the 'plexwatchhistory' cache for the leaf watch-history map built by -// prefetchWatchHistory() — leaf items (movies + episodes) keyed by own +// prefetchWatchHistory() - leaf items (movies + episodes) keyed by own // ratingKey. TTL and flush behaviour live on the cache definition in // lib/cache.ts. export const WATCH_HISTORY_BULK_CACHE_KEY = 'watch-history-bulk'; diff --git a/apps/server/src/modules/api/plex-api/plex-api.service.spec.ts b/apps/server/src/modules/api/plex-api/plex-api.service.spec.ts index 0f28de399..5e7b167d0 100644 --- a/apps/server/src/modules/api/plex-api/plex-api.service.spec.ts +++ b/apps/server/src/modules/api/plex-api/plex-api.service.spec.ts @@ -801,7 +801,7 @@ describe('PlexApiService.prefetchWatchHistory', () => { const cacheManager = (await import('../lib/cache')).default; // A full-looking page with no totalSize: queryAll may have truncated, so the - // map must NOT be cached — callers fall back to the per-item query instead. + // map must NOT be cached - callers fall back to the per-item query instead. const queryAll = jest.fn().mockResolvedValue({ MediaContainer: { Metadata: [ @@ -1076,7 +1076,7 @@ describe('PlexApiService.getWatchHistory bulk map', () => { it('falls through to per-item query for untyped callers on a leaf-map miss', async () => { // Untyped callers may pass show or season ratingKeys, which are never in - // the leaf map — a miss must not be reported as confirmed-empty history. + // the leaf map - a miss must not be reported as confirmed-empty history. const cacheManager = (await import('../lib/cache')).default; cacheManager .getCache('plexwatchhistory') diff --git a/apps/server/src/modules/api/plex-api/plex-api.service.ts b/apps/server/src/modules/api/plex-api/plex-api.service.ts index dd84cc646..dcc13c017 100644 --- a/apps/server/src/modules/api/plex-api/plex-api.service.ts +++ b/apps/server/src/modules/api/plex-api/plex-api.service.ts @@ -187,7 +187,7 @@ export class PlexApiService { this.plexClient = undefined; this.plexCommunityClient = undefined; this.plexTvClient = undefined; - // Drop the watch-history snapshot too — on a server/token switch it would + // Drop the watch-history snapshot too - on a server/token switch it would // otherwise serve the previous server's history for up to its TTL. this.watchHistoryPrefetch = undefined; cacheManager.getCache('plexguid').data.flushAll(); @@ -237,7 +237,7 @@ export class PlexApiService { if (settingsPlex.manualMode) { this.plexClient = undefined; this.logger.warn( - 'Plex connection failed (manual mode active — skipping re-discovery)', + 'Plex connection failed (manual mode active - skipping re-discovery)', ); return; } @@ -270,7 +270,7 @@ export class PlexApiService { if (!storedMachineId) { this.logger.debug( - 'No stored machine ID — cannot identify server for re-discovery', + 'No stored machine ID - cannot identify server for re-discovery', ); return false; } @@ -306,7 +306,7 @@ export class PlexApiService { const ok = await testClient.getStatus(); if (!ok) continue; - // Found a working connection — promote it + // Found a working connection - promote it this.plexClient = new PlexApi({ hostname: conn.address, port: conn.port, @@ -708,11 +708,11 @@ export class PlexApiService { * Show/season history is intentionally NOT rolled up here: the bulk endpoint * keys each row by leaf ratingKey, and grouping to show/season would depend on * grandparentKey/parentKey, which are undocumented on this endpoint and absent - * over some Plex connections — a missing key would silently read as "never + * over some Plex connections - a missing key would silently read as "never * watched". Show/season queries therefore fall through to the per-item * metadataItemID query in getWatchHistory, which Plex rolls up server-side. * - * On failure the error is logged and swallowed — getWatchHistory falls back + * On failure the error is logged and swallowed - getWatchHistory falls back * to per-item queries automatically when the map is absent. */ public prefetchWatchHistory(abortSignal?: AbortSignal): Promise { @@ -763,7 +763,7 @@ export class PlexApiService { if (typeof totalSize !== 'number' || records.length < totalSize) { this.logger.warn( `Watch history prefetch returned an unverifiable result ` + - `(received ${records.length}, totalSize ${totalSize ?? 'absent'}) — ` + + `(received ${records.length}, totalSize ${totalSize ?? 'absent'}) - ` + `falling back to per-item queries.`, ); return; @@ -784,7 +784,7 @@ export class PlexApiService { .data.set(WATCH_HISTORY_BULK_CACHE_KEY, leafMap); this.logger.log( - `Watch history prefetch complete: ${records.length} records — ` + + `Watch history prefetch complete: ${records.length} records - ` + `${leafMap.size} leaf items.`, ); } catch (error) { @@ -793,13 +793,13 @@ export class PlexApiService { } this.logger.warn( - `Watch history prefetch failed — falling back to per-item queries. Error: ${error}`, + `Watch history prefetch failed - falling back to per-item queries. Error: ${error}`, ); } } // The 'plexwatchhistory' cache stores by reference (useClones: false), so - // always hand callers a copy — plex-getter sorts these arrays in place. + // always hand callers a copy - plex-getter sorts these arrays in place. private getBulkWatchHistory( mapKey: string, itemId: string, @@ -842,7 +842,7 @@ export class PlexApiService { break; default: { // Untyped callers may pass any kind of ratingKey, so only a non-empty - // leaf hit is trusted — a miss falls through to the per-item query. + // leaf hit is trusted - a miss falls through to the per-item query. const records = this.getBulkWatchHistory( WATCH_HISTORY_BULK_CACHE_KEY, itemId, @@ -872,7 +872,7 @@ export class PlexApiService { * Returns the items in every active play session. Plex's * `/status/sessions` returns only the `MediaContainer` (no `Metadata`) when * nothing is playing, so an empty array is the normal "idle" result. Never - * cached — sessions are live state. Best-effort: the plexClient retries + * cached - sessions are live state. Best-effort: the plexClient retries * transient failures (axios-retry, exponential backoff), and a persistent * failure returns [] so a session outage degrades to normal handling rather * than blocking the run. @@ -1077,7 +1077,7 @@ export class PlexApiService { try { // Plex move is per-item. Omitting `after` puts the item at the front; // otherwise it lands immediately after `afterId`. Reordering a full - // collection is therefore O(n) sequential PUTs — acceptable for the + // collection is therefore O(n) sequential PUTs - acceptable for the // collection sizes Maintainerr manages. const afterQuery = afterId ? `?after=${afterId}` : ''; await this.plexClient.putQuery({ @@ -1224,7 +1224,7 @@ export class PlexApiService { message: string; } { // lib/plexApi wraps Axios failures in a plain Error with the original - // attached as `cause` — unwrap it, or the status and response body + // attached as `cause` - unwrap it, or the status and response body // (Plex's actual rejection reason) never reach the logs. const cause = error instanceof Error ? error.cause : undefined; const axiosError = axios.isAxiosError(error) @@ -1937,7 +1937,7 @@ export class PlexApiService { * Confirm a Plex item is still present. * * `getItemType` swallows every error as `null`, which conflates "gone" - * with "I couldn't ask right now" — fine for type lookup, dangerous for + * with "I couldn't ask right now" - fine for type lookup, dangerous for * cleanup decisions that delete the only restore-from-overlay backup. * This variant returns `false` only when Plex explicitly reports 404 * and rethrows on auth / network / 5xx so callers preserve state. @@ -2080,7 +2080,7 @@ export class PlexApiService { const episode = withThumb[Math.floor(Math.random() * withThumb.length)]; const displayTitle = episode.grandparentTitle - ? `${episode.grandparentTitle} — ${episode.title ?? episode.ratingKey}` + ? `${episode.grandparentTitle} - ${episode.title ?? episode.ratingKey}` : (episode.title ?? String(episode.ratingKey)); return { plexId: String(episode.ratingKey), title: displayTitle }; diff --git a/apps/server/src/modules/api/seerr-api/seerr-api.service.spec.ts b/apps/server/src/modules/api/seerr-api/seerr-api.service.spec.ts index 4de9bca84..30759cbe1 100644 --- a/apps/server/src/modules/api/seerr-api/seerr-api.service.spec.ts +++ b/apps/server/src/modules/api/seerr-api/seerr-api.service.spec.ts @@ -332,7 +332,7 @@ describe('SeerrApiService', () => { await expect(service.getRequestsForMedia(100)).resolves.toHaveLength(2); await expect(service.getRequestsForMedia(200)).resolves.toHaveLength(1); await expect(service.getRequestsForMedia(999)).resolves.toEqual([]); - // One sweep total — later lookups are served from the cached index. + // One sweep total - later lookups are served from the cached index. expect(getWithoutCache).toHaveBeenCalledTimes(1); // Returned values are deep copies: neither reshaping the array nor diff --git a/apps/server/src/modules/api/seerr-api/seerr-api.service.ts b/apps/server/src/modules/api/seerr-api/seerr-api.service.ts index af9d5ff57..fe02abcc0 100644 --- a/apps/server/src/modules/api/seerr-api/seerr-api.service.ts +++ b/apps/server/src/modules/api/seerr-api/seerr-api.service.ts @@ -332,7 +332,7 @@ export class SeerrApiService { // The HTTP helper swallows request failures and returns undefined; a // genuine empty result still carries pageInfo. A missing pageInfo means - // the sweep failed — surface that (transient), don't read it as empty. + // the sweep failed - surface that (transient), don't read it as empty. if (!resp?.pageInfo) { return undefined; } @@ -367,8 +367,8 @@ export class SeerrApiService { * Returns a deep copy of the title's request list (the cache holds the Map by * reference with useClones off), so callers may read or mutate it freely * without corrupting the shared index. `[]` means the sweep succeeded and the - * title has no request (definitive). `undefined` means the sweep failed — - * Seerr is unreachable — so the getter returns `undefined` (transient) and the + * title has no request (definitive). `undefined` means the sweep failed - + * Seerr is unreachable - so the getter returns `undefined` (transient) and the * comparator protects the item rather than treating it as "not requested". */ public async getRequestsForMedia( @@ -415,7 +415,7 @@ export class SeerrApiService { // requestDate reads requests[0].createdAt and the legacy per-item // getMovie/getShow path returned mediaInfo.requests oldest-first. The bulk // /request sweep is newest-first, so sort ascending by createdAt (tie-break - // on id) — requestDate, addUser and the season ordering then match the + // on id) - requestDate, addUser and the season ordering then match the // pre-#3152 behaviour regardless of how Seerr happened to page the sweep. requests.sort( (a, b) => @@ -424,7 +424,7 @@ export class SeerrApiService { ); // Group by media.tmdbId: Seerr keys every media row by tmdbId (non-null, - // indexed — tvdbId/imdbId are optional extras), and the metadata service + // indexed - tvdbId/imdbId are optional extras), and the metadata service // resolves each library item to that tmdbId via all its providers (with // tvdb/imdb -> tmdb bridging), so tmdbId is the canonical join key (and // matches the per-item getMovie/getShow path this replaces). media.requests diff --git a/apps/server/src/modules/api/servarr-api/common/servarr-api.service.ts b/apps/server/src/modules/api/servarr-api/common/servarr-api.service.ts index 08f5d65b2..715bceba6 100644 --- a/apps/server/src/modules/api/servarr-api/common/servarr-api.service.ts +++ b/apps/server/src/modules/api/servarr-api/common/servarr-api.service.ts @@ -254,7 +254,7 @@ export abstract class ServarrApi extends ExternalApiService { /** * Resolve a tag id for `label`, creating the tag if it doesn't exist yet. - * Matching is case-insensitive — *arr stores labels verbatim but treats them + * Matching is case-insensitive - *arr stores labels verbatim but treats them * case-insensitively, so we never create a duplicate that differs only in case. * * Race-tolerant: if the create fails (another caller created the same label in @@ -277,7 +277,7 @@ export abstract class ServarrApi extends ExternalApiService { return created.id; } - // Create failed (already exists from a concurrent caller, or errored) — + // Create failed (already exists from a concurrent caller, or errored) - // re-read and return the id if the label is now present. return match(await this.getTags()); }; diff --git a/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.spec.ts b/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.spec.ts index 9e1cad8d5..b4bf4c6b7 100644 --- a/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.spec.ts +++ b/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.spec.ts @@ -90,7 +90,7 @@ describe('RadarrApi', () => { }); // A 400 from a different validation rule (e.g. an invalid year) is a real - // failure — it must not be silently reported as "already excluded". + // failure - it must not be silently reported as "already excluded". it('returns false on a non-duplicate validation 400', async () => { postSpy.mockRejectedValue({ isAxiosError: true, diff --git a/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.ts b/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.ts index fe9b38ced..478cef0eb 100644 --- a/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.ts +++ b/apps/server/src/modules/api/servarr-api/helpers/radarr.helper.ts @@ -48,7 +48,7 @@ export class RadarrApi extends ServarrApi<{ movieId: number }> { }; // Intentionally uncached: this drives rule evaluation and resolves the - // movie that actions then mutate — both need Radarr's current truth, not a + // movie that actions then mutate - both need Radarr's current truth, not a // snapshot that can be up to DEFAULT_TTL stale. // Returns `null` when Radarr confirms the movie isn't tracked (empty // response) and `undefined` when the lookup itself failed (transport, auth, @@ -99,7 +99,7 @@ export class RadarrApi extends ServarrApi<{ movieId: number }> { /** * Add or remove a single tag on a batch of movies via the movie editor. - * `applyTags: 'add' | 'remove'` only — never 'replace', which would wipe every + * `applyTags: 'add' | 'remove'` only - never 'replace', which would wipe every * other tag the user has on those movies. Best-effort: returns false on failure * (callers treat tagging as non-fatal). No-ops on an empty id list. */ @@ -217,7 +217,7 @@ export class RadarrApi extends ServarrApi<{ movieId: number }> { * "already added" 400 as success rather than failing the whole collection * action (its unmonitor/delete has already run) on every re-run. The validator * also enforces non-empty tmdbId/title and a non-negative year, so a 400 from - * one of those is a real failure — surface it instead of silently marking the + * one of those is a real failure - surface it instead of silently marking the * movie excluded when it isn't. * * Goes through the shared post() client (rethrowing so we can read the status) @@ -259,7 +259,7 @@ export class RadarrApi extends ServarrApi<{ movieId: number }> { /** * True only for the exclusion validator's uniqueness failure. Radarr returns a * 400 with an array of `{ propertyName, errorMessage }`; the uniqueness rule is - * the one that means "already excluded — goal met". Any other validation + * the one that means "already excluded - goal met". Any other validation * failure (empty title, negative year, …) must stay a failure. */ private isAlreadyExcludedError(body: unknown): boolean { diff --git a/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.spec.ts b/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.spec.ts index 4d651ada5..3ef4caece 100644 --- a/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.spec.ts +++ b/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.spec.ts @@ -204,7 +204,7 @@ describe('SonarrApi', () => { // contract: `undefined` = the lookup itself failed (fail closed), `null` = // Sonarr confirmed the series isn't tracked. getWithoutCache swallows HTTP // errors to `undefined` without throwing, so the failure must be detected - // from that value — the catch path never sees it. (#3125) + // from that value - the catch path never sees it. (#3125) describe('getSeriesByTvdbId null/undefined contract (#3125)', () => { it('returns undefined when the lookup fails transiently (getWithoutCache → undefined)', async () => { jest diff --git a/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.ts b/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.ts index 528c48bac..bcb9317bd 100644 --- a/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.ts +++ b/apps/server/src/modules/api/servarr-api/helpers/sonarr.helper.ts @@ -107,7 +107,7 @@ export class SonarrApi extends ServarrApi<{ // Intentionally uncached: this endpoint is read straight after mutations // (unmonitor + file deletes) by the empty-show cleanup, and also drives - // rule evaluation — both need Sonarr's current truth, not a snapshot that + // rule evaluation - both need Sonarr's current truth, not a snapshot that // can be up to DEFAULT_TTL stale (see issue #2757 / #2891). // Returns `null` when Sonarr confirms the series isn't tracked (empty // response) and `undefined` when the lookup itself failed (transport, auth, @@ -153,7 +153,7 @@ export class SonarrApi extends ServarrApi<{ /** * Add or remove a single tag on a batch of series via the series editor. - * `applyTags: 'add' | 'remove'` only — never 'replace', which would wipe every + * `applyTags: 'add' | 'remove'` only - never 'replace', which would wipe every * other tag the user has on those series. Best-effort: returns false on failure * (callers treat tagging as non-fatal). No-ops on an empty id list. Sonarr tags * are series-level; there is no per-season tag. diff --git a/apps/server/src/modules/api/servarr-api/interfaces/servarr.interface.ts b/apps/server/src/modules/api/servarr-api/interfaces/servarr.interface.ts index a12006614..6caf9302f 100644 --- a/apps/server/src/modules/api/servarr-api/interfaces/servarr.interface.ts +++ b/apps/server/src/modules/api/servarr-api/interfaces/servarr.interface.ts @@ -66,7 +66,7 @@ export interface Tag { } /** - * A Radarr/Sonarr history record. `downloadId` is the download-client item id — + * A Radarr/Sonarr history record. `downloadId` is the download-client item id - * for a torrent client (e.g. qBittorrent) this is the torrent infohash. Only * the fields we consume are typed. * diff --git a/apps/server/src/modules/api/streamystats-api/streamystats-api.service.spec.ts b/apps/server/src/modules/api/streamystats-api/streamystats-api.service.spec.ts index 87b2f5b77..81279b7af 100644 --- a/apps/server/src/modules/api/streamystats-api/streamystats-api.service.spec.ts +++ b/apps/server/src/modules/api/streamystats-api/streamystats-api.service.spec.ts @@ -294,7 +294,7 @@ describe('StreamystatsApiService', () => { }); expect(result.status).toBe('OK'); - // The helper is constructed with url only — no apiKey leaks to a + // The helper is constructed with url only - no apiKey leaks to a // user-supplied URL via /api/settings/test/streamystats. const callArgs = StreamystatsApiMock.mock.calls.at(-1)?.[0]; expect(callArgs?.url).toBe('http://streamystats'); diff --git a/apps/server/src/modules/api/streamystats-api/streamystats-api.service.ts b/apps/server/src/modules/api/streamystats-api/streamystats-api.service.ts index 2d0169966..8b6090213 100644 --- a/apps/server/src/modules/api/streamystats-api/streamystats-api.service.ts +++ b/apps/server/src/modules/api/streamystats-api/streamystats-api.service.ts @@ -143,8 +143,8 @@ export class StreamystatsApiService { * so callers can skip rather than treat absence as "not watchlisted". * * The watchlist endpoints authenticate via Jellyfin's MediaBrowser token - * scheme — unlike the item-details endpoint, the `Bearer` header is rejected - * — so each call overrides the Authorization header accordingly. + * scheme - unlike the item-details endpoint, the `Bearer` header is rejected + * - so each call overrides the Authorization header accordingly. */ public async getWatchlistMembership(): Promise { if (!this.api || !this.settings.jellyfin_api_key) { diff --git a/apps/server/src/modules/collections/collection-handler.spec.ts b/apps/server/src/modules/collections/collection-handler.spec.ts index cde3f948b..3c0535f74 100644 --- a/apps/server/src/modules/collections/collection-handler.spec.ts +++ b/apps/server/src/modules/collections/collection-handler.spec.ts @@ -536,7 +536,7 @@ describe('CollectionHandler', () => { await collectionHandler.handleMedia(collection, collectionMedia); - // The post-handle save must carry the cleared link forward — saving the + // The post-handle save must carry the cleared link forward - saving the // stale snapshot would resurrect the dead BoxSet id and force the next rule // run to rediscover it via a 404. expect(collectionsService.saveCollection).toHaveBeenCalledWith( diff --git a/apps/server/src/modules/collections/collection-handler.ts b/apps/server/src/modules/collections/collection-handler.ts index adc160a16..6d14c9b79 100644 --- a/apps/server/src/modules/collections/collection-handler.ts +++ b/apps/server/src/modules/collections/collection-handler.ts @@ -18,7 +18,7 @@ import { RecentlyHandledMediaService } from './recently-handled-media.service'; * - `handled`: the configured action ran and the item was processed. * - `failed`: the action could not be completed; the item stays for retry. * - `removed-missing`: the item no longer existed on the media server and was - * pruned from the collection(s) — a cleanup, not a failure or a real handle. + * pruned from the collection(s) - a cleanup, not a failure or a real handle. */ export type HandleMediaResult = 'handled' | 'failed' | 'removed-missing'; @@ -114,8 +114,8 @@ export class CollectionHandler { // The action didn't run. Before treating this as a retryable failure, // check whether the item still exists: if it's already gone from the // media server there is nothing left to act on, and leaving it in the - // collection means re-processing it — and re-resolving its dead BoxSet - // link — on every run (#3023). A failed existence check is treated as + // collection means re-processing it - and re-resolving its dead BoxSet + // link - on every run (#3023). A failed existence check is treated as // "still present" so a transient blip never drops a live item. let exists = true; try { @@ -134,7 +134,7 @@ export class CollectionHandler { // The removal-by-id is a no-op on the media server for a gone item (Plex // skips 404, Jellyfin/Emby return 2xx), so these drop the stale DB rows. // A genuinely transient removal failure keeps the row, which the next run - // retries — no permanent stale state, so no special-casing needed here. + // retries - no permanent stale state, so no special-casing needed here. await this.collectionService.removeFromCollection(collection.id, [ { mediaServerId: media.mediaServerId, @@ -185,7 +185,7 @@ export class CollectionHandler { } case 'episode': { // Seerr tracks requests per season, not per episode, so there is - // no per-episode request to remove — deleting the season request + // no per-episode request to remove - deleting the season request // would drop the request for every other (still-present) episode // in that season. Skip the force-removal and let Seerr's // availability sync reconcile, as it does when Force Seerr is off. @@ -270,8 +270,8 @@ export class CollectionHandler { * dead BoxSet link doesn't linger and get re-resolved on every rule run * (#3023). Each pruned sibling is marked handled: the rule executor checks * that guard per collection, so without it the sibling's next pass could - * re-add the item — it may still resolve on the media server, and conditions - * like `isWatched` stay true — firing a spurious `Media Added` notification + * re-add the item - it may still resolve on the media server, and conditions + * like `isWatched` stay true - firing a spurious `Media Added` notification * and recreating the link this cleanup just removed. */ private async pruneSiblingCollections( diff --git a/apps/server/src/modules/collections/collection-poster.service.ts b/apps/server/src/modules/collections/collection-poster.service.ts index 394d7d312..3f6fd0e4f 100644 --- a/apps/server/src/modules/collections/collection-poster.service.ts +++ b/apps/server/src/modules/collections/collection-poster.service.ts @@ -19,14 +19,14 @@ export class InvalidCollectionPosterError extends Error {} * Storage model: * - One JPEG per Maintainerr collection at * `data/collection-posters/{collectionDbId}.jpg`. File presence is the - * flag — there is no DB column. Same on-disk pattern as the overlay + * flag - there is no DB column. Same on-disk pattern as the overlay * originals backup ([overlay-processor.service.ts](../overlays/overlay-processor.service.ts)). * - Db id is stable across media-server collection re-creates, so a stored * poster survives Plex/Jellyfin collection deletion + recreation. * * Coexistence: * - Maintainerr is one writer among several (Kometa, Posterizarr, manual - * uploads). This is a single write — last writer wins. Unlike per-item + * uploads). This is a single write - last writer wins. Unlike per-item * overlays (which re-apply on cron because they carry day-counter state), * collection posters carry no per-cycle state, so callers should write * only when the source bytes change (user upload, collection re-create); @@ -91,7 +91,7 @@ export class CollectionPosterService { .toBuffer(); } catch (error) { this.logger.warn( - `Rejected collection ${collectionDbId} poster upload — not a valid image`, + `Rejected collection ${collectionDbId} poster upload - not a valid image`, ); this.logger.debug(error); throw new InvalidCollectionPosterError( @@ -109,7 +109,7 @@ export class CollectionPosterService { } /** - * Remove the stored poster file. Does not touch the media-server side — + * Remove the stored poster file. Does not touch the media-server side - * the user must clear/refresh the poster in Plex/Jellyfin themselves if * they want the original artwork back. */ @@ -122,7 +122,7 @@ export class CollectionPosterService { /** * Issue a generic metadata-refresh request to the media server for the - * given collection. This does not force image replacement — it just nudges + * given collection. This does not force image replacement - it just nudges * Plex/Jellyfin to re-evaluate their own artwork sources, which may or may * not change the visible poster depending on configured agents and server * caching. Best-effort: returns `requested: false` when no mediaServerId is @@ -140,7 +140,7 @@ export class CollectionPosterService { mediaServer = await this.mediaServerFactory.getService(); } catch (error) { this.logger.warn( - 'Cannot refresh collection metadata — no media server configured', + 'Cannot refresh collection metadata - no media server configured', ); this.logger.debug(error); return { requested: false }; @@ -177,7 +177,7 @@ export class CollectionPosterService { mediaServer = await this.mediaServerFactory.getService(); } catch (error) { this.logger.warn( - 'Cannot push collection poster — no media server configured', + 'Cannot push collection poster - no media server configured', ); this.logger.debug(error); return { attempted: false, pushed: false }; diff --git a/apps/server/src/modules/collections/collection-worker.server.spec.ts b/apps/server/src/modules/collections/collection-worker.server.spec.ts index 10ccd7e44..b864ad918 100644 --- a/apps/server/src/modules/collections/collection-worker.server.spec.ts +++ b/apps/server/src/modules/collections/collection-worker.server.spec.ts @@ -256,7 +256,7 @@ describe('CollectionWorkerService', () => { await collectionWorkerService.execute(); - // The item was already gone — nothing on disk changed, so no sync and + // The item was already gone - nothing on disk changed, so no sync and // neither the handled nor the failed notification fires. expect(seerrApi.api.post).not.toHaveBeenCalled(); expect(eventEmitter.emit).not.toHaveBeenCalledWith( diff --git a/apps/server/src/modules/collections/collection-worker.service.ts b/apps/server/src/modules/collections/collection-worker.service.ts index d549cc068..fd10b288b 100644 --- a/apps/server/src/modules/collections/collection-worker.service.ts +++ b/apps/server/src/modules/collections/collection-worker.service.ts @@ -95,7 +95,7 @@ export class CollectionWorkerService extends TaskBase { } // Currently-playing media is deferred to the next run so we don't act on - // it — chiefly delete it — out from under an active viewer. Best-effort: + // it - chiefly delete it - out from under an active viewer. Best-effort: // fetched once at the start of the run (media that starts playing mid-run // isn't protected until next time), and an empty set (nothing playing or // a failed lookup) simply means "handle as usual". @@ -246,7 +246,7 @@ export class CollectionWorkerService extends TaskBase { // The item was already gone from the media server and has been // pruned from the collection(s). It wasn't a failure and nothing // on disk was altered, so it stays out of both notification lists - // and doesn't trigger availability sync — the handler already + // and doesn't trigger availability sync - the handler already // logged the cleanup. removedMissingMedia++; } else { diff --git a/apps/server/src/modules/collections/collections.service.spec.ts b/apps/server/src/modules/collections/collections.service.spec.ts index 4940e55f1..7e0677bde 100644 --- a/apps/server/src/modules/collections/collections.service.spec.ts +++ b/apps/server/src/modules/collections/collections.service.spec.ts @@ -2018,7 +2018,7 @@ describe('CollectionsService', () => { // `deleteSoonest` is equivalent to ordering by `collection_media.addDate` // because `deleteAfterDays` is constant across a collection. SQL does the // pagination so we don't have to hydrate every row in the collection - // before slicing — critical for collections with hundreds of items where + // before slicing - critical for collections with hundreds of items where // hydrating all rows would block the UI for minutes. const collection = createCollection({ id: 9, @@ -2644,7 +2644,7 @@ describe('CollectionsService', () => { childCount: rows.length, } as any); - // Hand back metadata where every item has the same library addedAt — + // Hand back metadata where every item has the same library addedAt - // if the comparator falls through to MediaItem.addedAt (the bug) the // ordering becomes whatever Map iteration gives us; the assertion // below would fail. @@ -2748,7 +2748,7 @@ describe('CollectionsService', () => { ); expect(found?.id).toBe('box-1'); - // Own library searched first, then the other one — never re-searching it. + // Own library searched first, then the other one - never re-searching it. expect(mediaServer.getCollections).toHaveBeenCalledWith('shows'); expect(mediaServer.getCollections).toHaveBeenCalledWith('movies'); expect(mediaServer.getCollections).toHaveBeenCalledTimes(2); diff --git a/apps/server/src/modules/collections/collections.service.ts b/apps/server/src/modules/collections/collections.service.ts index f6ab2ec76..1a4ea06de 100644 --- a/apps/server/src/modules/collections/collections.service.ts +++ b/apps/server/src/modules/collections/collections.service.ts @@ -235,7 +235,7 @@ export class CollectionsService { * automatic collection sharing this collection's media server collection. * * Throws on repository failure. Callers must treat a thrown error as - * "ownership unknown" — silently defaulting to an empty set would + * "ownership unknown" - silently defaulting to an empty set would * re-introduce the cross-rule contamination this method exists to prevent * (sibling-owned children would be imported as `manual` into the wrong * rule's collection_media). @@ -321,7 +321,7 @@ export class CollectionsService { ); this.logger.debug(error); // An exception is not evidence the server rejected the adds, so - // report nothing attempted — callers must not heal on this. + // report nothing attempted - callers must not heal on this. return { attempted: 0, rejected: 0 }; } } @@ -329,7 +329,7 @@ export class CollectionsService { /** * Collections healed once already (per process). A second total * rejection without an accepted add in between means recreation didn't - * fix the cause — stop churning delete/recreate and leave the loud log. + * fix the cause - stop churning delete/recreate and leave the loud log. */ private readonly healedCollectionIds = new Set(); @@ -359,7 +359,7 @@ export class CollectionsService { if (this.healedCollectionIds.has(collection.id)) { this.logger.error( - `Media server collection for "${collection.title}" still rejects every add after being recreated — leaving it in place. Check the Plex response body logged above for the rejection reason.`, + `Media server collection for "${collection.title}" still rejects every add after being recreated - leaving it in place. Check the Plex response body logged above for the rejection reason.`, ); return false; } @@ -374,7 +374,7 @@ export class CollectionsService { } this.logger.warn( - `Media server collection ${collection.mediaServerId} for "${collection.title}" is empty and rejected every add — deleting it so it can be recreated`, + `Media server collection ${collection.mediaServerId} for "${collection.title}" is empty and rejected every add - deleting it so it can be recreated`, ); await mediaServer.deleteCollection(serverColl.id); this.healedCollectionIds.add(collection.id); @@ -694,7 +694,7 @@ export class CollectionsService { ); } catch (verificationError) { this.logger.warn( - `Failed to verify collection "${collection.title}" after getCollectionChildren error — keeping link`, + `Failed to verify collection "${collection.title}" after getCollectionChildren error - keeping link`, ); this.logger.debug(error); this.logger.debug(verificationError); @@ -703,13 +703,13 @@ export class CollectionsService { if (!stillExists) { this.logger.warn( - `Collection "${collection.title}" references a media server collection that no longer exists — clearing stale link`, + `Collection "${collection.title}" references a media server collection that no longer exists - clearing stale link`, ); collection.mediaServerId = null; await this.saveCollection(collection); } else { this.logger.warn( - `getCollectionChildren failed for "${collection.title}" but collection still exists on server — keeping link`, + `getCollectionChildren failed for "${collection.title}" but collection still exists on server - keeping link`, ); this.logger.debug(error); } @@ -906,7 +906,7 @@ export class CollectionsService { } try { - // Plex rejects move/prefs on smart collections — skip defensively even + // Plex rejects move/prefs on smart collections - skip defensively even // though Maintainerr-managed collections are non-smart. const serverCollection = await mediaServer.getCollection( collection.mediaServerId, @@ -1018,7 +1018,7 @@ export class CollectionsService { if (!sort || sort === 'deleteSoonest') { // SQL-paginate by `collection_media.addDate`. `deleteSoonest` is // equivalent to `addDate` ordering because `deleteAfterDays` is - // constant for every item in a collection — so the only sort key + // constant for every item in a collection - so the only sort key // that actually matters lives on the `collection_media` row and the // database can paginate it directly without hydrating MediaItem // metadata for every row in the collection. This keeps page loads @@ -1029,7 +1029,7 @@ export class CollectionsService { // so the polished alphabetical-within-day order is what users see // when browsing the actual Plex/Jellyfin collection. The Maintainerr // UI page may show same-day items in a slightly different order - // (by `addDate, id` instead of by title) — acceptable because the + // (by `addDate, id` instead of by title) - acceptable because the // primary sort key is correct and Maintainerr's DB remains the // source of truth driving the next push. const direction = @@ -1052,7 +1052,7 @@ export class CollectionsService { } // Explicit sort on a MediaItem-side key (airDate / rating / watchCount / - // title) — the sort value isn't on `collection_media`, so we have to + // title) - the sort value isn't on `collection_media`, so we have to // hydrate the whole collection before paginating. Acceptable because // these sorts are rarely used compared to `deleteSoonest` and the // default load. @@ -1122,7 +1122,7 @@ export class CollectionsService { // Only remove a row when the media server *confirms* the item is gone. // `itemExists` returns false solely on a 404/empty result and throws on // an inconclusive check (network / 5xx / auth), unlike `getMetadata` - // which returns undefined for both absent and failed reads — so a + // which returns undefined for both absent and failed reads - so a // transient blip can no longer delete a still-present item's row. let exists = true; try { @@ -1918,7 +1918,7 @@ export class CollectionsService { .filter((childId): childId is string => Boolean(childId)), ); this.logger.debug( - `[checkAutomaticMediaServerLink] Shared collection ${serverColl.id} has ${serverChildIds.size} children — checking for local rule-owned drift`, + `[checkAutomaticMediaServerLink] Shared collection ${serverColl.id} has ${serverChildIds.size} children - checking for local rule-owned drift`, ); const resyncResult = await this.resyncRuleOwnedItemsToMediaServerCollection( @@ -1934,7 +1934,7 @@ export class CollectionsService { } // An empty shared collection that rejected every resynced item - // can't be repaired in place — fall back to delete-and-recreate. + // can't be repaired in place - fall back to delete-and-recreate. // The sibling rule group loses nothing: the collection has no // children, and its link re-establishes via the title relink. if ( @@ -1977,8 +1977,8 @@ export class CollectionsService { collection.mediaServerId !== null && originalMediaServerId !== null ) { - // Jellyfin/Emby: a BoxSet can drain — its items re-imported with new - // ids, or a one-time add that didn't fully land — and sit on the server + // Jellyfin/Emby: a BoxSet can drain - its items re-imported with new + // ids, or a one-time add that didn't fully land - and sit on the server // under-populated while the DB still lists its rule-owned items. Re-add // the missing ones: empty BoxSets accept adds, so this repopulates in // place, the BoxSet id (with its overlays/poster) stays stable, and @@ -2006,8 +2006,8 @@ export class CollectionsService { if (!serverColl) { this.logger.debug( originalMediaServerId - ? `[checkAutomaticMediaServerLink] Media server collection for "${collection.title}" no longer exists — clearing link. It will be recreated automatically when items match the rule.` - : `[checkAutomaticMediaServerLink] No media server collection for "${collection.title}" — collection is empty and will be created automatically when items match the rule.`, + ? `[checkAutomaticMediaServerLink] Media server collection for "${collection.title}" no longer exists - clearing link. It will be recreated automatically when items match the rule.` + : `[checkAutomaticMediaServerLink] No media server collection for "${collection.title}" - collection is empty and will be created automatically when items match the rule.`, ); collection.mediaServerId = null; collection = await this.saveCollection(collection); @@ -2306,7 +2306,7 @@ export class CollectionsService { manualMembershipSource, ); - // Only notify for items whose membership was persisted — both + // Only notify for items whose membership was persisted - both // server-rejected items and locally-rolled-back items never // entered the collection and will be retried (and re-notified) // on a later run. @@ -2331,7 +2331,7 @@ export class CollectionsService { } // Every add rejected by the server: if the collection is also - // empty it is unpopulatable in place — heal by delete so the + // empty it is unpopulatable in place - heal by delete so the // next pass recreates it fresh. Keyed to server rejections only; // local persistence failures must never delete the collection. if (serverRejectedIds.size >= newMedia.length) { @@ -2410,7 +2410,7 @@ export class CollectionsService { * * Called after a delete-style action frees the underlying file. The item * still resolves on the media server at this point, so removing it from the - * sibling BoxSets now — while we still have a valid id to remove — keeps the + * sibling BoxSets now - while we still have a valid id to remove - keeps the * media server from holding unresolved linked-item paths once the library * drops the item on its next scan. Those dead links are what Jellyfin * re-resolves on every rule run, producing the "Unable to find linked item @@ -2419,7 +2419,7 @@ export class CollectionsService { * the sibling memberships. * * Returns the ids of the sibling collections it pruned, so the caller can - * mark the item recently-handled for each of them — otherwise the rule + * mark the item recently-handled for each of them - otherwise the rule * executor's next pass re-adds it (the id still resolves and conditions like * `isWatched` stay true), recreating the membership this just removed. */ @@ -3247,7 +3247,7 @@ export class CollectionsService { try { await this.collectionRepo.delete(collection.id); - // Drop any stored poster bytes; the media-server side is left alone — + // Drop any stored poster bytes; the media-server side is left alone - // Plex/Jellyfin will recompute a thumb from member items as usual. try { this.collectionPosterService.removeStoredPoster(collection.id); @@ -3319,7 +3319,7 @@ export class CollectionsService { // movie<->show crossover is intentional and required: a BoxSet is one // server-global container that can hold both, and this fallback exists // precisely to let a show rule find a BoxSet currently populated with - // movies only. Do not narrow this to the collection's own type — that + // movies only. Do not narrow this to the collection's own type - that // would reintroduce the bug. Matching reuses matchCollectionInLibrary so // the primary and fallback share one comparison; the name match only // bootstraps the first link, after which the stored mediaServerId is used. diff --git a/apps/server/src/modules/collections/entities/collection.entities.ts b/apps/server/src/modules/collections/entities/collection.entities.ts index 0db6a3d82..6f19b859f 100644 --- a/apps/server/src/modules/collections/entities/collection.entities.ts +++ b/apps/server/src/modules/collections/entities/collection.entities.ts @@ -135,7 +135,7 @@ export class Collection { // When true, Maintainerr keeps a Radarr/Sonarr tag (label = this collection's // title / rule group name) on the *arr entity for as long as the item is a - // member of this collection — applied on entry, removed on exit. + // member of this collection - applied on entry, removed on exit. @Column({ nullable: false, default: false }) tagInArr: boolean; diff --git a/apps/server/src/modules/collections/recently-handled-media.service.ts b/apps/server/src/modules/collections/recently-handled-media.service.ts index 2fb19cac5..be6bd2e43 100644 --- a/apps/server/src/modules/collections/recently-handled-media.service.ts +++ b/apps/server/src/modules/collections/recently-handled-media.service.ts @@ -11,7 +11,7 @@ import { OnEvent } from '@nestjs/event-emitter'; * item from the collection. The rule executor then re-evaluates the same * conditions seconds later. Conditions like "watched at all" or * "lastViewedAt before N days" stay true after the action, so the item is - * re-added and a `Media Added` notification fires — confusing users who + * re-added and a `Media Added` notification fires - confusing users who * just received a `Media Removed` event for the same title. * * Lifecycle: @@ -26,7 +26,7 @@ import { OnEvent } from '@nestjs/event-emitter'; * * State is in-memory and per-collection. A process restart wipes it, so * one re-add/notification can slip through after a restart until the - * handler runs again — acceptable. Because the rule executor consumes + * handler runs again - acceptable. Because the rule executor consumes * each collection's marks on its next pass, the structure stays bounded * by what the handler produces between two consecutive rule passes. */ diff --git a/apps/server/src/modules/collections/sort-utils.spec.ts b/apps/server/src/modules/collections/sort-utils.spec.ts index 96c0c68ca..0e1bf0f17 100644 --- a/apps/server/src/modules/collections/sort-utils.spec.ts +++ b/apps/server/src/modules/collections/sort-utils.spec.ts @@ -88,7 +88,7 @@ describe('compareMediaItemsBySort tiebreakers', () => { }); it('does not apply a title tiebreaker to status sorts (manual/excluded)', () => { - // Status sorts intentionally only partition — incoming order must be + // Status sorts intentionally only partition - incoming order must be // preserved within each partition for stable filter UX. const items: MediaItem[] = [ item({ title: 'C', maintainerrIsManual: true }), @@ -120,7 +120,7 @@ describe('compareMediaItemsBySort show-aware title ordering', () => { it('groups episodes from the same show together when sorting by title', () => { // Episode titles are deliberately interleaved across shows so that an - // episode-title-only sort would yield Aurora, Bravo, Comet, Delta — + // episode-title-only sort would yield Aurora, Bravo, Comet, Delta - // which interleaves Show Alpha and Show Beta episodes. The show-aware // comparator must instead group all of Show Alpha first. const items: MediaItem[] = [ @@ -424,7 +424,7 @@ describe('compareMediaItemsBySort missing values', () => { it('treats viewCount === 0 as a real value, only undefined trails to the end', () => { // Distinguishes "watched zero times" (a real data point) from - // "watch count not reported" — pre-fix both collapsed to 0. + // "watch count not reported" - pre-fix both collapsed to 0. const items: MediaItem[] = [ item({ title: 'Unknown', viewCount: undefined }), item({ title: 'Watched', viewCount: 3 }), diff --git a/apps/server/src/modules/metadata/metadata.service.spec.ts b/apps/server/src/modules/metadata/metadata.service.spec.ts index 0002bd109..00271b5e1 100644 --- a/apps/server/src/modules/metadata/metadata.service.spec.ts +++ b/apps/server/src/modules/metadata/metadata.service.spec.ts @@ -426,7 +426,7 @@ describe('MetadataService', () => { if (id === 'movie-1') { return movieItem; } - // The id-less container — must never be fetched for a movie. + // The id-less container - must never be fetched for a movie. return createMediaItem({ id: 'container-1', type: 'movie', @@ -740,7 +740,7 @@ describe('MetadataService', () => { }); // Re-scan mixup: a newer library item wrongly tagged with an older entry's - // id. Titles are literally identical — only the year distinguishes them. + // id. Titles are literally identical - only the year distinguishes them. // This is the case a title-first policy would silently accept. it('rejects a rescan id mixup where titles match exactly but years differ', async () => { const libraryItem = createMediaItem({ @@ -1071,7 +1071,7 @@ describe('MetadataService', () => { title: 'Sample Series', type: 'tv', externalIds: { type: 'tv', tvdb: 1 }, - // No `ended` from TVDB — say its status was 'Unknown'. + // No `ended` from TVDB - say its status was 'Unknown'. ended: undefined, firstAirDate: '2017-04-25', seasonCount: 4, diff --git a/apps/server/src/modules/metadata/metadata.service.ts b/apps/server/src/modules/metadata/metadata.service.ts index 678966e92..d713e62fd 100644 --- a/apps/server/src/modules/metadata/metadata.service.ts +++ b/apps/server/src/modules/metadata/metadata.service.ts @@ -92,7 +92,7 @@ export class MetadataService { /** * Validates policy provider keys against all registered providers (not just - * available ones). An unavailable provider key like 'tvdb' is still valid — + * available ones). An unavailable provider key like 'tvdb' is still valid - * it just means the provider isn't configured right now. The "unsupported" * warning only fires for completely unknown keys (e.g. a typo). */ @@ -371,7 +371,7 @@ export class MetadataService { // Only episodes/seasons resolve their ids from a parent (up to the show). // A movie's (or show's) own provider ids live on the item itself, and on // Emby/Jellyfin a movie's parentId points at an id-less library/container - // folder — walking up there discards the real ids and the lookup fails + // folder - walking up there discards the real ids and the lookup fails // (#3065). Switch on item.type, never on parentId presence. if (item.type !== 'season' && item.type !== 'episode') { return item; @@ -516,7 +516,7 @@ export class MetadataService { * Returns metadata details for the given IDs. * * Default behavior (no options): walks providers in preference order and - * returns the first non-undefined record — the fast path that existing + * returns the first non-undefined record - the fast path that existing * callers (poster/backdrop lookups, ID resolution) rely on. * * `{ merge: true }`: walks every available provider and fills any optional @@ -634,7 +634,7 @@ export class MetadataService { continue; } - // Same-provider id is a redirect — authoritative, no lookup needed. + // Same-provider id is a redirect - authoritative, no lookup needed. const isRedirect = provider === sourceProvider; // Don't corroborate against an unconfigured provider: keep the id silently @@ -940,7 +940,7 @@ export class MetadataService { * configured providers in preference order. Each provider is asked for * details about whatever direct ID it can extract from the item, and * the first provider whose release year matches the media server's year - * "vouches" for the ID set — we return its details and use its external + * "vouches" for the ID set - we return its details and use its external * IDs to fill in the other provider slots. * * This is the ID-primary / year-sanity model: @@ -951,7 +951,7 @@ export class MetadataService { * form, edition suffixes) is normal and comparing them caused the * regressions in #2636 / #2638. * - Cross-provider fallback gives the library a second opinion when - * the preferred provider disagrees with the media server on year — + * the preferred provider disagrees with the media server on year - * the scenario the metadata settings description already promises * users when they configure TVDB alongside TMDB. * @@ -996,13 +996,13 @@ export class MetadataService { // in untagged libraries and stays at debug. if (providerDetails.year === undefined) { this.logger.warn( - `Accepted direct provider IDs for "${item.title}" via ${provider.name} without a year check — ${provider.name} returned no release year for this entry.`, + `Accepted direct provider IDs for "${item.title}" via ${provider.name} without a year check - ${provider.name} returned no release year for this entry.`, ); return providerDetails; } if (itemYear === undefined) { this.logger.debug( - `Accepted direct provider IDs for "${item.title}" via ${provider.name} without a year check — media server item has no year.`, + `Accepted direct provider IDs for "${item.title}" via ${provider.name} without a year check - media server item has no year.`, ); return providerDetails; } @@ -1054,7 +1054,7 @@ export class MetadataService { } // Two providers agreeing on a year the media server disputes ⇒ the media - // server is the outlier. Accept rather than reject — we can't write its year + // server is the outlier. Accept rather than reject - we can't write its year // back anyway, so rejecting would only block the rule. const agreement = this.findProviderYearAgreement(disagreements); if (agreement) { diff --git a/apps/server/src/modules/metadata/providers/tvdb-metadata.provider.spec.ts b/apps/server/src/modules/metadata/providers/tvdb-metadata.provider.spec.ts index 690872862..99786829f 100644 --- a/apps/server/src/modules/metadata/providers/tvdb-metadata.provider.spec.ts +++ b/apps/server/src/modules/metadata/providers/tvdb-metadata.provider.spec.ts @@ -28,11 +28,11 @@ describe('TvdbMetadataProvider', () => { year: '2017', defaultSeasonType: 1, seasons: [ - // Specials in the default ordering — should be filtered out. + // Specials in the default ordering - should be filtered out. { number: 0, type: { id: 1 } }, { number: 1, type: { id: 1 } }, { number: 2, type: { id: 1 } }, - // Alternative orderings — should be filtered out by defaultSeasonType. + // Alternative orderings - should be filtered out by defaultSeasonType. { number: 1, type: { id: 2 } }, { number: 2, type: { id: 2 } }, { number: 3, type: { id: 3 } }, @@ -64,7 +64,7 @@ describe('TvdbMetadataProvider', () => { const details = await provider.getDetails(322399, 'tv'); - // Only number > 0 in the default ordering (type.id === 1) — 2 seasons. + // Only number > 0 in the default ordering (type.id === 1) - 2 seasons. expect(details?.seasonCount).toBe(2); }); diff --git a/apps/server/src/modules/notifications/agents/webhookUrl.ts b/apps/server/src/modules/notifications/agents/webhookUrl.ts index 20c192db3..125d9f422 100644 --- a/apps/server/src/modules/notifications/agents/webhookUrl.ts +++ b/apps/server/src/modules/notifications/agents/webhookUrl.ts @@ -14,9 +14,9 @@ export interface WebhookUrlValidation { * notification agent into the way the server leaves the network. On success the * normalised URL string is returned for posting. * - * Shared by every agent that posts to an operator-supplied URL — the + * Shared by every agent that posts to an operator-supplied URL - the * `webhookUrl` agents (webhook, slack, lunasea, discord) and ntfy's server - * `url` — so the guard stays in one place. + * `url` - so the guard stays in one place. */ export function validateWebhookUrl( url: string | undefined, diff --git a/apps/server/src/modules/notifications/notifications.service.ts b/apps/server/src/modules/notifications/notifications.service.ts index 7c3c9afd8..3456e8ac2 100644 --- a/apps/server/src/modules/notifications/notifications.service.ts +++ b/apps/server/src/modules/notifications/notifications.service.ts @@ -863,7 +863,7 @@ export class NotificationService implements OnModuleInit { dayAmount?: number, ): Promise { // Collection name and day count are plain string substitutions that don't - // need the media server — resolve them up front so an unavailable media + // need the media server - resolve them up front so an unavailable media // server (which only affects the media-title lookups below) can't leave // their placeholders raw. Strip the collection clause entirely when there's // no collection context (e.g. an infrastructure-level failure) so we never @@ -942,7 +942,7 @@ export class NotificationService implements OnModuleInit { private getTitle(item: MediaItem): string { // Branch on the server-agnostic item type, not on parentId/grandparentId // presence. Plex leaves a movie's parent empty, but Emby/Jellyfin set - // parentId to the containing library folder — so keying off parentId + // parentId to the containing library folder - so keying off parentId // misclassified Emby movies as seasons and rendered them as // "undefined - season undefined". switch (item.type) { @@ -985,7 +985,7 @@ export class NotificationService implements OnModuleInit { private ruleQueueStatusChanged(event: RuleHandlerQueueStatusUpdatedEventDto) { const nowActive = !!event.data?.processingQueue; if (nowActive === this.batchActive) { - // Mid-batch progress update — nothing to do. + // Mid-batch progress update - nothing to do. return; } // Reset on every transition (in either direction). Clearing on the diff --git a/apps/server/src/modules/overlays/overlay-processor.service.spec.ts b/apps/server/src/modules/overlays/overlay-processor.service.spec.ts index e73545697..946c7e6d9 100644 --- a/apps/server/src/modules/overlays/overlay-processor.service.spec.ts +++ b/apps/server/src/modules/overlays/overlay-processor.service.spec.ts @@ -991,9 +991,9 @@ describe('OverlayProcessorService', () => { await service.revertCollection(42); - // Backup file must not be deleted on failure — we still need it for retry. + // Backup file must not be deleted on failure - we still need it for retry. expect(deleteSpy).not.toHaveBeenCalled(); - // State must not be cleared on failure — next run reattempts the revert. + // State must not be cleared on failure - next run reattempts the revert. expect(stateService.removeState).not.toHaveBeenCalled(); // No reverted event should be emitted because nothing was actually reverted. expect(eventEmitter.emit).not.toHaveBeenCalled(); @@ -1038,10 +1038,10 @@ describe('OverlayProcessorService', () => { await service.revertCollection(42); expect(mediaServer.itemExists).toHaveBeenCalledWith('media-1'); - // Skip the upload — Plex would close the connection mid-stream (EPIPE) + // Skip the upload - Plex would close the connection mid-stream (EPIPE) // for a deleted item. expect(provider.uploadImage).not.toHaveBeenCalled(); - // Stale state and the backup are no longer useful — clear them so we + // Stale state and the backup are no longer useful - clear them so we // don't retry forever and pin a deleted item's bitmap on disk. expect(deleteSpy).toHaveBeenCalledWith('media-1'); expect(stateService.removeState).toHaveBeenCalledWith(42, 'media-1'); diff --git a/apps/server/src/modules/overlays/overlay-processor.service.ts b/apps/server/src/modules/overlays/overlay-processor.service.ts index 9063cbc98..1f31cb036 100644 --- a/apps/server/src/modules/overlays/overlay-processor.service.ts +++ b/apps/server/src/modules/overlays/overlay-processor.service.ts @@ -159,7 +159,7 @@ export class OverlayProcessorService { // A failed existence check (network blip, 5xx, auth, or a media-server // switch in progress) leaves `exists` optimistically true so the upload - // still runs and any failure follows the existing retry path — we never + // still runs and any failure follows the existing retry path - we never // drop a backup on uncertainty. `getService()` is resolved inside the try // so its transient throws are caught here too. let exists = true; @@ -334,7 +334,7 @@ export class OverlayProcessorService { if (shouldApply) { this.logger.log( - `Applying template overlay to item ${itemId} — ${daysLeft} day(s) left`, + `Applying template overlay to item ${itemId} - ${daysLeft} day(s) left`, ); const success = await this.applyTemplateOverlay( itemId, @@ -603,7 +603,7 @@ export class OverlayProcessorService { await this.saveOriginalPoster(itemId, posterBuf); } - // Build render context — raw data; per-element formatting is done by the render service + // Build render context - raw data; per-element formatting is done by the render service const daysLeft = this.getDaysLeft(deleteDate); const context: TemplateRenderContext = { deleteDate, diff --git a/apps/server/src/modules/overlays/overlay-render.service.ts b/apps/server/src/modules/overlays/overlay-render.service.ts index 02cc25f71..55dad831a 100644 --- a/apps/server/src/modules/overlays/overlay-render.service.ts +++ b/apps/server/src/modules/overlays/overlay-render.service.ts @@ -573,7 +573,7 @@ export class OverlayRenderService { layerBuf = await this.applyOpacity(layerBuf, el.opacity); } - // Clamp layer to poster bounds – sharp.composite() throws + // Clamp layer to poster bounds - sharp.composite() throws // when a composite layer extends beyond the base image. const layerMeta = await sharp(layerBuf).metadata(); let lw = layerMeta.width ?? sw; diff --git a/apps/server/src/modules/overlays/overlay-task.service.ts b/apps/server/src/modules/overlays/overlay-task.service.ts index a3a091e5e..38c4ade0b 100644 --- a/apps/server/src/modules/overlays/overlay-task.service.ts +++ b/apps/server/src/modules/overlays/overlay-task.service.ts @@ -16,7 +16,7 @@ export class OverlayTaskService extends TaskBase { super(taskService, logger); this.logger.setContext(OverlayTaskService.name); this.name = 'Overlay Handler'; - // Default to a rarely-firing cron — will be set in onBootstrapHook + // Default to a rarely-firing cron - will be set in onBootstrapHook this.cronSchedule = '0 0 0 1 1 *'; // Once a year, Jan 1st } diff --git a/apps/server/src/modules/overlays/providers/emby-overlay.provider.ts b/apps/server/src/modules/overlays/providers/emby-overlay.provider.ts index be50c15d1..6508eb97a 100644 --- a/apps/server/src/modules/overlays/providers/emby-overlay.provider.ts +++ b/apps/server/src/modules/overlays/providers/emby-overlay.provider.ts @@ -50,7 +50,7 @@ export class EmbyOverlayProvider implements IOverlayProvider { const ep = await this.emby.findRandomEpisode(sectionKeys); if (!ep?.Id) return null; const name = ep.Name ?? ''; - const title = ep.SeriesName ? `${ep.SeriesName} — ${name}` : name; + const title = ep.SeriesName ? `${ep.SeriesName} - ${name}` : name; return { itemId: ep.Id, title }; } diff --git a/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.spec.ts b/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.spec.ts index 6a0b9bb33..45fe44471 100644 --- a/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.spec.ts +++ b/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.spec.ts @@ -81,7 +81,7 @@ describe('JellyfinOverlayProvider', () => { await expect(provider.getRandomEpisode(['lib-1'])).resolves.toEqual({ itemId: 'jf-ep', - title: 'Series Name — Episode One', + title: 'Series Name - Episode One', }); }); diff --git a/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.ts b/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.ts index b9ee0ab9e..d47fabf8d 100644 --- a/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.ts +++ b/apps/server/src/modules/overlays/providers/jellyfin-overlay.provider.ts @@ -16,7 +16,7 @@ import { IOverlayProvider } from './overlay-provider.interface'; * Reads/writes only the `Primary` image: movies and shows have their poster * there, and episodes have their still there (Jellyfin's `Thumb` is mostly * unpopulated for episodes and shows a 16:9 series banner for - * continue-watching fallback — neither is what an overlay should target). + * continue-watching fallback - neither is what an overlay should target). */ @Injectable() export class JellyfinOverlayProvider implements IOverlayProvider { @@ -54,7 +54,7 @@ export class JellyfinOverlayProvider implements IOverlayProvider { const ep = await this.jf.findRandomEpisode(sectionKeys); if (!ep?.Id) return null; const name = ep.Name ?? ''; - const title = ep.SeriesName ? `${ep.SeriesName} — ${name}` : name; + const title = ep.SeriesName ? `${ep.SeriesName} - ${name}` : name; return { itemId: ep.Id, title }; } diff --git a/apps/server/src/modules/overlays/providers/overlay-provider.factory.ts b/apps/server/src/modules/overlays/providers/overlay-provider.factory.ts index 0dbd24e22..fc10f4476 100644 --- a/apps/server/src/modules/overlays/providers/overlay-provider.factory.ts +++ b/apps/server/src/modules/overlays/providers/overlay-provider.factory.ts @@ -11,7 +11,7 @@ import { PlexOverlayProvider } from './plex-overlay.provider'; * * Delegates server-type resolution to MediaServerFactory so the inferred-type * fallback (Plex credentials present but media_server_type unset → infer Plex) - * stays a single source of truth. Returns null when no server is configured — + * stays a single source of truth. Returns null when no server is configured - * callers (processor, controller) log and skip. * * Runtime server switches are guarded at MediaServerFactory.getService(); the diff --git a/apps/server/src/modules/overlays/providers/overlay-provider.interface.ts b/apps/server/src/modules/overlays/providers/overlay-provider.interface.ts index a93a5eda0..c5e666eef 100644 --- a/apps/server/src/modules/overlays/providers/overlay-provider.interface.ts +++ b/apps/server/src/modules/overlays/providers/overlay-provider.interface.ts @@ -6,7 +6,7 @@ import { /** * Server-agnostic contract for overlay-specific media-server interactions. * - * Intentionally narrower than IMediaServerService — overlays are a feature, + * Intentionally narrower than IMediaServerService - overlays are a feature, * not a core media-server responsibility, so the I/O and editor helpers the * overlay module needs live here. The overlay processor, controller, and * editor UI depend on this interface only; each supported media server @@ -22,7 +22,7 @@ export interface IOverlayProvider { /** * Library sections suitable for the overlay editor's section picker. - * Returns only movie and show libraries — music / photos etc. never carry + * Returns only movie and show libraries - music / photos etc. never carry * overlay-worthy artwork in this feature. */ getSections(): Promise; @@ -42,8 +42,8 @@ export interface IOverlayProvider { /** * Download the artwork for `itemId`. Both Plex and Jellyfin expose the - * correct image on the item itself — poster for movies/shows, still for - * episodes — so providers don't need a kind hint. Returns null when no + * correct image on the item itself - poster for movies/shows, still for + * episodes - so providers don't need a kind hint. Returns null when no * artwork exists for the item. */ downloadImage(itemId: string): Promise; diff --git a/apps/server/src/modules/overlays/providers/overlay-provider.module.ts b/apps/server/src/modules/overlays/providers/overlay-provider.module.ts index 62d692864..31157e5bd 100644 --- a/apps/server/src/modules/overlays/providers/overlay-provider.module.ts +++ b/apps/server/src/modules/overlays/providers/overlay-provider.module.ts @@ -13,7 +13,7 @@ import { PlexOverlayProvider } from './plex-overlay.provider'; * MediaServerModule exports MediaServerFactory (used by the overlay factory * to resolve the configured server type) and JellyfinAdapterService (used by * JellyfinOverlayProvider for overlay-specific Jellyfin methods). - * PlexApiModule exports PlexApiService (used by PlexOverlayProvider — the + * PlexApiModule exports PlexApiService (used by PlexOverlayProvider - the * existing Plex overlay helpers live there unchanged). */ @Module({ diff --git a/apps/server/src/modules/overlays/providers/plex-overlay.provider.ts b/apps/server/src/modules/overlays/providers/plex-overlay.provider.ts index 477539e15..cdb61cea2 100644 --- a/apps/server/src/modules/overlays/providers/plex-overlay.provider.ts +++ b/apps/server/src/modules/overlays/providers/plex-overlay.provider.ts @@ -9,7 +9,7 @@ import { IOverlayProvider } from './overlay-provider.interface'; /** * Plex implementation of IOverlayProvider. * - * Pure delegation over existing PlexApiService helpers — every Plex-specific + * Pure delegation over existing PlexApiService helpers - every Plex-specific * concept (`thumb` URL, `upload://posters/` URI scheme, X-Plex-Token, * type=4 episode filter, content-addressed dedup, eventual-consistency * retry loop) stays inside PlexApiService. This class adds no Plex logic. diff --git a/apps/server/src/modules/rules/constants/constants.service.ts b/apps/server/src/modules/rules/constants/constants.service.ts index 7d8aae69d..93a8146f5 100644 --- a/apps/server/src/modules/rules/constants/constants.service.ts +++ b/apps/server/src/modules/rules/constants/constants.service.ts @@ -99,7 +99,7 @@ export class RuleConstanstService { * Translate a (null) rule value into a human-readable explanation of why * it was missing. Surfaces in the Test Media YAML output so users stop * seeing bare "null" values and can tell the field has no data for this - * item. Derived dynamically from the property's existing metadata — no + * item. Derived dynamically from the property's existing metadata - no * static table to maintain. Rules comparisons still fail closed; this is * purely diagnostic. */ @@ -184,7 +184,7 @@ export class RuleConstanstService { let value: string; // The encoder writes the RuleType humanName (e.g. TEXT_LIST -> "text list"), - // so normalise spaces to underscores before matching — otherwise "TEXT LIST" + // so normalise spaces to underscores before matching - otherwise "TEXT LIST" // misses the 'TEXT_LIST' case, leaving ruleType undefined and throwing on the // return's .toString(), which fails the whole YAML import. switch (identifier.type.toUpperCase().split(' ').join('_')) { diff --git a/apps/server/src/modules/rules/constants/rules.constants.ts b/apps/server/src/modules/rules/constants/rules.constants.ts index 759f2a8a1..598b7ffe2 100644 --- a/apps/server/src/modules/rules/constants/rules.constants.ts +++ b/apps/server/src/modules/rules/constants/rules.constants.ts @@ -1300,7 +1300,7 @@ export class RuleConstants { type: RuleType.NUMBER, showType: ['episode'], }, - // Rating properties — sourced from Jellyfin's CommunityRating and CriticRating. + // Rating properties - sourced from Jellyfin's CommunityRating and CriticRating. // CommunityRating is provider-dependent, commonly TMDb and sometimes IMDb. // CriticRating is typically the Rotten Tomatoes Tomatometer via OMDb. // IDs match Plex so rules migrate without property ID remapping. @@ -1415,7 +1415,7 @@ export class RuleConstants { // configured and Jellyfin is the active server (see RulesService). // // A Streamystats "watchlist" is a user-created curated list, and only - // PUBLIC lists are reachable with Maintainerr's Jellyfin API key — see + // PUBLIC lists are reachable with Maintainerr's Jellyfin API key - see // the StreamystatsWatchlistMembership contract for why. These properties // act as a "users curated this" protection signal. id: Application.STREAMYSTATS, @@ -1439,7 +1439,7 @@ export class RuleConstants { // Parent-inclusive variants: a Streamystats list holds the show item ID, // not its seasons/episodes, so the item-only props above never match a // watchlisted show when evaluated below show level. These roll the - // parent show (and season) in. Show-only and season/episode-only — a + // parent show (and season) in. Show-only and season/episode-only - a // show is the top level (no parent) and a movie has no parent show. { id: 2, diff --git a/apps/server/src/modules/rules/getter/emby-getter.service.ts b/apps/server/src/modules/rules/getter/emby-getter.service.ts index 42a9e3c25..a21dd0600 100644 --- a/apps/server/src/modules/rules/getter/emby-getter.service.ts +++ b/apps/server/src/modules/rules/getter/emby-getter.service.ts @@ -25,7 +25,7 @@ import { * Implements property getters for Emby media server. * Mirrors PlexGetterService functionality for Emby. Emby and Jellyfin share * the same .NET BoxSet backend, so the quirks below are inherited from that - * lineage and largely match the Jellyfin getter — they're not Emby-specific. + * lineage and largely match the Jellyfin getter - they're not Emby-specific. * * Key differences from Plex: * - Watch history requires iterating over all users (no central endpoint) @@ -290,7 +290,7 @@ export class EmbyGetterService { } // At season/show level this returns the UNION of users that watched - // any descendant episode — not the intersection. A user who watched + // any descendant episode - not the intersection. A user who watched // 3/6 episodes is included. This is the documented behaviour and is // covered by the #2559 regression test in // jellyfin-getter.service.spec.ts. Use `sw_allEpisodesSeenBy` when @@ -421,7 +421,7 @@ export class EmbyGetterService { return communityRating?.value ?? null; } - // Smart collection properties — Emby has no native smart collections + // Smart collection properties - Emby has no native smart collections // (TheMovieDb-driven "Automatic Creation of Collections" is metadata // grouping, not filter rules; the third-party Smart Playlists plugin // is out of scope here). Fall back to normal collection count/names, @@ -604,7 +604,7 @@ export class EmbyGetterService { * show or season, or null when nothing has been watched. Emby does not * expose a watched timestamp on the parent item, so the only way to derive * a "last watched" signal for shows/seasons is to walk the children and - * take the max. This is an aggregate — it is not the view date of the + * take the max. This is an aggregate - it is not the view date of the * highest-numbered episode, the way the Plex/Tautulli `sw_lastWatched` * getters compute it. Used by the `lastViewedAt` rule only. */ diff --git a/apps/server/src/modules/rules/getter/getter-property-coverage.spec.ts b/apps/server/src/modules/rules/getter/getter-property-coverage.spec.ts index a6274263e..962e125db 100644 --- a/apps/server/src/modules/rules/getter/getter-property-coverage.spec.ts +++ b/apps/server/src/modules/rules/getter/getter-property-coverage.spec.ts @@ -20,12 +20,12 @@ import { TautulliGetterService } from './tautulli-getter.service'; // Purpose: prove that EVERY rule property (all ~166 across all applications) // has a getter path that resolves to a RuleValueType without throwing, and // that the result is independent of the TypeORM version. Run on both 0.3.x and -// 1.0.x with the same code and diff the emitted JSON (GETTER_MATRIX_OUT) — it +// 1.0.x with the same code and diff the emitted JSON (GETTER_MATRIX_OUT) - it // must be byte-identical, since getters derive values from (mocked) API // responses in memory, not from the database. // // Deterministic by construction: dependencies are auto-mocked (return -// undefined) and the input media item is a fixed literal — no faker, no clock. +// undefined) and the input media item is a fixed literal - no faker, no clock. const libItem: MediaItem = { id: 'fixed-item-1', @@ -122,7 +122,7 @@ const apps: Array<{ ]; // Deterministic, version-independent summary of a getter result (no raw values -// like random ids/dates — we record shape, which is what must stay stable). +// like random ids/dates - we record shape, which is what must stay stable). const describeResult = (v: unknown): string => { if (v === undefined) return 'undefined'; if (v === null) return 'null'; diff --git a/apps/server/src/modules/rules/getter/getter.service.ts b/apps/server/src/modules/rules/getter/getter.service.ts index 2742c0e69..acb7c24c3 100644 --- a/apps/server/src/modules/rules/getter/getter.service.ts +++ b/apps/server/src/modules/rules/getter/getter.service.ts @@ -44,7 +44,7 @@ export class ValueGetterService { switch (val1) { // Route Plex/Jellyfin/Emby Application IDs to the configured media // server's getter. This handles community rules that reference the - // "wrong" server type — e.g. a rule authored with Application.JELLYFIN + // "wrong" server type - e.g. a rule authored with Application.JELLYFIN // can still evaluate against a configured Emby server. case Application.PLEX: case Application.JELLYFIN: diff --git a/apps/server/src/modules/rules/getter/jellyfin-getter.service.ts b/apps/server/src/modules/rules/getter/jellyfin-getter.service.ts index 0e0a0d2e2..b7c4602a5 100644 --- a/apps/server/src/modules/rules/getter/jellyfin-getter.service.ts +++ b/apps/server/src/modules/rules/getter/jellyfin-getter.service.ts @@ -291,7 +291,7 @@ export class JellyfinGetterService { } // At season/show level this returns the UNION of users that watched - // any descendant episode — not the intersection. A user who watched + // any descendant episode - not the intersection. A user who watched // 3/6 episodes is included. This is the documented behaviour and is // covered by the #2559 regression test in // jellyfin-getter.service.spec.ts. Use `sw_allEpisodesSeenBy` when @@ -603,7 +603,7 @@ export class JellyfinGetterService { * show or season, or null when nothing has been watched. Jellyfin does not * expose a watched timestamp on the parent item, so the only way to derive * a "last watched" signal for shows/seasons is to walk the children and - * take the max. This is an aggregate — it is not the view date of the + * take the max. This is an aggregate - it is not the view date of the * highest-numbered episode, the way the Plex/Tautulli `sw_lastWatched` * getters compute it. Used by the `lastViewedAt` rule only. */ diff --git a/apps/server/src/modules/rules/getter/plex-getter.service.spec.ts b/apps/server/src/modules/rules/getter/plex-getter.service.spec.ts index 97a2e0328..da20e0ed3 100644 --- a/apps/server/src/modules/rules/getter/plex-getter.service.spec.ts +++ b/apps/server/src/modules/rules/getter/plex-getter.service.spec.ts @@ -937,7 +937,7 @@ describe('PlexGetterService', () => { // Pre-refactor behaviour (#1630): dedupe runs on the RAW tag, so the // exact-equal 'Space Saga' from season, show and the smart collection - // collapse to one — but the episode's ' Space Saga ' (whitespace variant) + // collapse to one - but the episode's ' Space Saga ' (whitespace variant) // survives the raw dedupe and only trims afterwards, leaving two entries. expect(result).toEqual(['Space Saga', 'Space Saga', 'Season Set']); }); @@ -1140,7 +1140,7 @@ describe('PlexGetterService', () => { return [makeWatchEntry({ viewedAt: 1_700_000_000, accountID: 1 })]; } if (rk === 'sibling-a') { - // A non-admin user watched this sibling — only visible via history. + // A non-admin user watched this sibling - only visible via history. return [makeWatchEntry({ viewedAt: 1_710_000_000, accountID: 2 })]; } return []; diff --git a/apps/server/src/modules/rules/getter/plex-getter.service.ts b/apps/server/src/modules/rules/getter/plex-getter.service.ts index 959500c69..8d1226be2 100644 --- a/apps/server/src/modules/rules/getter/plex-getter.service.ts +++ b/apps/server/src/modules/rules/getter/plex-getter.service.ts @@ -306,7 +306,7 @@ export class PlexGetterService { season.ratingKey, ); for (const episode of episodes) { - // Errors propagate to the outer catch — silently treating a + // Errors propagate to the outer catch - silently treating a // failed lookup as "no viewers" would drop genuine viewers from // `allViewers` and mark the show as unwatched-by-everyone. const viewers = await this.plexApi.getWatchHistory( @@ -342,7 +342,7 @@ export class PlexGetterService { return []; } // At season/show level this returns the UNION of users that watched - // any descendant episode — not the intersection. Plex's per-show + // any descendant episode - not the intersection. Plex's per-show // watch history aggregates child views, so any account that watched // at least one episode appears here. Use `sw_allEpisodesSeenBy` when // you need "watched every episode" semantics instead. @@ -820,8 +820,8 @@ export class PlexGetterService { // collection with this item, so one recently-watched sibling keeps // the whole set out of the delete pool. // - // We use getWatchHistory (/status/sessions/history/all) — not the - // per-child lastViewedAt field — because library metadata is scoped + // We use getWatchHistory (/status/sessions/history/all) - not the + // per-child lastViewedAt field - because library metadata is scoped // to the calling account (admin-only), while the history endpoint // returns every user's entries when called with an admin token. // Same pattern as the existing lastViewedAt rule (prop id 7). diff --git a/apps/server/src/modules/rules/getter/radarr-getter.service.ts b/apps/server/src/modules/rules/getter/radarr-getter.service.ts index 7ef072783..ab2a60bcd 100644 --- a/apps/server/src/modules/rules/getter/radarr-getter.service.ts +++ b/apps/server/src/modules/rules/getter/radarr-getter.service.ts @@ -102,7 +102,7 @@ export class RadarrGetterService { if (movieResponse === undefined) { // The Radarr lookup itself failed (or every candidate's lookup - // returned undefined) — could be a transient outage. Fail closed + // returned undefined) - could be a transient outage. Fail closed // rather than returning null (definitive absence), which would drop // the item from the collection on a transient blip. (#3125) return undefined; diff --git a/apps/server/src/modules/rules/getter/seerr-getter.service.spec.ts b/apps/server/src/modules/rules/getter/seerr-getter.service.spec.ts index 03fd3d73a..7a6109d38 100644 --- a/apps/server/src/modules/rules/getter/seerr-getter.service.spec.ts +++ b/apps/server/src/modules/rules/getter/seerr-getter.service.spec.ts @@ -570,7 +570,7 @@ describe('SeerrGetterService', () => { const { service, seerrApi } = createService(); // SeerrApiService.buildRequestIndex normalises each title's list to - // oldest-first, so requestDate is the first (earliest) request — matching + // oldest-first, so requestDate is the first (earliest) request - matching // the pre-#3152 getMovie ordering, not the newest re-request (#3152). seerrApi.getRequestsForMedia.mockResolvedValue([ movieRequest({ id: 1, createdAt: '2026-04-01' }), @@ -720,7 +720,7 @@ describe('SeerrGetterService', () => { }); }); - describe('releaseDate (property id=2) — per-item fallback', () => { + describe('releaseDate (property id=2) - per-item fallback', () => { it('should resolve movie releaseDate via getMovie, not the request index', async () => { const { service, seerrApi } = createService(); diff --git a/apps/server/src/modules/rules/getter/seerr-getter.service.ts b/apps/server/src/modules/rules/getter/seerr-getter.service.ts index 23806a025..6705cf803 100644 --- a/apps/server/src/modules/rules/getter/seerr-getter.service.ts +++ b/apps/server/src/modules/rules/getter/seerr-getter.service.ts @@ -70,7 +70,7 @@ export class SeerrGetterService { // releaseDate (movie releaseDate / tv firstAirDate / season|episode // airDate) is not carried by the /request list endpoint, so it keeps the - // per-item getMovie/getShow/getSeason fallback. Accepted limitation — + // per-item getMovie/getShow/getSeason fallback. Accepted limitation - // releaseDate rules were not part of #3152. if (prop?.name === 'releaseDate') { return await this.getReleaseDate( @@ -83,7 +83,7 @@ export class SeerrGetterService { // Every other Seerr property derives from the request set. Read the // run-scoped request index (one bulk /request sweep, deduped + cached) - // instead of a per-item getMovie/getShow — the per-item path rate-limited + // instead of a per-item getMovie/getShow - the per-item path rate-limited // under whole-library runs and silently degraded matches to near-zero // (#3152). const requestsForMedia = await this.seerrApi.getRequestsForMedia(tmdbId); @@ -97,7 +97,7 @@ export class SeerrGetterService { // Reconstruct the per-title view the property logic expects. When the // title has no request the synthetic mediaInfo carries an empty request // list, so the switch yields the definitive "not requested" values - // (0 / [] / null) — the core #3152 fix (these items previously + // (0 / [] / null) - the core #3152 fix (these items previously // rate-limited to null and were skipped). const mediaResponse = this.toMediaResponse(requestsForMedia); const tvMediaResponse = @@ -238,7 +238,7 @@ export class SeerrGetterService { /** * Resolves the Seerr release/air date via the per-item getMovie/getShow/ * getSeason endpoints. The bulk /request index carries request data, not the - * TMDB release/air dates, so this property keeps the per-item fallback — + * TMDB release/air dates, so this property keeps the per-item fallback - * releaseDate-seeded rules were not part of #3152. A communication failure * returns `undefined` (transient skip, mirrors #3125); an untracked title * (no mediaInfo) returns `null`, preserving the prior behavior. diff --git a/apps/server/src/modules/rules/getter/sonarr-getter.service.spec.ts b/apps/server/src/modules/rules/getter/sonarr-getter.service.spec.ts index 767374528..aa7704098 100644 --- a/apps/server/src/modules/rules/getter/sonarr-getter.service.spec.ts +++ b/apps/server/src/modules/rules/getter/sonarr-getter.service.spec.ts @@ -250,7 +250,7 @@ describe('SonarrGetterService', () => { // concurrently, all sharing ONE memoized `showResponse.seasons` array via the // run-scoped ArrLookupCache. The latest-aired-season scan must not mutate that // shared array, or evaluating one season corrupts the answer for the others. - // (Test Media passes no cache, so it never hit this — hence the run/test split.) + // (Test Media passes no cache, so it never hit this - hence the run/test split.) describe('shared ArrLookupCache across show seasons (#3153)', () => { it.each([ { type: 'season', title: 'SEASONS' }, @@ -1068,7 +1068,7 @@ describe('SonarrGetterService', () => { const response = await callGet(7); - // Returns undefined (comparator skips) — must NOT serve metadata's + // Returns undefined (comparator skips) - must NOT serve metadata's // 'ended: true' while Sonarr is unreachable, since that would change // collection membership during an outage. expect(response).toBeUndefined(); diff --git a/apps/server/src/modules/rules/getter/sonarr-getter.service.ts b/apps/server/src/modules/rules/getter/sonarr-getter.service.ts index 74d50c3e1..357c12573 100644 --- a/apps/server/src/modules/rules/getter/sonarr-getter.service.ts +++ b/apps/server/src/modules/rules/getter/sonarr-getter.service.ts @@ -116,7 +116,7 @@ export class SonarrGetterService { // The series lookup is keyed on the resolved tvdbId and is identical for // every episode/season of a show. The API call stays uncached (the - // cleanup needs post-deletion truth — #2757/#2891), but during rule + // cleanup needs post-deletion truth - #2757/#2891), but during rule // evaluation we dedupe it through the run-scoped memo, which is gone // before any deletion runs. Evict on a failed (undefined) lookup so a // transient error doesn't mark the whole series unresolved for the run. @@ -141,7 +141,7 @@ export class SonarrGetterService { if (showResponse === undefined) { // The Sonarr lookup itself failed (or every candidate's lookup - // returned undefined) — could be a transient outage. Fail closed + // returned undefined) - could be a transient outage. Fail closed // rather than substituting metadata-provider values, which would // silently change rule evaluation while Sonarr is down. return undefined; @@ -549,7 +549,7 @@ export class SonarrGetterService { // similar: `status` (Sonarr lowercase enum vs provider free-form strings), // `originalLanguage` (full name vs ISO 639-1 vs ISO 639-2/B), and `rating` // (different scales / aggregations). Sonarr-only state (monitored, tags, - // filePath, diskSize, …) is also absent — providers can't supply it. + // filePath, diskSize, …) is also absent - providers can't supply it. private static readonly METADATA_FALLBACK_SUPPORTED = new Set([ 'ended', 'firstAirDate', diff --git a/apps/server/src/modules/rules/getter/streamystats-getter.service.spec.ts b/apps/server/src/modules/rules/getter/streamystats-getter.service.spec.ts index 9b0567236..f87519ffb 100644 --- a/apps/server/src/modules/rules/getter/streamystats-getter.service.spec.ts +++ b/apps/server/src/modules/rules/getter/streamystats-getter.service.spec.ts @@ -20,7 +20,7 @@ const membershipOf = ( describe('StreamystatsGetterService', () => { const createService = ( users: { id: string; name: string }[] = [], - // Items resolvable by getMetadata — the `_including_parent` props look up + // Items resolvable by getMetadata - the `_including_parent` props look up // the item's parent chain through the media server's metadata path. items: ReturnType[] = [], ) => { @@ -115,7 +115,7 @@ describe('StreamystatsGetterService', () => { it('returns undefined (transient skip) when the user lookup fails closed', async () => { // getUsers() returns [] on failure; with owners present that is a lookup - // failure, not "nobody owns it" — must skip, never an empty list. + // failure, not "nobody owns it" - must skip, never an empty list. const { service, streamystatsApi } = createService([]); const libItem = createMediaItem({ type: 'movie', id: 'item-1' }); streamystatsApi.getWatchlistMembership.mockResolvedValue( @@ -193,7 +193,7 @@ describe('StreamystatsGetterService', () => { }); it('skips (undefined) when the item metadata cannot be fetched', async () => { - // getMetadata returns undefined (item not registered) — the parent chain + // getMetadata returns undefined (item not registered) - the parent chain // is unknown, so skip rather than fall back to an item-only check. const season = createMediaItem({ type: 'season', @@ -225,7 +225,7 @@ describe('StreamystatsGetterService', () => { ); expect(await service.get(IS_IN_WATCHLIST_PROP_ID, season)).toBe(false); - // The base prop is item-only — no parent resolution needed. + // The base prop is item-only - no parent resolution needed. expect(getMetadata).not.toHaveBeenCalled(); }); }); diff --git a/apps/server/src/modules/rules/getter/streamystats-getter.service.ts b/apps/server/src/modules/rules/getter/streamystats-getter.service.ts index 06a398d7c..36354a343 100644 --- a/apps/server/src/modules/rules/getter/streamystats-getter.service.ts +++ b/apps/server/src/modules/rules/getter/streamystats-getter.service.ts @@ -16,7 +16,7 @@ import { definedUniqueValues } from '../helpers/rule-property.helper'; * contributes its own rule Application. Properties surface membership of * Streamystats watchlists (a "users curated this" protection signal). * - * Only PUBLIC watchlists are visible to Maintainerr — see + * Only PUBLIC watchlists are visible to Maintainerr - see * StreamystatsWatchlistMembership for why. Usernames are resolved through the * server-agnostic media-server abstraction (Jellyfin is the only configured * server when this getter runs, since the Application is gated to it). @@ -52,7 +52,7 @@ export class StreamystatsGetterService { } const itemIds = await this.resolveWatchlistItemIds(prop.name, libItem); - // `undefined` when the parent chain couldn't be resolved — skip rather + // `undefined` when the parent chain couldn't be resolved - skip rather // than fall back to an item-only check, which would defeat the parent // variant and could let a protected item match a destructive rule. if (!itemIds) { @@ -95,13 +95,13 @@ export class StreamystatsGetterService { /** * The Jellyfin item IDs to check for watchlist membership. The base props * check the item alone. The `_including_parent` variants additionally roll - * in the parent show (and season) for a season/episode — a Streamystats list + * in the parent show (and season) for a season/episode - a Streamystats list * holds the show item ID, not its seasons, so a season would otherwise never * inherit its watchlisted show. * * Parents are resolved through the media server's `getMetadata` (the same * canonical path the other getters use) so the parent IDs come from the - * mapper's hierarchy resolution — `SeriesId` for seasons, not the + * mapper's hierarchy resolution - `SeriesId` for seasons, not the * library-folder `parentId`. Gated on the server-agnostic `type` so a movie * is never rolled up. Returns `undefined` when metadata can't be fetched, so * the caller skips rather than falling back to an item-only check. @@ -137,7 +137,7 @@ export class StreamystatsGetterService { const users = await mediaServer.getUsers(); // getUsers() is fail-closed (returns [] on error). A public watchlist is // always owned by a user, so an empty user list while we have owners to - // resolve means the lookup failed — surface that as undefined (transient + // resolve means the lookup failed - surface that as undefined (transient // skip) rather than an empty list, which would otherwise flip negative // list comparisons and could let protected items match destructive rules. if (users.length === 0) { diff --git a/apps/server/src/modules/rules/getter/tautulli-getter.service.ts b/apps/server/src/modules/rules/getter/tautulli-getter.service.ts index 790a1113c..aab48bbb2 100644 --- a/apps/server/src/modules/rules/getter/tautulli-getter.service.ts +++ b/apps/server/src/modules/rules/getter/tautulli-getter.service.ts @@ -52,7 +52,7 @@ export class TautulliGetterService { switch (prop.name) { // At season/show level `sw_watchers` returns the UNION of users that - // watched any descendant episode — not the intersection. Tautulli's + // watched any descendant episode - not the intersection. Tautulli's // history aggregates child views via grandparent_rating_key / // parent_rating_key, so any account that watched at least one // episode appears here. Use `sw_allEpisodesSeenBy` when you need diff --git a/apps/server/src/modules/rules/helpers/arr-lookup-cache.ts b/apps/server/src/modules/rules/helpers/arr-lookup-cache.ts index 6f17e5297..08879fa06 100644 --- a/apps/server/src/modules/rules/helpers/arr-lookup-cache.ts +++ b/apps/server/src/modules/rules/helpers/arr-lookup-cache.ts @@ -5,7 +5,7 @@ * Those two lookups are deliberately uncached at the API layer: the empty-show * cleanup re-reads them straight after a deletion, and a persisted entry would * serve pre-deletion state (#2757 / #2891). #2897 de-cached them for that - * reason — but that also de-cached the rule-evaluation path, where the same + * reason - but that also de-cached the rule-evaluation path, where the same * series/movie is resolved once per item. On an episode-level rule that means * the same series is looked up thousands of times per run. * @@ -15,7 +15,7 @@ * and the cleanup still reads Sonarr/Radarr fresh. * * It is intentionally NOT used by the Tautulli / Seerr / Plex / Jellyfin / Emby - * getters — those already cache at the API layer, so routing them through here + * getters - those already cache at the API layer, so routing them through here * would just be redundant double-caching. */ export class ArrLookupCache { @@ -26,7 +26,7 @@ export class ArrLookupCache { * caller (items are evaluated in parallel batches, so the same series/movie * is commonly requested many times at once). If `evictOnFailure` reports the * resolved value as a failure, the entry is dropped so a transient error - * doesn't poison the rest of the run — later items retry instead. + * doesn't poison the rest of the run - later items retry instead. */ memoize( key: string, diff --git a/apps/server/src/modules/rules/helpers/exclusion-cascade.helper.ts b/apps/server/src/modules/rules/helpers/exclusion-cascade.helper.ts index cc2c66ad9..2d4856b2f 100644 --- a/apps/server/src/modules/rules/helpers/exclusion-cascade.helper.ts +++ b/apps/server/src/modules/rules/helpers/exclusion-cascade.helper.ts @@ -3,7 +3,7 @@ import { Exclusion } from '../entities/exclusion.entities'; /** * Pre-computed sets used to decide whether a media item is covered by any * exclusion. Cascade is driven by `exclusion.type` and `exclusion.mediaServerId` - * — the `parent` field on Exclusion records the entry point of the original + * - the `parent` field on Exclusion records the entry point of the original * exclusion request (used by bulk find/delete) and must not be used as a * cascade key for typed rows, or a single-episode exclusion would skip every * other episode of the same show (issue #2858). diff --git a/apps/server/src/modules/rules/helpers/rule-property.helper.ts b/apps/server/src/modules/rules/helpers/rule-property.helper.ts index 910cbb5b9..730cf6f13 100644 --- a/apps/server/src/modules/rules/helpers/rule-property.helper.ts +++ b/apps/server/src/modules/rules/helpers/rule-property.helper.ts @@ -16,8 +16,8 @@ export function uniqueTrimmedRulePropertyNames( names: readonly string[], ): string[] { // Behaviour-preserving extraction of the Plex smart-collection name logic - // from #1630: de-duplicate on the RAW value first — collapsing a collection - // that appears at several parent levels or as a smart collection — and trim + // from #1630: de-duplicate on the RAW value first - collapsing a collection + // that appears at several parent levels or as a smart collection - and trim // only afterwards. Because dedupe runs before trimming, names that differ // only in surrounding whitespace (or case) remain separate list entries. // That distinction is user-visible: these feed COUNT_* comparators on the diff --git a/apps/server/src/modules/rules/helpers/rule.comparator.service.ts b/apps/server/src/modules/rules/helpers/rule.comparator.service.ts index 0816424f6..24da4f21a 100644 --- a/apps/server/src/modules/rules/helpers/rule.comparator.service.ts +++ b/apps/server/src/modules/rules/helpers/rule.comparator.service.ts @@ -130,9 +130,9 @@ export class RuleComparatorService { // condition of the new section and is persisted as a string // ("0"/"1"), or null when unset. +null === 0 is true in JS, so the // bare `+operator === 0` check coerced an unset operator to AND. - // Guard against null first, then coerce — mirroring the + // Guard against null first, then coerce - mirroring the // within-section idiom (`operator != null && +operator === ...`) - // used elsewhere in this service — so an unset operator falls + // used elsewhere in this service - so an unset operator falls // through to OR while an explicit AND ("0") is still honoured. // Persisted rules are normalised to an explicit operator by // migration, so null should not reach here in practice. @@ -226,7 +226,7 @@ export class RuleComparatorService { // trigger an external lookup. Plex leaf watch history is prefetched for the // rule batch, but show/season rollups and other integrations can still make // per-item calls. These getters are pure reads and touch no comparator - // state, so we resolve them up front in bounded parallel batches — the same + // state, so we resolve them up front in bounded parallel batches - the same // chunk + Promise.all idiom used for Plex collection writes. Batching lives // only here, so the total in-flight lookups never exceed // RULE_EVALUATION_CONCURRENCY. The mutation pass below then stays strictly @@ -405,7 +405,7 @@ export class RuleComparatorService { // Key conversion off the first-value slot's *declared* type, not the // runtime value. This preserves the previous behavior for every action // (before/after/in_last/in_next plus equals/not_equals) while still - // converting custom_days when firstVal is null — see issue #2582. + // converting custom_days when firstVal is null - see issue #2582. const firstValProperty = this.ruleConstanstService .getRuleConstants() .applications.find((app) => app.id === rule.firstVal[0]) diff --git a/apps/server/src/modules/rules/helpers/yaml.service.spec.ts b/apps/server/src/modules/rules/helpers/yaml.service.spec.ts index 722c6f55d..169c08ab5 100644 --- a/apps/server/src/modules/rules/helpers/yaml.service.spec.ts +++ b/apps/server/src/modules/rules/helpers/yaml.service.spec.ts @@ -19,7 +19,7 @@ describe('RuleYamlService', () => { }); // A two-section rule whose section-1 boundary uses an explicit AND operator. - // YAML imports persist operators numerically, so AND is the number 0 — which + // YAML imports persist operators numerically, so AND is the number 0 - which // is falsy and used to be dropped on export. const twoSectionRules = (sectionOperator: RuleDto['operator']): RuleDto[] => [ { diff --git a/apps/server/src/modules/rules/rules.service.deleteRuleGroup.spec.ts b/apps/server/src/modules/rules/rules.service.deleteRuleGroup.spec.ts index 223308126..1e7fb1908 100644 --- a/apps/server/src/modules/rules/rules.service.deleteRuleGroup.spec.ts +++ b/apps/server/src/modules/rules/rules.service.deleteRuleGroup.spec.ts @@ -189,7 +189,7 @@ describe('RulesService.deleteRuleGroup', () => { }); }); - describe('Behavior A — membership tag cleanup on delete', () => { + describe('Behavior A - membership tag cleanup on delete', () => { it("strips members' *arr membership tags before deleting a tagging-enabled group", async () => { const group = { id: 42, collectionId: 100 }; const collection = { diff --git a/apps/server/src/modules/rules/rules.service.exclusion-scoping.integration.spec.ts b/apps/server/src/modules/rules/rules.service.exclusion-scoping.integration.spec.ts index c49f02474..8fad59402 100644 --- a/apps/server/src/modules/rules/rules.service.exclusion-scoping.integration.spec.ts +++ b/apps/server/src/modules/rules/rules.service.exclusion-scoping.integration.spec.ts @@ -12,13 +12,13 @@ import { RulesService } from './rules.service'; // Deterministic, real-DB integration test for cross-group exclusion scoping. // // This composes the two real pieces the rule executor uses: -// 1. RulesService.getExclusions(rulegroupId) — against a real SQLite repo +// 1. RulesService.getExclusions(rulegroupId) - against a real SQLite repo // 2. the executor's gate: `if (!isMediaItemExcluded(cascade, item)) add(item)` // (rule-executor.service.ts handleCollection) // // It proves end-to-end that an item excluded *in another group* IS added to a // group that didn't exclude it, while global exclusions still apply everywhere. -// No cron/scheduler/media-server — fully reproducible (unlike a live dev run). +// No cron/scheduler/media-server - fully reproducible (unlike a live dev run). // EntitySchema mirror of the Exclusion entity (avoids decorator/metadata setup). const ExclusionSchema = new EntitySchema({ @@ -38,7 +38,7 @@ const GROUP_B = 169; // a different group const MOVIE_X = 'movie-x'; // scoped-excluded in A only const MOVIE_G = 'movie-global'; // globally excluded -describe('Exclusion scoping (real DB) — excluded-in-A item is added in B', () => { +describe('Exclusion scoping (real DB) - excluded-in-A item is added in B', () => { let ds: DataSource; let repo: Repository; let service: RulesService; @@ -91,7 +91,7 @@ describe('Exclusion scoping (real DB) — excluded-in-A item is added in B', () return !isMediaItemExcluded(cascade, { id: itemId }); }; - it('group B (no own exclusion for MOVIE_X) ADDS it — the A-scoped exclusion does not leak', async () => { + it('group B (no own exclusion for MOVIE_X) ADDS it - the A-scoped exclusion does not leak', async () => { expect(await isAddedTo(GROUP_B, MOVIE_X)).toBe(true); }); diff --git a/apps/server/src/modules/rules/rules.service.exclusions.spec.ts b/apps/server/src/modules/rules/rules.service.exclusions.spec.ts index 6ef7ed48d..eba995d71 100644 --- a/apps/server/src/modules/rules/rules.service.exclusions.spec.ts +++ b/apps/server/src/modules/rules/rules.service.exclusions.spec.ts @@ -8,7 +8,7 @@ import { RulesService } from './rules.service'; // Regression coverage for global-exclusion handling (ruleGroupId IS NULL). // TypeORM 1.x throws on a bare `null` in a `where` clause, so these paths must // use IsNull() and must not feed a null id into a lookup. -describe('RulesService exclusions — global (null ruleGroupId) handling', () => { +describe('RulesService exclusions - global (null ruleGroupId) handling', () => { const logger = createMockLogger(); const createService = (overrides?: { @@ -95,7 +95,7 @@ describe('RulesService exclusions — global (null ruleGroupId) handling', () => expect(exclusionRepo.find).toHaveBeenNthCalledWith(1, { where: { ruleGroupId: 5 }, }); - // second call: the global exclusions — must use IsNull(), never `null` + // second call: the global exclusions - must use IsNull(), never `null` const globalCallWhere = exclusionRepo.find.mock.calls[1][0].where; expect(isNullOperator(globalCallWhere.ruleGroupId)).toBe(true); }); @@ -408,7 +408,7 @@ describe('RulesService exclusions — global (null ruleGroupId) handling', () => type: 'movie', }), delete: jest.fn().mockResolvedValue(undefined), - // another rule group still excludes this item — last-exclusion-wins + // another rule group still excludes this item - last-exclusion-wins count: jest.fn().mockResolvedValue(1), }; const ruleGroupRepository = { diff --git a/apps/server/src/modules/rules/rules.service.ts b/apps/server/src/modules/rules/rules.service.ts index 07d8b8713..e80c6678e 100644 --- a/apps/server/src/modules/rules/rules.service.ts +++ b/apps/server/src/modules/rules/rules.service.ts @@ -282,7 +282,7 @@ export class RulesService { if (group) { if (group.collectionId) { - // Behavior A: deleting a tagging group makes every member "leave" — strip + // Behavior A: deleting a tagging group makes every member "leave" - strip // their *arr membership tags first (best-effort), while the collection // media rows still exist (deleteCollection removes them next). try { @@ -500,7 +500,7 @@ export class RulesService { // Behavior A: if a tagging-enabled collection is about to have its members // wiped below (crucial-setting change) and tagInArr is being turned off in // the same save, capture the members first so the toggle reconcile can - // still untag them (otherwise the rows — and our only record of them — are + // still untag them (otherwise the rows - and our only record of them - are // gone before the reconcile runs). let preDeleteMembers: CollectionMedia[] | undefined; @@ -537,7 +537,7 @@ export class RulesService { if (dbCollection.mediaServerId) { const mediaServer = await this.getMediaServer(); try { - // Use the OLD library ID — we're cleaning up items that belonged + // Use the OLD library ID - we're cleaning up items that belonged // to the previous library, not the one the rule is moving to. await mediaServer.cleanupCollectionForLibrary( dbCollection.mediaServerId, @@ -595,7 +595,7 @@ export class RulesService { sonarrQualityProfileId: params.sonarrQualityProfileId ?? null, tagInArr: params.tagInArr ?? false, // If the collection block is left out of an update, keep the saved - // values instead of sending undefined — otherwise we'd unlink a manual + // values instead of sending undefined - otherwise we'd unlink a manual // collection or switch off Plex visibility. visibleOnRecommended: params.collection?.visibleOnRecommended ?? @@ -651,7 +651,7 @@ export class RulesService { } // Behavior A: one-time *arr membership-tag reconcile on a tagInArr toggle - // — enabling tags current members, disabling untags them (ongoing changes + // - enabling tags current members, disabling untags them (ongoing changes // are handled by the executor's per-run deltas). Best-effort; awaited so // the backfill completes before the save returns. if ( @@ -724,7 +724,7 @@ export class RulesService { // disabling untags them using the previous collection (still tagInArr=true, with // its old title) so the correct label is removed even if renamed in the same save. // `preDeleteMembers` covers the disable case where a crucial-setting change in the - // same save already wiped the rows — pass the snapshot taken before the wipe. + // same save already wiped the rows - pass the snapshot taken before the wipe. private async reconcileMembershipTagsOnToggle( previous: Collection | undefined, saved: Collection, @@ -792,7 +792,7 @@ export class RulesService { // collection; a global one resolves the single configured instance. Removal is // conservative and must run AFTER the rows are deleted: it leaves the tag in // place if any exclusion for the item survives (another rule group or a global - // one), so a still-excluded item keeps its protection — last-exclusion-wins. + // one), so a still-excluded item keeps its protection - last-exclusion-wins. private async syncExclusionTag( mode: 'add' | 'remove', item: { mediaServerId: string; type: MediaItemType | undefined }, @@ -1113,7 +1113,7 @@ export class RulesService { } // Behavior B (https://features.maintainerr.info/posts/81): opt-in removal of - // the protective *arr tag on un-exclude — this is the POST /rules/exclusion + // the protective *arr tag on un-exclude - this is the POST /rules/exclusion // remove path used by the media modal. Untag the top-level item once, after // its rows are deleted so the shared-tag guard is accurate. if (this.servarrTagService.anyExclusionUntaggingEnabled()) { diff --git a/apps/server/src/modules/rules/tasks/exclusion-corrector.service.ts b/apps/server/src/modules/rules/tasks/exclusion-corrector.service.ts index a7e2f64c1..fab3b91b1 100644 --- a/apps/server/src/modules/rules/tasks/exclusion-corrector.service.ts +++ b/apps/server/src/modules/rules/tasks/exclusion-corrector.service.ts @@ -138,7 +138,7 @@ export class ExclusionTypeCorrectorService implements OnModuleInit { } this.logger.log( - `Backfilling type for ${exclusionsWithoutType.length} exclusion(s) from media server metadata — this may take a moment on first run`, + `Backfilling type for ${exclusionsWithoutType.length} exclusion(s) from media server metadata - this may take a moment on first run`, ); const mediaServer = await this.mediaServerFactory.getService(); diff --git a/apps/server/src/modules/rules/tasks/rule-executor-job-manager.service.ts b/apps/server/src/modules/rules/tasks/rule-executor-job-manager.service.ts index b5bf4f5f1..3223f537b 100644 --- a/apps/server/src/modules/rules/tasks/rule-executor-job-manager.service.ts +++ b/apps/server/src/modules/rules/tasks/rule-executor-job-manager.service.ts @@ -76,7 +76,7 @@ export class RuleExecutorJobManagerService implements OnApplicationShutdown { } public async stopProcessing() { - // Drop any queued work – we only care about the in-flight execution + // Drop any queued work - we only care about the in-flight execution this.queue.length = 0; this.abortController?.abort(); @@ -233,7 +233,7 @@ export class RuleExecutorJobManagerService implements OnApplicationShutdown { ); // Everything between acquire and release lives inside this try so - // release() always runs — including if `emitStatusUpdate` synchronously + // release() always runs - including if `emitStatusUpdate` synchronously // throws (e.g. an event listener throws). Without this, a thrown // listener would leak the lock entry forever and every future // `tryAcquire` would return null until restart (#2799). diff --git a/apps/server/src/modules/rules/tasks/rule-executor.service.spec.ts b/apps/server/src/modules/rules/tasks/rule-executor.service.spec.ts index 8ff714b4f..ca54c484f 100644 --- a/apps/server/src/modules/rules/tasks/rule-executor.service.spec.ts +++ b/apps/server/src/modules/rules/tasks/rule-executor.service.spec.ts @@ -319,7 +319,7 @@ describe('RuleExecutorService', () => { expect(collectionService.addToCollection).not.toHaveBeenCalled(); expect(collectionService.removeFromCollection).not.toHaveBeenCalled(); expect(logger.debug).toHaveBeenCalledWith( - "Skipping media server sync for 'Test Collection' — no media server collection exists because no items currently match the rule.", + "Skipping media server sync for 'Test Collection' - no media server collection exists because no items currently match the rule.", ); }); @@ -1331,7 +1331,7 @@ describe('RuleExecutorService', () => { collectionService.getCollectionMedia.mockResolvedValue([] as any); rulesService.getExclusions.mockResolvedValue([ // parent=showId mirrors what setExclusion writes when the AddModal is - // opened from a show overview — the regression hinges on this shape. + // opened from a show overview - the regression hinges on this shape. { mediaServerId: 'ep-1', parent: 'show-1', type: 'episode' }, ] as any); @@ -1731,7 +1731,7 @@ describe('RuleExecutorService', () => { .mockResolvedValue(undefined); // Abort during the pre-evaluation cache reset, i.e. after the top-level - // abort check but before the prefetch — so only the pre-prefetch check + // abort check but before the prefetch - so only the pre-prefetch check // can stop the sweep. const abortController = new AbortController(); rulesService.resetCacheIfGroupUsesRuleThatRequiresIt.mockImplementation( diff --git a/apps/server/src/modules/rules/tasks/rule-executor.service.ts b/apps/server/src/modules/rules/tasks/rule-executor.service.ts index a0c80bb09..64818dcca 100644 --- a/apps/server/src/modules/rules/tasks/rule-executor.service.ts +++ b/apps/server/src/modules/rules/tasks/rule-executor.service.ts @@ -236,7 +236,7 @@ export class RuleExecutorService { // collection; the post-handle state would then look "already linked" and // make the sync below import that collection's existing contents as // manual. Capturing the pre-run state lets the run that *establishes* the - // link skip that import — the membership-provenance guard (#2663) + // link skip that import - the membership-provenance guard (#2663) // otherwise reads the link state too late (after handleCollection has // already linked it) and never skips. // @@ -246,7 +246,7 @@ export class RuleExecutorService { // pre-existing, rule-unselected items get tracked as manual then. We can't // distinguish "was on the BoxSet at adoption time" from "the user added it // to the BoxSet afterwards" without persisted provenance, so the guard - // only removes the noisy first-run flood — a long-lived adopted collection + // only removes the noisy first-run flood - a long-lived adopted collection // still absorbs its foreign items as manual on a later run. const collectionLinkedBeforeRun = ruleGroup.collection?.id ? Boolean( @@ -271,7 +271,7 @@ export class RuleExecutorService { // evaluation are served from an in-memory map instead of individual // HTTP requests. Reused across rule groups within a scheduler batch; // rebuilt here when the reset above flushed it. Gated on a centrally - // queryable history endpoint — Jellyfin/Emby (per-user history) keep + // queryable history endpoint - Jellyfin/Emby (per-user history) keep // their per-item path. Abort-checked first so a cancellation that // lands just before evaluation doesn't kick off a long history sweep. abortSignal.throwIfAborted(); @@ -436,7 +436,7 @@ export class RuleExecutorService { } else if (children && children.length > 0) { // When two automatic rule groups share a title they end up linked // to the same media server collection. Items rule-owned by a - // sibling collection must not be imported here as manual — that + // sibling collection must not be imported here as manual - that // would subject them to this rule's deleteAfterDays. If we cannot // determine sibling ownership (DB error), refuse to import: a // silent fallback to "no siblings" would re-introduce the @@ -625,7 +625,7 @@ export class RuleExecutorService { if (!linkedCollection.mediaServerId) { this.logger.debug( - `Skipping media server sync for '${linkedCollection.title}' — no media server collection exists because no items currently match the rule.`, + `Skipping media server sync for '${linkedCollection.title}' - no media server collection exists because no items currently match the rule.`, ); return {}; } @@ -690,7 +690,7 @@ export class RuleExecutorService { // Filter exclusions out of results. Cascade is keyed off the show/season // exclusion's own mediaServerId (via type), so a single-episode exclusion - // only skips that episode — not its siblings (issue #2858). + // only skips that episode - not its siblings (issue #2858). const desiredMediaServerIds = new Set(); for (const item of this.resultData ?? []) { @@ -756,7 +756,7 @@ export class RuleExecutorService { // Conditions like "watched" / "lastViewedAt before N days" stay true // after the handler action, so without this guard the user gets a // `Media Removed` event immediately followed by `Media Added` for - // the same title — confusing and noisy via email/Discord. + // the same title - confusing and noisy via email/Discord. // // The marks are consumed here: this pass blocks the immediate echo, // and any subsequent pass treats the items normally. If the rule diff --git a/apps/server/src/modules/rules/tests/rule.comparator.service.executeRulesWithData.spec.ts b/apps/server/src/modules/rules/tests/rule.comparator.service.executeRulesWithData.spec.ts index 0c6689890..364504b05 100644 --- a/apps/server/src/modules/rules/tests/rule.comparator.service.executeRulesWithData.spec.ts +++ b/apps/server/src/modules/rules/tests/rule.comparator.service.executeRulesWithData.spec.ts @@ -439,7 +439,7 @@ describe('RuleComparatorService.executeRulesWithData', () => { }); // Unary EXISTS/NOT_EXISTS must distinguish the getter's `null` (definitive - // absence — e.g. lastViewedAt for a never-watched item) from `undefined` + // absence - e.g. lastViewedAt for a never-watched item) from `undefined` // (outer-catch transport failure in plex/seerr-getter). Without the // tightened shouldCompare, `!hasExistsValue(undefined) === true` would // spuriously add items on every transient API blip (#1446). @@ -490,7 +490,7 @@ describe('RuleComparatorService.executeRulesWithData', () => { it('completes the run and logs a skip (not a crash) when a unary rule value is unavailable', async () => { // getCustomValueIdentifier dereferences customValue.ruleTypeId. A unary // rule (EXISTS/NOT_EXISTS) carries no customVal, so logging the skipped - // comparison must not reach this helper — otherwise the run threw + // comparison must not reach this helper - otherwise the run threw // "Cannot read properties of undefined (reading 'ruleTypeId')" and aborted. ruleConstanstService.getCustomValueIdentifier.mockImplementation( (customValue: { ruleTypeId: number; value: string }) => ({ @@ -537,7 +537,7 @@ describe('RuleComparatorService.executeRulesWithData', () => { describe('OR sections', () => { // section 0: viewCount EQUALS 0 (operator: null = OR boundary) // section 1: viewCount BIGGER 0 (operator: null = OR boundary) - // These two conditions are mutually exclusive, which proves OR semantics — + // These two conditions are mutually exclusive, which proves OR semantics - // if AND were used, no item could satisfy both simultaneously. // // Field [Application.PLEX, 5] = viewCount @@ -624,7 +624,7 @@ describe('RuleComparatorService.executeRulesWithData', () => { // section 0: viewCount EQUALS 0 // section 1: viewCount SMALLER 1 // An item with viewCount = 0 matches both sections. OR semantics must - // union the sections and dedupe, so the item appears exactly once — a + // union the sections and dedupe, so the item appears exactly once - a // regression here (e.g. pushing per matching section) would yield two. const overlappingRules = [ createStoredRule( diff --git a/apps/server/src/modules/settings/dto's/setting.dto.ts b/apps/server/src/modules/settings/dto's/setting.dto.ts index 4c1a59107..3026c45be 100644 --- a/apps/server/src/modules/settings/dto's/setting.dto.ts +++ b/apps/server/src/modules/settings/dto's/setting.dto.ts @@ -74,7 +74,7 @@ export class SettingDto { rules_handler_job_cron: string; - // *arr exclusion tagging (https://features.maintainerr.info/posts/81) — Radarr and Sonarr configured independently + // *arr exclusion tagging (https://features.maintainerr.info/posts/81) - Radarr and Sonarr configured independently radarr_tag_exclusions: boolean; radarr_exclusion_tag: string; diff --git a/apps/server/src/modules/settings/media-server-switch.service.ts b/apps/server/src/modules/settings/media-server-switch.service.ts index 3f61ab52b..e2d8a3164 100644 --- a/apps/server/src/modules/settings/media-server-switch.service.ts +++ b/apps/server/src/modules/settings/media-server-switch.service.ts @@ -122,7 +122,7 @@ export class MediaServerSwitchService { } // Initialize the now-active media server adapter once the switch lock is - // released — getService() refuses while a switch is in progress, so this + // released - getService() refuses while a switch is in progress, so this // can't run inside executeSwitchInternal. When the target's credentials // carried over (e.g. switching back to a still-configured server), this // brings the adapter up immediately so its connection test reflects reality diff --git a/apps/server/src/modules/settings/rule-migration.service.spec.ts b/apps/server/src/modules/settings/rule-migration.service.spec.ts index 3afaab4c6..486fbd7d7 100644 --- a/apps/server/src/modules/settings/rule-migration.service.spec.ts +++ b/apps/server/src/modules/settings/rule-migration.service.spec.ts @@ -523,7 +523,7 @@ describe('RuleMigrationService', () => { expect(result.skippedRules).toBe(1); const sectionOne = result.rules.filter((r) => r.section === 1); expect(sectionOne).toHaveLength(1); - // Inherits the dropped boundary's AND — does NOT keep its own OR. + // Inherits the dropped boundary's AND - does NOT keep its own OR. expect(sectionOne[0].operator).toBe(RuleOperators.AND); // Group's first rule still null. expect(result.rules[0].operator).toBeNull(); diff --git a/apps/server/src/modules/settings/rule-migration.service.ts b/apps/server/src/modules/settings/rule-migration.service.ts index 4b9b0e934..eedc6c1a7 100644 --- a/apps/server/src/modules/settings/rule-migration.service.ts +++ b/apps/server/src/modules/settings/rule-migration.service.ts @@ -18,12 +18,12 @@ import { reassertSectionBoundaryOperators } from '../rules/helpers/section-opera import { RuleGroup } from '../rules/entities/rule-group.entities'; import { Rules } from '../rules/entities/rules.entities'; -/** Singleton instance – avoids re-creating the constant data on every call. */ +/** Singleton instance - avoids re-creating the constant data on every call. */ const RULE_CONSTANTS = new RuleConstants(); /** * Single source of truth mapping each supported media server to its rule - * `Application` id. The `Record` type is exhaustive — adding + * `Application` id. The `Record` type is exhaustive - adding * a new media server to `MediaServerType` is a compile error until it is mapped * here, which keeps the migrator in step with the supported-server list. */ @@ -81,7 +81,7 @@ interface PropertyCompatibility { * property. The ID will be rewritten during migration. * - **incompatible** if none of the above applies. * - * This is fully data-driven from `rules.constants.ts` — no hardcoded lists. + * This is fully data-driven from `rules.constants.ts` - no hardcoded lists. */ function computePropertyCompatibility( sourceApp: Application, @@ -382,7 +382,7 @@ export class RuleMigrationService { } } - // Cache compatibility per source app – avoids recomputing for every rule. + // Cache compatibility per source app - avoids recomputing for every rule. const compatCache = new Map(); const getCompat = (sourceApp: Application): PropertyCompatibility => { if (!compatCache.has(sourceApp)) { @@ -452,7 +452,7 @@ export class RuleMigrationService { // rule so a dropped boundary can't silently flip the section AND<->OR. reassertSectionBoundaryOperators(result, sectionCombineOp); - // Backfill any remaining unset within-section operator to OR — the default + // Backfill any remaining unset within-section operator to OR - the default // the comparator and the NormalizeRuleSectionOperators migration both apply. // reassert only sets section-boundary operators, so a pre-explicit-operator // community rule can still carry a null within-section operator here, which @@ -500,7 +500,7 @@ export class RuleMigrationService { /** * Analyze an in-memory rule DTO to determine if it can be migrated. - * Only the `incompatible` set is checked — remapped properties are compatible + * Only the `incompatible` set is checked - remapped properties are compatible * by definition (the two sets are mutually exclusive). */ private analyzeRuleDto( diff --git a/apps/server/src/modules/settings/settings-operations.service.ts b/apps/server/src/modules/settings/settings-operations.service.ts index c23670aa2..fb37dee3c 100644 --- a/apps/server/src/modules/settings/settings-operations.service.ts +++ b/apps/server/src/modules/settings/settings-operations.service.ts @@ -64,7 +64,7 @@ export class SettingsOperationsService { } // ========================================================================== - // Read API — delegated to the passive settings store + // Read API - delegated to the passive settings store // ========================================================================== public init() { @@ -143,7 +143,7 @@ export class SettingsOperationsService { } // ========================================================================== - // Coordination — test / save / reinit flows + // Coordination - test / save / reinit flows // ========================================================================== public async addRadarrSetting( @@ -1314,7 +1314,7 @@ export class SettingsOperationsService { } const unreachableMessage = - "Couldn't reach plex.tv to verify your credentials — retrying. Your saved token is still in use."; + "Couldn't reach plex.tv to verify your credentials - retrying. Your saved token is still in use."; try { switch (await this.plexApi.validateAuthToken()) { diff --git a/apps/server/src/modules/storage-metrics/storage-metrics.controller.ts b/apps/server/src/modules/storage-metrics/storage-metrics.controller.ts index 8cdb10a40..4a63d6f45 100644 --- a/apps/server/src/modules/storage-metrics/storage-metrics.controller.ts +++ b/apps/server/src/modules/storage-metrics/storage-metrics.controller.ts @@ -27,7 +27,7 @@ export class StorageMetricsController { @Get('library-sizes') @ApiOperation({ summary: - 'Accurate per-library size computed by iterating media items. Potentially slow — call on demand.', + 'Accurate per-library size computed by iterating media items. Potentially slow - call on demand.', }) @ApiResponse({ status: 200, diff --git a/apps/server/src/modules/storage-metrics/storage-metrics.service.ts b/apps/server/src/modules/storage-metrics/storage-metrics.service.ts index 5e75a122e..c86e41fe2 100644 --- a/apps/server/src/modules/storage-metrics/storage-metrics.service.ts +++ b/apps/server/src/modules/storage-metrics/storage-metrics.service.ts @@ -285,8 +285,8 @@ export class StorageMetricsService { // Coalesce mounts that survived the per-host dedupe but clearly point at // the same shared volume from different hosts (e.g. Radarr/Sonarr running // in separate LXC containers against the same NAS). A byte-exact match on - // totalSpace plus freeSpace — or a matching volume label plus byte-exact - // totalSpace — is implausible across truly unrelated filesystems, since + // totalSpace plus freeSpace - or a matching volume label plus byte-exact + // totalSpace - is implausible across truly unrelated filesystems, since // any block-level write would diverge them. const merged = this.mergeSharedMountsAcrossHosts(seen); @@ -690,7 +690,7 @@ export class StorageMetricsService { // Group by mediaServerId so the same item across multiple collections // is counted once. Per-item sizes are identical across rows for the - // same id, so MAX is equivalent to "any" — it just deduplicates. + // same id, so MAX is equivalent to "any" - it just deduplicates. // Partitioning by collection.type assigns each unique item to its // collection's type bucket; collections are typed homogeneously, so // overlap between movie and show buckets is not a real-world concern. diff --git a/apps/server/src/modules/tasks/execution-lock.service.ts b/apps/server/src/modules/tasks/execution-lock.service.ts index 7b1b689e6..b582f0f9b 100644 --- a/apps/server/src/modules/tasks/execution-lock.service.ts +++ b/apps/server/src/modules/tasks/execution-lock.service.ts @@ -45,7 +45,7 @@ export class ExecutionLockService { // Store `current` directly so the release callback below can recognise // its own entry by reference and delete it on release. Storing the // chained promise (prior.then(() => current)) instead would leak the - // entry forever — `tryAcquire` checks `locks.has(key)` and would never + // entry forever - `tryAcquire` checks `locks.has(key)` and would never // return non-null again, which is the root cause of #2799. this.locks.set(key, current); diff --git a/apps/server/src/utils/secretMasking.ts b/apps/server/src/utils/secretMasking.ts index a767ebb2b..8aacbf3e3 100644 --- a/apps/server/src/utils/secretMasking.ts +++ b/apps/server/src/utils/secretMasking.ts @@ -205,7 +205,7 @@ const maskUrlAuthority = (authority: string): string => { } } - // Only treat as host:port when there is exactly one colon — multiple colons + // Only treat as host:port when there is exactly one colon - multiple colons // means a bare IPv6 literal (e.g. 2001:db8::1), which must not be split. const colonIdx = authority.lastIndexOf(':'); if ( diff --git a/apps/server/src/utils/sharp.ts b/apps/server/src/utils/sharp.ts index 9a078cd51..feb52c1ff 100644 --- a/apps/server/src/utils/sharp.ts +++ b/apps/server/src/utils/sharp.ts @@ -3,8 +3,8 @@ * * sharp ships prebuilt native binaries. Since libvips 8.18 (sharp 0.35) the * prebuilt Linux x64 binary requires a CPU with the x86-64-v2 microarchitecture - * (SSE4.2, POPCNT, …). On older CPUs — or minimal VM CPU models such as QEMU - * `kvm64`, which do not expose those features to the guest — sharp throws + * (SSE4.2, POPCNT, …). On older CPUs - or minimal VM CPU models such as QEMU + * `kvm64`, which do not expose those features to the guest - sharp throws * "Unsupported CPU" while loading its addon at `require()` time. * * Both consumers (overlay rendering and collection posters) import sharp at diff --git a/apps/server/test/rules-test-matrix.e2e.ts b/apps/server/test/rules-test-matrix.e2e.ts index af7ff1967..aa557ef2e 100644 --- a/apps/server/test/rules-test-matrix.e2e.ts +++ b/apps/server/test/rules-test-matrix.e2e.ts @@ -192,7 +192,7 @@ async function executeScenario(baseUrl: string, scenario: Scenario) { // Generated coverage of every RuleType x RulePossibility. The identical // generator runs on the baseline and current builds, so any per-cell diff -// signals real comparator drift — the operand values only need to reach the +// signals real comparator drift - the operand values only need to reach the // code path, not be domain-realistic. Binary ops get a value + missing-first // case; unary ops (EXISTS / NOT_EXISTS) get a present + absent case. function buildGeneratedMatrix(): Scenario[] { diff --git a/apps/ui/src/api/settings.ts b/apps/ui/src/api/settings.ts index 49ea0b228..a4c6102e9 100644 --- a/apps/ui/src/api/settings.ts +++ b/apps/ui/src/api/settings.ts @@ -69,7 +69,7 @@ export interface ISettings { collection_handler_job_cron: string rules_handler_job_cron: string metadata_provider_preference?: MetadataProviderPreference - // *arr exclusion tagging (https://features.maintainerr.info/posts/81) — Radarr and Sonarr configured independently + // *arr exclusion tagging (https://features.maintainerr.info/posts/81) - Radarr and Sonarr configured independently radarr_tag_exclusions?: boolean radarr_exclusion_tag?: string radarr_untag_on_unexclude?: boolean diff --git a/apps/ui/src/components/AddModal/index.spec.tsx b/apps/ui/src/components/AddModal/index.spec.tsx index a4241a52f..3de579e3d 100644 --- a/apps/ui/src/components/AddModal/index.spec.tsx +++ b/apps/ui/src/components/AddModal/index.spec.tsx @@ -30,7 +30,7 @@ vi.mock('../../utils/ApiHandler', () => ({ PostApiHandler: vi.fn(), })) -describe('AddModal — global exclusion warning', () => { +describe('AddModal - global exclusion warning', () => { const getApiHandlerMock = vi.mocked(GetApiHandler) const postApiHandlerMock = vi.mocked(PostApiHandler) @@ -82,14 +82,14 @@ describe('AddModal — global exclusion warning', () => { }) afterEach(() => cleanup()) - it('Add + all collections, item has scoped exclusions: warns with item — rule-group links, then Proceed submits a global exclusion', async () => { + it('Add + all collections, item has scoped exclusions: warns with item - rule-group links, then Proceed submits a global exclusion', async () => { stubApi(scopedStatus) renderExclude() fireEvent.click(await screen.findByRole('button', { name: 'Submit' })) await screen.findByText('Confirmation Required') - // each scoped exclusion is listed as "" + // each scoped exclusion is listed as " - " expect(screen.getByRole('button', { name: 'Archive Queue' })).toBeTruthy() expect(screen.getByRole('button', { name: 'Stale Movies' })).toBeTruthy() expect(screen.getAllByText(/Mock Charlie/).length).toBeGreaterThan(0) diff --git a/apps/ui/src/components/AddModal/index.tsx b/apps/ui/src/components/AddModal/index.tsx index cdd356417..91525b07f 100644 --- a/apps/ui/src/components/AddModal/index.tsx +++ b/apps/ui/src/components/AddModal/index.tsx @@ -130,7 +130,7 @@ const AddModal = (props: IAddModal) => { } // Only ADDING a global exclusion clears the item's rule-group exclusions. - // If it has any, warn and list each as "item — rule group", reusing the + // If it has any, warn and list each as "item - rule group", reusing the // backdrop's status data (no-cache fetch) so labels/links match and stay // fresh. (selectedAction 0 = Add, 1 = Remove.) if ( @@ -165,7 +165,7 @@ const AddModal = (props: IAddModal) => { return } } catch { - // Warning data unavailable — proceed without it. + // Warning data unavailable - proceed without it. } } @@ -346,7 +346,7 @@ const AddModal = (props: IAddModal) => {
    {affectedExclusions.map((e) => (
  • - {e.title} —{' '} + {e.title} -{' '}