Commit Graph
26 Commits
Author SHA1 Message Date
timothy 50cd29d841 fix(api): #265 review — quote-aware If-Match scanner, RFC OWS trim, de-BOM
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Failing after 2m30s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 4m23s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m44s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Independent review fix commit (cold fork MERGEABLE-WITH-NITS + Codex BLOCKED, 2 Highs):

- Codex H1: a comma (0x2C) is a valid etagc and can appear INSIDE a quoted opaque-tag
  ("3,5" is ONE tag). The old Split(',') broke it into two malformed tokens → 400. Replaced
  with a quote-aware position scanner that treats a comma as a separator only outside the
  quotes; "3,5" is now one valid non-canonical tag → 412.
- Codex H2: RFC 7230 OWS is SP/HTAB only. string.Trim() also strips NBSP and other Unicode
  whitespace, letting " * " masquerade as the "*" force-write escape. Trim only
  (' ', '\t'); such input is now Malformed → 400.
- Fork nit: corrected the canonical-guard comment (interior-whitespace tags are rejected by
  IsEtagc, not NumberStyles.None).
- CI Formatting gate: de-BOM the 8 touched legacy Application .cs (charset=utf-8, #311/#310).
- Tests: added comma-in-tag ("3,5", "x,y","3"), empty-element tolerance, NBSP-not-OWS,
  trailing-junk, lowercase-weak, wildcard-in-list cases. Full ErsatzTV.Tests green (1556).

Refs #253 #197
2026-07-12 23:09:36 +02:00
timothyandClaude Opus 4.8 8090e10408 fix(api): #265 — If-Match evaluates per RFC 7232 (valid-but-non-matching → 412, not 400)
The shared optimistic-concurrency parser (ConcurrencyHeaders.ParseIfMatch) classified any
non-canonical/weak/list If-Match value as Malformed → 400. Per RFC 7232 §3.1 a syntactically
-valid entity-tag that simply doesn't strong-match must be 412; 400 is only for a genuine
grammar violation.

- Rewrite ParseIfMatch as a real RFC 7232 entity-tag/list parser: walks the comma-separated
  1#entity-tag list, validates each [W/]DQUOTE *etagc DQUOTE member, and collects the strong
  members whose opaque text is our canonical decimal. Weak / empty / non-canonical /
  out-of-range tags are valid but contribute no version (→ empty set → 412); genuine grammar
  violations (unquoted, SP-in-tag, unterminated, garbage) → 400.
- Reshape IfMatchCondition.ExpectedVersion : Option<int> → ExpectedVersions : Option<Seq<int>>
  and VersionedAggregateExtensions.CheckVersion → set membership (any strong match proceeds;
  empty set always 412). Threads through 10 replace/update commands + handlers + request
  mappers + 9 controllers.
- No wire-contract change (400 + 412 already declared on every PUT; the field is header-derived
  and internal — no DTO/route/response-type/OpenAPI change).
- Tests: ConcurrencyHeadersTests rewritten for the new classification (lists, weak, empty,
  non-canonical → Version/empty-set; grammar violations → Malformed) + new
  VersionedAggregateExtensionsTests for CheckVersion membership/empty-set/force-write.
- Docs: api-conventions.md §7a rewritten; decisions.md entry appended.

Refs #253 #197
fixes #265

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:09:36 +02:00
timothyandClaude Opus 4.8 ec26e1be5b fix(api): #316 review — POST-ify graphics-elements refresh, LockedError→409, no-store machine-key
- GET /api/graphics-elements no longer side-effects; refresh moved to
  POST /api/graphics-elements/refresh (204), closing a CSRF vector on a GET.
- PrepareTroubleshootingPlaybackHandler now returns a typed LockedError from
  both atomic lock-acquire failures; ApiResults.ToErrorResult maps it to 409
  instead of falling through to 422, so a lock lost in the race between the
  controller's pre-check and the handler's atomic acquire still reports 409.
- AuthController.MachineKey sets Cache-Control: no-store + Pragma: no-cache
  on the 200 response carrying the master API key.
- Reworded the stale "subtitleId query parameter" endpoint description now
  that playback/start takes a JSON body.
- Regenerated openapi/v1.json + docs/endpoint-index.md; docs/api-conventions.md
  updated with the LockedError pattern (§3a) and the ToErrorResult table row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:50:42 +02:00
timothy 78262ea4cb Merge remote-tracking branch 'origin/main' into feat/91b-blazor-removal
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m47s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m29s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m23s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 5m48s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m8s
2026-07-11 21:17:03 +02:00
timothyandClaude Opus 4.8 4407dd53af fix(91b): delete orphaned Blazor NavigationManagerExtensions
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m47s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Cold-review Low-1: ErsatzTV/Extensions/NavigationManagerExtensions.cs survived
the removal — the last non-deleted .cs still importing
Microsoft.AspNetCore.Components/JSInterop and calling the deleted
blazorHelpers.scrollToFragment JS. Fully unreferenced (compiled only via the
shared framework). Removing it completes the Blazor deletion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:09:26 +02:00
timothyandClaude Opus 4.8 c40ffefa99 feat(253): PR3 optimistic-concurrency fan-out — Diff + Scalar aggregates
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m11s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Fans the frozen Block recipe (api-conventions §7a) across the five Diff/Scalar
replace-all endpoints, completing the #253 PR2→PR4 arc's implementable core:

- #6 Collection custom-order, #7 Playout alternate-schedules, #8 Playout templates
  (shared Playout.Version), #9 MultiCollection, #10 RerunCollection — each: pre-check
  412 as a standalone Either after validation (H2, subtype survives the Join flatten),
  unconditional Version++ (M1), guarded save, controller If-Match/ETag/400/412, SPA
  editor ETag round-trip + 412 conflict dialog.
- H1: the two Playout handlers' catch(Exception)→422 restructured so the guard's
  PreconditionFailedError returns before the catch (412, not 422).
- M2: RerunCollection/Collection refresh runs unconditionally on save; MultiCollection
  keeps its name-only→no-rebuild optimization by bumping on the first (name) save.
- H3: UpdateDefaultDecoHandler bulk-bumps Playout.Version via .SetProperty.
- Shared ConcurrencyHeaders.MalformedIfMatchProblem() for the 400 guard.
- Deferred (→ #269): same-root non-bulk sibling config writers' ETag rotation.

Tests: per-handler pre-check/bump concurrency tests (Playout ×2 incl. non-vacuous
racing-save backstop, Rerun, Multi incl. name-only-no-rebuild M2, Collection);
controller tests get a DefaultHttpContext for the header read/write. Docs:
api-conventions §7a fan-out status, spa-conventions §4a list-editor note, decisions.

Refs #253

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:23:48 +02:00
timothy 8878bf9e11 docs(review): record deferred If-Match 412-semantics refinement (#265) as an acceptable-defer
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m27s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m34s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Codex re-review of the fix commit confirmed both prior findings resolved and raised one
new Medium: RFC 7232 would 412 (not 400) a syntactically-valid but non-matching If-Match
(non-canonical "03", weak W/"3", tag lists, empty, overflow). Deferred to #197 (cold
contract pass) as #265 — fail-safe today (the mutation is rejected, never applied) and no
first-party client is affected. Records the deferral where the #253 fan-out will copy the
parser: a code comment in ConcurrencyHeaders + a note in api-conventions §7a.

Refs #253 #265
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:18:35 +02:00
timothy ee39effe0b fix(review): close client load-TOCTOU + canonicalize If-Match parse (Codex High/Medium)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m39s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Codex independent review of #263 surfaced two defects the fork review missed:

- High — client load TOCTOU: BlockEditor read root metadata (getBlock) and items+ETag
  (getBlockItemsWithMeta) concurrently, so a concurrent write landing between them (with
  the items read resolving last) left a stale root paired with a current ETag → the save
  silently overwrote the concurrent change with no 412. Fix: read items+ETag FIRST, then
  the root metadata, so the captured ETag is never newer than the root version and any
  inconsistency fails safe (save 412s → conflict dialog → reload).
- Medium — `ParseIfMatch` accepted non-canonical strong tags ("03", "+3", " 3 ") as
  version 3. An ETag is opaque; only the exact emitted form is valid. Fix: canonical
  decimal only (`NumberStyles.None` + no leading zeros) → else 400.

Tests: new `ConcurrencyHeadersTests` (canonical parse + padded/signed/whitespace/weak/
unquoted/list/overflow/empty → malformed); `ApiResultsTests` gains the 412 mapping case.
Existing BlocksScreen tests still green (load reordering is behavior-preserving for the
non-concurrent path).

Refs #253
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:13:26 +02:00
timothyandClaude Opus 4.8 94ebf34ccd feat(api): optimistic-concurrency contract for replace-all PUTs — PR1 infra + Block reference (#253)
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m24s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m25s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adds the shared optimistic-concurrency contract so a stale second tab can no longer
silently overwrite a fresher edit. PR1 lands the infra + the Block reference aggregate;
PRs 2–4 fan the same recipe across the other 8 roots (design: #253#issuecomment-8472).

Contract
- `IVersionedAggregate` (`int Version`) on all 9 replace-all roots (ProgramSchedule,
  Block, Template, DecoTemplate, Playlist, Collection, Playout, MultiCollection,
  RerunCollection), EF-mapped `.IsConcurrencyToken()`; one dual-provider migration
  `AddAggregateVersions` (nullable:false, default 0).
- Strong `ETag` of `Version` on the aggregate GET; `If-Match` on the PUT; mismatch →
  412 (distinct from the §3a 409 build-lock guard). Successful PUT returns the new ETag.
- `PreconditionFailedError : BaseError` → 412 in `ApiResults.ToErrorResult`;
  `ConcurrencyHeaders.ParseIfMatch/SetETag`; malformed If-Match → 400; `*`/absent =
  Phase-1 force-write.

Block reference wiring
- Handler: standalone `Either` via `CheckVersion` AFTER validation (never through
  `Apply`, which Join()-flattens the subtype to 422), unconditional `Version++`,
  `SaveChangesWithConcurrencyGuard` backstop (DbUpdateConcurrencyException → 412).
- `BlockViewModel.Version` (header-only, not echoed in the body); controller sets the
  ETag on GET items and on the successful PUT.
- SPA: `client.requestWithMeta` seam; `blocks.getBlockItemsWithMeta` + `replaceBlock`
  If-Match/ETag round-trip; `BlockEditor` holds the ETag, sends If-Match, and on 412
  opens a blocking "changed elsewhere — reload" dialog.

Tests
- Handler contract tests: stale-If-Match → 412 (no mutation), matching/absent → success
  + bump, no-op save still bumps, and a two-context racing save → 412; proven
  non-vacuous (drop `.IsConcurrencyToken()` → the race test fails).
- Controller tests: malformed If-Match → 400, If-Match threaded to the command, ETag on
  GET/PUT, 412 passthrough. SPA: requestWithMeta ETag, replaceBlock If-Match, 412 dialog.

Docs: api-conventions §7a, spa-conventions §4a, domain-model glossary, decisions log.

Refs #253
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:51:59 +02:00
timothyandClaude Fable 5 28910ff557 fix(api): gate playout mutations on EntityLocker build lock (409) + mirror lock state in SPA (fixes #215)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 25s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Blazor disabled per-playout Reset/Erase/Delete/Edit while a BuildPlayout was
in flight (EntityLocker.IsPlayoutLocked); the REST API had no equivalent, so a
client could race an in-flight build with a destructive ExecuteDelete and leave
a half-built playout. After Blazor removal this safety invariant would vanish
entirely (adversarial-reviewer#18 removal gate).

Server:
- Add public ApiResults.ConflictProblem(title, detail) (409, mirrors NotFoundProblem).
- Inject IEntityLocker into PlayoutController; guard every id-keyed mutation
  (PUT {id}, PUT .../deco, PUT .../alternate-schedules, PUT .../templates,
  POST .../erase-items, POST .../erase-items-and-history, DELETE {id}) → 409
  when IsPlayoutLocked(id); add [ProducesResponseType(...409)] to each.
- Guard ChannelController.ResetPlayout the same way after resolving the id.
- reset-all stays 202 (ResetAllPlayoutsHandler already skips locked playouts).
- Stamp IsLocked onto PlayoutListItemResponseModel from IsPlayoutLocked.

SPA:
- Disable Reset/Erase/Erase-and-history/Delete for a locked row + show a
  "Building…" Badge; on a 409 surface the error and refresh the list.

Tests: controller-level 409 guard tests (delete/erase/PUT/deco/channel-reset)
+ IsLocked projection test; new OpenAPI contract + metadata 409 rows.
Docs: api-conventions §3a, blazor-route-parity playouts verdict, decisions.md.
Regenerated v1.json + web types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:34:23 +02:00
timothy c113142a3d fix(api): standardize error response contract
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 3m34s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
refs #46
2026-06-28 14:58:48 +02:00
timothyandClaude Opus 4.8 8f7d240fc8 feat(api): REST foundation + Channels CRUD (#34)
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 3m43s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m58s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 3m44s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m0s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m41s
First slice of the REST API (ersatztv#2). Idiomatic REST over existing MediatR handlers; design in docs/rest-api.md.

Foundation: NotFoundError : BaseError (Core) to split 404 from 422; ApiResults mapping helpers (Either/Option -> 201+Location / 200 / 204 / 404 / 422), additive (existing *ToActionResult untouched); ApiKeyAuthorizationFilter (optional X-Api-Key on mutating actions, gated by Api:WriteKey, no-op when unset) — independent of the IPTV JWT toggle so Jellyfin/Dispatcharr reads are unaffected, applied at controller-class level so every mutating action incl. ResetPlayout is covered fail-safe.

Channels: POST /api/channels (201+Location), PUT/{id} (200/404), DELETE/{id} (204/404), GET/{id} (200/404). Request DTOs -> existing commands; responses reuse ChannelViewModel. Ported page-only validations into handlers (ShowInEpg-when-disabled, external-logo-URL, Group NotEmpty) + FFmpegProfile/Watermark/Filler existence on update for create-parity. Fixed a latent 500: the .Filter(c>0) existence pattern threw TaskCanceledException on not-found; rewritten to AnyAsync.

Tests (NUnit, TZ=UTC): handler success/404/422, ApiResults mapping, API-key filter, controller status/Location/mapping, a controller-security regression net, and a create->read->delete EF integration test on a new in-memory SQLite harness. ErsatzTV.Tests 46/46, Architecture 5/5.

Refs #34

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:51:06 +02:00
Jason DoveandGitHub c62cc98c9f add elasticsearch search index provider (#1370)
* wip

* first pass at elasticsearch; movies kind of work

* use field name constants

* properly sort search results

* fix some crashes

* fix page map/jump letters

* optimize page map using terms aggregation

* index all item types

* optionally use elastic search

* code cleanup

* automatically rebuild lucene index after improper shutdown

* update changelog
2023-08-11 13:57:50 -05:00
Jason DoveandGitHub 2ce0fcb264 proxy server improvements (#996) 2022-10-13 12:17:13 -05:00
Jason DoveandGitHub c02b83d0d6 code cleanup (#743)
* update tools

* run code cleanup

* update dependencies
2022-04-19 17:47:18 -05:00
Jason DoveandGitHub 1ab98578ab refactor namespaces and imports (#670)
* re-namespace

* optimize usings

* more usings

* more of the same

* more implicit/global usings

* cleanup all usings

* minor fixes
2022-03-03 15:36:07 -06:00
Jason DoveandGitHub 90d6a59d3f normalize smart quotes in search queries (#605) 2022-01-30 11:09:32 -06:00
Jason DoveandGitHub b580125e86 fix searching when queries include non-ascii characters (#271) 2021-06-15 10:33:02 -05:00
Jason DoveandGitHub d4a2197dfa async fixes (#128)
* refactor local metadata provider

* resolve async warnings

* more async fixes
2021-04-03 11:01:20 -05:00
Jason DoveandGitHub b1ecbafb6e tv ui update (#56)
* update tv show ui

* update tv season ui

* list episode details on tv season page

* remove episode page

* remove breadcrumbs

* move home link

* code cleanup

* update screenshots
2021-03-10 12:24:18 +00:00
Jason DoveandGitHub 363eb2c276 Rebuild playouts with modified collections (#39)
* rebuild playouts when items are removed from collections

* rebuild playouts when items are added to collections

* simplify logic
2021-03-05 00:50:26 +00:00
Jason DoveandGitHub 3ed83a276f fix database migration (#35) 2021-03-02 18:32:14 +00:00
Jason DoveandGitHub 9568a0e22f Fix channel logo migration (#32)
* fix channel logo migration

* add onscreen episode-num to epg

* bump log level for nfo parse failures
2021-02-28 19:15:02 +00:00
Jason DoveandGitHub f392bab118 Database redesign (#31)
* starting database redesign

* set season and episode numbers

* use datetimes in db (utc); update movie metadata

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

* clean cache folder on startup (move channel logos, delete everything else)

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
2021-02-28 17:48:01 +00:00
Jason DoveandGitHub db054ece24 Database migrations (#13)
* remove last use of dbcontextfactory

* add initial migration
2021-02-12 12:50:04 +00:00
Jason Dove 4d52e115b5 Initial commit 2021-02-08 21:13:53 -06:00