1dbcf2cd9ae26b176a4b90d8eb4ec1dfd2a73dd3
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
02e62767a1 |
feat(watchsync): sync watchlists with Trakt/Simkl/MDBList (#227)
* feat(watchsync): sync watchlists with Trakt/Simkl/MDBList Extend the watch-providers feature to sync a user's watchlist, generalizing the existing favorites pipeline rather than duplicating it. What changed - Generalize the favorites sync into one ListKind-parameterized pipeline (internal/watchsync/lists.go) driving both favorites and watchlist; the per-favorites service methods are replaced by kind-generic ones. The shadow table watch_provider_favorite_items becomes watch_provider_list_items with a list_kind discriminator. - Providers: Trakt gains watchlist sync (/sync/watchlist, distinct from favorites); Simkl gains plan-to-watch sync; MDBList is re-mapped from favorites to watchlist (its only list is a watchlist) — its capabilities now report import_favorites=false / import_watchlist=true, and the migration re-binds existing MDBList connections. - Auto-remove watched items from the watchlist: a standalone, default-on profile preference (user_profiles.remove_watched_from_watchlist) removes a movie when watched and a series once every episode is watched. Implemented as watchstate.CompletionObserver (internal/watchlist.Maintainer), wired into the manual mark-watched, playback-stop, and jellycompat mark-played paths. - Optional MDBList sort-order mirroring: an opt-in, capability-gated toggle mirrors MDBList's watchlist order into Silo via user_watchlist.sort_index; ListWatchlist orders by sort_index then added_at, so both /api/v1/watchlist and the catalog watchlist view inherit it. - Real-time + scheduled: local add/remove pushes to connected providers immediately (removals gated by the opt-in removals toggle); the hourly job is the inbound/import + retry/reconcile path. - Web: watch-provider settings gain watchlist import/export/removals and "mirror watchlist order" toggles plus watchlist sync stats. Why - The favorites and watchlist pipelines are ~90% identical; generalizing keeps one code path (per CLAUDE.md's anti-duplication guidance) instead of cloning. API/compat - All new fields on ConnectionStatus/Capabilities/ConnectionUpdate/SyncRun and the web types are additive (Silo v1 additive-only rule). No existing field is renamed, removed, or retyped. Risks / follow-up - MDBList capability flip is intentional and client-visible: silo-android / silo-apple may need to surface MDBList under the watchlist (not favorites) UI. - MDBList existing users: their MDBList list previously mirrored Silo favorites and now mirrors Silo watchlist; the first post-migration sync is a union (removals default off), so nothing is destructively purged. - Order mirroring reflects the order MDBList returns from /watchlist/items (couldn't confirm against their docs — Cloudflare-blocked); if it ever diverges from the UI sort, a sort param is the small follow-up. Tests: new maintainer (auto-remove) and watchlist-order unit tests; provider + service tests updated. go build, go test (affected pkgs), migrate-validate, verify-local-paths, web prettier/eslint/tsc all pass. AI-use disclosure: implemented with Claude Code (Claude Opus 4.8). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(watchsync): update list shadow table references * fix(watchsync): address review — retry/progress + error propagation Addresses CodeRabbit review on #227: - maintainer: propagate transient catalog lookup errors instead of silently treating every items.GetByID failure as "maybe an episode". - exportList: mark every queued item not confirmed sent (not_found, failed, or omitted) so the pending loop always advances; the next run's upsert clears the error and re-attempts, so transient failures still retry. - removePendingListItems + realtime removal: treat Sent and NotFound as reconciled; leave true failures pending (no last_error, which would strand them from the removal query) so the scheduled run retries, using in-memory dedupe to terminate the loop. - exportLocalListItems: send the normalized items (with computed ProviderItemKey), not the original event slice. - UpdateConnection: clear mirrored watchlist order before persisting the disable and propagate failures, so a failed clear can't report "disabled" while sort_index ordering is still active. - web: include favorite + watchlist removal counts in the exported "sent" total. - test: align serviceFakeRepo list-state with Postgres (clear last_error on successful transitions); add maintainer error-propagation test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cac435c4b9 |
Fix watch-state unwatch sync across user data and Jellyfin mappings (#179)
* Refine playback session handling and API responses * fix(watchstate): harden completed-history visibility |
||
|
|
074d106402 |
feat(jellycompat): BoxSet collections, genre-by-name, and airtight ABS media exclusion (#115)
* feat(jellycompat): exclude audiobook libraries, add BoxSets and genre-by-name
- Audiobook libraries (type 'audiobooks'/'audiobook') no longer appear in
Views/VirtualFolders, and all browse/search/genre/detail paths are clamped
to movie/series/episode so audiobook items cannot leak or stream through
the Jellyfin compat surface (they are served by the ABS-compat API).
- Library collections are now exposed as Jellyfin BoxSets:
IncludeItemTypes=BoxSet listing (optionally scoped via ParentId library),
/Items/{id} BoxSet detail, ParentId children with curated position order
preserved (explicit SortBy delegates to catalog ordering), poster/backdrop
presigning, and visibility + library-access filtering.
- /Items with only unexposable IncludeItemTypes (e.g. Playlist) returns an
empty result instead of falling through to views/browse.
- New GET /Genres/{name} endpoint resolving canonical genre casing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(jellycompat): centralize ABS media-type exclusion, fix BoxSet edge cases from review
- Add catalog.AccessFilter.ExcludedMediaTypes, enforced by applyAccessFilter
and threaded through Search/GetByIDsWithAccess/EnsureAccessible and
BrowseFavorites. The compat layer stamps audiobook+podcast exclusions onto
every resolved access filter (one wrap in withDefaults), closing the
favorites, recommendations, and item-image leak paths that per-call-site
guards missed.
- Treat podcast libraries like audiobook libraries: hidden from Views, items
excluded everywhere (they're served by the ABS-compat API).
- HandleItems: BoxSet listing no longer hijacks user-state-filtered queries
(IncludeItemTypes=BoxSet&Filters=IsFavorite returns empty again),
IncludeItemTypes=CollectionFolder returns library views as before, and
Ids=<boxsetId> re-hydrates the BoxSet DTO instead of falling through to
the views response.
- BoxSet artwork is now durable: stable signed tags seeded from the artwork
key (no churn on presign rotation) plus a collections fallback in the
images handler, so posters survive restarts and cache expiry.
- BoxSet listing filters/sorts/pages the lightweight collection rows before
building DTOs, so a Limit=24 page over 300 collections no longer presigns
~600 posters per request; collection children also page before hydrating
user state.
- Dedupe: shared loadVisibleCollection guard, shared collection-page writer,
single scoped-types implementation, emptyQueryResult helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(jellycompat): address PR review comments
- withCompatAccessExclusions merges compat exclusions with any the base
resolver already supplies instead of conditionally skipping them.
- Explicit type filters clamp to a closed allowlist (movie/series/episode/
season) rather than passing unknown types through to catalog queries.
- Collection artwork on the session path applies the same visibility rules
as the BoxSet item endpoints (hidden or inaccessible-library collections
404 instead of serving posters).
- loadVisibleCollection propagates infrastructure errors instead of masking
transient DB failures as 404/empty; only ErrLibraryCollectionNotFound maps
to not-found.
- ListFavorites filters ABS-surface favorites before applying the
limit/offset window (over-fetching the raw rows) so pages don't shrink or
shift, and presigns artwork only for the returned page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b246f271cb |
fix(jellycompat): hide dismissed and superseded entries from Resume (#114)
The jellycompat /UserItems/Resume endpoint served the raw in_progress
list, so Jellyfin clients showed every half-watched episode a profile
ever abandoned, including stale entries from earlier seasons of shows
the viewer had long moved past. The first-party Continue Watching row
already hid those via dismissal filtering and the superseded-episode
check, but that logic was private to internal/sections.
Extract the shared rules into internal/catalog
(ContinueWatchingProgressFilter, HomeDismissalIndex) and apply them to
the compat Resume path through a new UserDataService method,
FilterResumeProgress. The sections fetcher now delegates to the same
code, so both surfaces agree on what "still watching" means.
Resume pagination keeps advancing by raw batch counts so filtering
cannot terminate scans early, and the raw-offset fast path is limited
to StartIndex=0 for Resume because filtered lists make raw offsets
diverge from the visible list. The watched-items view ("completed"
status) stays unfiltered, and the per-series collapse deliberately
stays first-party only to preserve Jellyfin endpoint semantics.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c085b12fd1 | Initial Silo migration |