The prior commit (a4700185b) made SongMetadata.Artists/AlbumArtists
coalesce null to [] via backing-field getters, reasoning that EF
Core's PreferField access mode never observes the getter. Adversarial
review disproved this on real TvContext/SQLite: a single read of
.Artists on a TRACKED entity mutates the backing field through the
getter, flips the entity to Modified, and the next SaveChanges writes
[] over what was a NULL column -- silent data loss waiting on the
first tracked reader (today all readers happen to be AsNoTracking).
This also reversed docs/decisions/records/api/selection-projection-include-chain.md
(#671) without the doc update CLAUDE.md requires; #691 is that
record's own "sweep by FIELD" follow-up, so it should follow the
record, not contradict it.
Revert SongMetadata.cs to plain auto-properties (byte-identical to
origin/main, BOM still stripped per the #311 gate). Guard the read
sites instead, per the #671 convention (Optional(...).Flatten(),
matching Playouts/Mapper.cs and MediaItems/Mapper.cs):
- SongVideoGenerator.cs: hoist `artists`/`albumArtists` locals once
near the top of the metadata loop instead of repeating the guard at
each of the six former call sites.
- MediaCollectionRepository.cs (GroupIntoFakeCollections): guard the
two AlbumArtists reads at lines ~1147/~1160 that #691 never named --
dropping the entity-level fix without these would trade one bug for
two.
Verified RED per guard by removing only the Optional(...).Flatten()
clause (not the whole file): the artists local throws
ArgumentNullException at SongVideoGenerator.cs:88, the albumArtists
local at :89 (List.ToList() on a null IList<string> source -- same
loaded-gun shape the review demonstrated, precise exception type is
ArgumentNullException rather than NullReferenceException since the
throw site is Enumerable.ToList's null-source check). Restored both;
existing SongVideoGeneratorTests still pass. Full ErsatzTV.Core.Tests:
685 passed (1 pre-existing skip), ErsatzTV.Tests: 1996 passed (4
pre-existing skips), 0 failures in each. No EF model drift
(`dotnet ef migrations has-pending-model-changes` reports none).
`dotnet format --verify-no-changes` on the three touched files exits
0.
Refs #691
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's Formatting job failed: 19 touched files carried a BOM, which .editorconfig
forbids (charset=utf-8). Pure encoding change — one byte per file, no semantic
diff (verified: every hunk is `-namespace` -> `+namespace`).
Self-inflicted. The patches that edited these legacy files wrote them back as
utf-8-sig to "preserve the existing style", but the #311 fix-as-you-touch gate
requires a file to be normalized when you touch it — that is the whole point of
scoping the gate to changed files instead of reformatting the ~2500 legacy BOM
files at once. dotnet format leaves the EF-generated Designer/snapshot files
alone as generated code, and its verify skips them the same way, so they stay as
ef emitted them.
Two corrections to what I believed going in:
- `dotnet format --include` does NOT no-op here. It reported `error CHARSET` for
each file and exit 2, reproducing CI exactly, and fixed them in place. The note
claiming otherwise is wrong for this invocation.
- My first BOM check reported all files clean. The od pattern was wrong; reading
the first three bytes directly found 19. A detector that can only say "ok" is
worse than no detector.
Core.Tests 565, ErsatzTV.Tests 1673, Architecture.Tests 5 — all passed. API
artifacts still in sync.
Refs #70
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds PlaybackOrder.WeightedShuffle = 9 (Classic engine only) plus the
per-source weight it distributes by.
Why a new order rather than making ShuffleInOrder weight-aware: ShuffleInOrder's
balanced shuffle pads sources to equal length with Option.None spacers, and
spacers emit nothing — so one cycle plays every item exactly once and airtime
stays proportional to collection size. It prevents *clumping*, not *domination*.
Retrofitting weights onto it would silently change shipped users' output.
WeightedShuffleCollectionEnumerator picks a source by smooth weighted
round-robin (acc += weight; richest wins; pays the total), then takes that
source's next item. Weights 3:1 emit A A B A; ties break to the earliest source
in list order. Fair-share is the equal-weights default, so one mechanism covers
both behaviors in the issue.
One rotation is sized so the source needing the most picks works through all its
items at its share; smaller sources loop within it — that looping is what makes
equal weights mean equal airtime regardless of library size. The sequence stays a
pure function of (Seed, Index), so it restores by replay like its siblings and
needs no per-source persisted counters.
It consumes ShuffleSourceBuilder.GetCollectionItemsForShuffleInOrder unchanged —
the schedule-entity-free entry point #380 reserved for this issue. Source
identity is destroyed on the Shuffle path (MultiCollectionGrouper collapses to
GroupedMediaItem + Distinct), so the weight is only reachable via
CollectionWithItems on the ShuffleInOrder-shaped path.
Weight is persisted on MultiCollectionItem AND MultiCollectionSmartItem — the
mirror is mandatory; omitting the smart item silently un-weights smart-collection
members. Both carry a DB default of 1: without it existing rows would migrate to
0, and a 0-weight source is dropped by the enumerator.
Refs #70
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix validation in new form layout
* pin mediatr to last oss version
* update dependencies
* cleanup code in core
* cleanup code in ffmpeg
* cleanup code in infra
* cleanup code in scanner
* cleanup code in application
* cleanup main code
* cleanup test code
* solution-wide code cleanup
* 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
* flag local movies as file not found
* show warning icon on cards
* unflag movie that is found during scan
* skip missing files when building playouts
* add state to search index
* add file not found health check
* link to search from file not found health check
* support flagging other media kinds as file not found
* continue to schedule missing items
* support episode files not found
* wip trash page
* fix trash url
* trash page is functional
* update changelog
* fix changelog merge