Commit Graph
84 Commits
Author SHA1 Message Date
dtourolle b11188e9dd docs(player): backend unification findings + correct false parity claims
Investigation into unifying the playback backends (Linux/MPV, Android/ExoPlayer,
Windows/webview) onto one engine with hardware acceleration. Conclusion: video
cannot be unified onto a native engine; audio can.

The blocker is not mpv-specific. WebKitGTK, WebView2 and Android WebView each
draw into their own compositor surface, so a native video surface sits either
entirely above or entirely below the webview and cannot interleave with HTML.
GStreamer and libVLC fail identically. mpv would additionally regress streaming:
it has no adaptive bitrate, while the current hls.js path does.

Six specs added:
- playback-backend-unification: the analysis and decision, with evidence
- android-audio-settings-parity: set_audio_settings on ExoPlayerBackend
- android-native-video-spike: timeboxed test of SurfaceView compositing
- windows-native-audio-backend: replace the webview <audio> shim with libmpv
- libmpv2-migration: dead libmpv git pin -> libmpv2, plus a LICENSE file
- playback-docs-corrections: the requirement-status fixes applied here

Corrections to requirements.md, all verified against source:
- UR-031/DR-034 claimed crossfade was "Done (Linux only)". It is implemented
  nowhere (mpv_backend.rs has a bare TODO) and is architecturally blocked on
  mpv, whose single-stream audio chain cannot feed acrossfade's two inputs.
- Parity matrix listed crossfade as a Linux/Android gap; it is neither.
- The matrix omitted the equalizer, which has the same Linux-only shape.
- The suggested ConcatenatingMediaSource is deprecated in current Media3.

nativeAdapter.ts cited tauri#10152 as an upstream blocker for native Android
video. That issue is a stale feature request, dead since 2024-07-01; the
capability shipped in tauri 27d01834 (2024-09-02), and the related
black-screen bug was fixed in wry 0.39.4 (we ship 0.55.x). What is genuinely
unproven is SurfaceView-behind-WebView compositing, which the spike now tracks.
2026-07-28 23:03:17 +02:00
dtourolle d1c01a6bc3 feat(player): defer single tap so a double tap doesn't also toggle pause
A tap cannot be classified when it lands — it may still turn out to be
the first half of a double tap. Play/pause is therefore deferred until
the 300ms double-tap window closes, and cancelled outright if a second
tap arrives, so a double tap seeks without also toggling pause.

Forward skip moves from 10s to 30s (back stays 10s), for both double tap
and the keyboard arrows.

The timing rules live in tapGestures.ts so they are unit-testable
without mounting the player. Rapid double taps now chain off a
still-in-flight seek target instead of all resolving against the same
not-yet-updated position.

TRACES: UR-005, UR-061 | DR-092 | UT-085, UT-086, UT-087, UT-088
2026-07-28 01:33:04 +02:00
dtourolle e5d3cc06f2 fix(android): register WebView JS bridges once; stop audio-focus fight
Locking the screen killed audio on video playback even with the
background-audio toggle armed.

configureWebViewForMedia() ran from onCreate's delayed post AND from
every onResume, re-calling addJavascriptInterface on each pass — five
times in a 45s session. WebView binds injected objects at page-load
time, so re-injecting over a live page leaves JS holding a stale proxy:
the object stays truthy (passing the `bridge()?.` optional chain) while
its methods vanish. Logcat showed 66 "WebView: Unknown object" errors
and, in JS, "TypeError: setEnabled is not a function".

So the toggle turned blue but never reached native. backgroundAudioEnabled
stayed false, onStop never dispatched 'jellytau-background', the handoff
never ran, and audio stopped the instant the screen locked. PiP and audio
focus broke identically.

- Register the bridges exactly once per WebView (identity-compared), and
  split the idempotent settings/chrome-client work into
  configureWebViewSettings() so it still runs on every resume.
- Forward WebView console output to logcat as "JellyTauWeb". The frontend
  was previously invisible to adb, which is what made this bug so hard to
  place; keep it for the next boundary-spanning diagnosis.
- setBackgroundAudioEnabled now reports whether native was actually
  reached instead of silently no-oping, so a dead bridge can never again
  masquerade as an armed toggle.

Removing the re-injection revived a latent conflict it had been masking:
the focus calls started working, and three AUDIOFOCUS_GAIN requesters
inside one uid began fighting — MainActivity, ExoPlayer, and Chromium's
own AudioFocusDelegate. The grant was followed ~45ms later by
AUDIOFOCUS_LOSS, whose handler paused playback, so arming background
audio (or just pressing play) paused the video in a loop.

WebView already manages focus for <video>. Drop the redundant
AndroidAudioFocus bridge, its listeners and its helpers entirely, and
leave focus to whichever engine is actually rendering — consistent with
the player-is-authoritative principle.

Also drops the dead AndroidBackgroundAudio.isSupported() probe, unused
since the button gate moved to platform().

TRACES: UR-040 | IR-025, DR-051 | UT-062
2026-07-28 01:32:57 +02:00
dtourolle 124da29fc7 fix(search): route the library header search to /search
Typing in the desktop header search bar ran library.search() in place and
relied on /library rendering the results inline. On every other /library/**
route nothing rendered them, so the search bar looked broken: results were
fetched and never shown.

Make /search the single surface that renders results. The header bar becomes a
navigator — it hands the query and route-derived scope to /search via ?q= and
?scope=, which seed the page and run the search on arrival. The inline result
block and the header's scope chips are removed; the chips live on /search,
which owns the results. The empty `all` scope is omitted from the URL, and
typing while already on /search does not push a history entry per keystroke.
2026-07-25 15:13:45 +02:00
dtourolle 5927299c0f feat(search): rank results by match quality and split TV/People groups
Neither search backend orders by *where* the query matched, so a mid-word hit
could outrank a prefix one — typing "parks" surfaced "Sparks of Love" above
"Parks and Recreation".

Add `domain/search_rank.rs`, which sorts by match position (prefix →
word-start → mid-word substring → no name match), then by media kind so a
container outranks its own contents. The sort is stable, so each backend's own
relevance still breaks ties it was never overruled on. `repository_search`
applies it to both the instant cache result and the merged cache+server union,
so the list does not reshuffle when server results land. Ranking lives in Rust
because "a better match" is domain vocabulary, not presentation.

On the frontend, the combined `tvShows` result group splits into separate
Shows and Episodes groups so a show no longer competes with its own episodes
for a slot, and a People group is added so searching an actor's name reaches
their bio. A stored `tvShows` order expands in place, keeping the position an
upgrading user chose for it.
2026-07-25 15:13:32 +02:00
dtourolle 7650efcb7f fix(player): scale video to fill the player viewport
The <video> element used `max-w-full max-h-full`, which only ever shrinks
oversized media. A source smaller than the window (480p on a 1080p display)
rendered at its intrinsic size — a small picture floating in a black frame.

Fill the container and let `object-contain` do the scaling, so the picture
fits whichever axis constrains it in both directions while preserving aspect
ratio. The sizing rules move to `videoFit.ts` so they are unit-testable
outside the component.
2026-07-25 15:12:53 +02:00
dtourolle fb967433f0 fix(library): populate "More Episodes" for series without season folders
The Episode Focus View's episode strip collapsed to just the current
episode on some series. Two causes:

- Series that expose episodes directly as children rather than under
  season folders yielded an empty season fetch, leaving allEpisodes
  empty. The library page now groups those flat episode children by
  their season number and synthesizes minimal season headers.
- isCurrentEpisode over-matched: episodes with no season/episode number
  compared equal (undefined === undefined) and every one of them looked
  like the focused episode.

Extracts the strip's pure logic into episodeStrip.ts so both behaviours
are unit-tested, per the failing-test-first rule.

TRACES: UR-058 | DR-087
2026-07-25 09:21:15 +02:00
dtourolle ee584aced2 fix(autoplay): advance to the next episode in background audio mode
An episode handed off to the audio-only path for background playback is a
MediaType::Audio item, so autoplay's video-only checks stopped
recognising it as an episode: playback simply ended at the episode
boundary instead of continuing to the next one.

- Carry episode identity (item_type, series_id) through the
  background-audio handoff so the backend queue item still knows it's an
  episode; is_episode_item now trusts item_type over the media_type
  heuristic, and the sleep timer's episode counter follows.
- The frontend normally performs the advance by navigating to
  /player/<id>, which is unavailable while the WebView is suspended.
  advance_to_next_episode_audio_only drives it entirely in the backend:
  fetch the next episode, build its audio-only stream URL, and load it
  into the native audio player, preserving episode identity so the
  following boundary advances too.
- Android's autoplay dispatch routes background-audio episodes to that
  backend advance and keeps the countdown path for the foreground.
- get_audio_only_stream_url_for_video joins the MediaRepository trait
  (online delegates to the existing builder, offline errors) so the
  controller can reach it without a frontend round-trip.

TRACES: UR-040, UR-023 | DR-052 | JA-032
2026-07-25 09:21:08 +02:00
dtourolle eb76c96e94 feat(player): skipping an episode marks it watched, not paused
Skipping to the next episode left a mid-episode resume point behind, so
the skipped episode reappeared in Continue Watching with a partial
progress bar. Skipping means "done with this one", not "stopped here".

- reportSkippedEpisode marks the outgoing episode played instead of
  reporting a stop position, and arms a one-shot suppression consumed by
  the player's stop handler, so VideoPlayer's post-navigation unmount
  stop report can't overwrite the 100% progress with the partial one.
- Continue Watching drops resume entries superseded by Next Up: an
  in-progress episode whose series has a next-up entry strictly later in
  series order (season, then episode) is hidden from the Home and TV
  rows. Movies, series without a next-up entry, and items with unknown
  or mixed ordering are always kept.

Adds UR-059, DR-088, DR-089.

TRACES: UR-059 | DR-088, DR-089
2026-07-25 09:20:56 +02:00
dtourolle d4e2cd120c feat(player): webview audio backend for platforms without a native one
Adds WebviewAudioBackend, used on non-Linux/non-Android targets (e.g.
Windows) where there is no libmpv/ExoPlayer. Instead of decoding, it
emits a WebviewAudioLoad event with the stream URL; a frontend <audio>
element (WebviewAudioAdapter + webviewAudio service) plays it and reports
state/position back through the existing player_report_* round-trip, so
the Rust PlayerController stays the single source of truth. Play/pause/
seek reach the element via the existing ControlCommand event.

All video already renders in the webview on every platform, so this
completes audio-only playback for Windows (video via WebView2, audio via
<audio>). Pure Rust + Tauri events, so it still cross-compiles from Linux.

Regenerates bindings.ts (adds webview_audio_load; also carries the
equalizer EQ bindings).

TRACES: UR-003, UR-004, UR-005 | DR-004
2026-07-24 23:49:23 +02:00
dtourolle c543f90ad3 feat(audio): graphic equalizer with presets and custom bands
Adds a 10-band graphic equalizer to AudioSettings (enabled flag +
per-band dB gains, normalised to 10 entries and clamped to range).
Presets return gain curves; the settings page gains EQ UI. libmpv
applies the filter on Linux (Android parity pending). Old persisted
settings without EQ fields load as disabled + flat.

Also includes the requirements/traceability/ux-flows doc updates for
this feature and the home long-press routing (UR-058/DR-087).

TRACES: UR-027 | IR-020, DR-030 | UT-079, UT-080, UT-081, UT-082
2026-07-24 23:49:13 +02:00
dtourolle 589f08b873 feat(home): tap opens detail, long-press plays from home cards
Home carousel cards route a tap to the item's detail / Episode Focus
View and a ~500ms long-press to a confirm-then-play flow. MediaCard gains
an onLongPress prop with pointer-based detection (cancelled on >10px move
so carousel scroll is unaffected, trailing click suppressed). Episode
taps route to /library/<seriesId>?episode=<id>; the bare-episode detail
page links back to its parent series/season.

TRACES: UR-058 | DR-087
2026-07-24 23:49:03 +02:00
dtourolle e2c9d68311 docs(downloads): mark UR-055/056 Done; fix colliding UT ids
The browsable Downloaded library + Transfers split + on-disk usage
(f25deba, plus today's grouping/perf fixes) fully implement UR-055 and
UR-056, but the requirements doc still listed them and DR-081..085 as
Planned. Flip to Done.

Also fix UT-id collisions: the downloaded-browse and formatBytes tests
reused UT-046..050 (already assigned to smart-cache/playlist tests in the
matrix). Reassign to UT-071..078 and register them in §4, including the
new music/TV container-rollup and orphan-leaf regression tests.
2026-07-24 22:22:43 +02:00
dtourolle 90f03dd142 fix(player): show background-audio button on all Android video playback
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 11m49s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 10m18s
Traceability Validation / Check Requirement Traces (push) Successful in 1m2s
Build & Release / Run Tests (push) Successful in 10m46s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 7m16s
Build & Release / Build Linux (push) Successful in 24m59s
Build & Release / Build Android (push) Successful in 33m5s
Build & Release / Create Release (push) Successful in 17s
The audio-only (background-audio) button was gated on the
AndroidBackgroundAudio JS-bridge probe, resolved once as a const at mount.
The bridge is injected into the WebView asynchronously and races component
mount, so on some loads the probe returned false and never recovered,
hiding the button on 'some videos' at random.

Gate on platform() === 'android' instead (synchronous, stable), matching
the convention in VolumeControl. toggleBackgroundAudio() already no-ops if
the bridge is momentarily absent.

Bump version to 0.0.18.
2026-07-24 20:32:08 +02:00
dtourolle 514e42fccb Merge branch 'frontend-domain-model' into ci-docs-publish-fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 8m34s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m20s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
Build & Release / Run Tests (push) Successful in 5m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m38s
Build & Release / Build Linux (push) Successful in 17m22s
Build & Release / Build Android (push) Successful in 22m44s
Build & Release / Create Release (push) Successful in 14s
2026-07-23 22:18:59 +02:00
dtourolle 9b1c9b3c91 feat(settings): rework settings page; remove unused SkeletonLoader/StorageManagement
Settings page refactor plus supporting docs (requirements, ux-flows,
traceability) and the frontend-domain-model spec with implementation-status
banner. Removes SkeletonLoader and StorageManagement components (no remaining
references).
2026-07-23 22:18:37 +02:00
dtourolle 1780109fb1 domain: flip last missed catalog .type reader (movies page) to .kind
library/movies/+page.svelte handleItemClick still read item.type === "Folder"
(missed in the phase-2a sweep). Now item.kind === "folder". Final sweep
confirms zero catalog .type/runTimeTicks/primaryImageTag/playbackPositionTicks
reads remain anywhere in src/.

Decision on dropping the dual-carry legacy Rust fields: KEPT. They are now
internal-only (no frontend reader), but internal Rust still depends on
item_type (SQL WHERE item_type='Audio', player audio filter, player-queue
passthrough) and the DB stores ticks. Removing them needs a DB/query-layer
migration with real regression risk and zero frontend benefit — out of scope
for the frontend-model goal, which is met.

Frontend 626, check clean.
2026-07-23 22:15:15 +02:00
dtourolle 3a18ad060b domain: delete dead jellyfinFieldMapping; scope playbackUnits to session boundary (phase 4e)
jellyfinFieldMapping.ts (SORT_FIELD_MAP friendly->Jellyfin sort names) had
zero consumers — sort code passes raw Jellyfin field names directly — so it
and its test are deleted.

playbackUnits.ts can't be removed: its tick<->seconds helpers are still the
correct converters for the remote Jellyfin *session* boundary
(SessionInfo.playState.positionTicks, NowPlayingItem.runTimeTicks), which
legitimately arrives in ticks. Documented that narrowed role; formatTime/
calculateProgress remain neutral seconds-based presentation helpers.

Note (out of scope): sortBy still passes raw Jellyfin field names
("SortName", "CommunityRating") — a separate sort-taxonomy leak that would
need its own Rust SortKey, like the search-scope work.

Frontend 626 tests (jellyfinFieldMapping's 18 removed with it), check clean.
2026-07-23 22:13:54 +02:00
dtourolle 1968c06172 domain: neutral StreamKind for media streams (phase 4d)
Add StreamKind enum (audio/video/subtitle/other) to the domain module with
a total stream_kind_from_jellyfin mapper. MediaStream gains a kind field
(dual-carry), populated at the mapping seam. Frontend VideoPlayer track/
subtitle selection and the channel-video check now use stream.kind instead
of the Jellyfin stream.type string.

Rust 456 (+ stream_kinds_map test), frontend 644, check clean.
2026-07-23 22:11:31 +02:00
dtourolle ec8a7610f5 domain: player/reporting ticks -> milliseconds (phase 4c)
Playback position now crosses the IPC boundary in milliseconds. Ticks
survive only inside Rust (DB storage, Jellyfin API) and at the genuine
remote-session boundary (session seek / transfer / RemoteControls).

Rust command signatures (ms in, converted to ticks internally):
- storage_update_playback_progress / _context: position_ms
- repository_report_playback_start / _progress / _stopped: position_ms
- PlaybackProgress.position_ticks -> position_ms (converted in the query)

Frontend:
- playbackReporting, playerEvents, VideoPlayer, Queue, player/[id] resume:
  seconds*1000 / durationMs/1000 instead of tick math.
- repository-client + syncService param names -> positionMs.
- Tests updated to ms fixtures/assertions.

Out of scope (legitimately ticks): NowPlayingItem, PlayState.positionTicks,
sessionSeek, playbackModeTransferToLocal, RemoteControls, SessionCard — the
remote Jellyfin session API.

Rust 456, frontend 644, check clean.
2026-07-23 22:02:29 +02:00
dtourolle 93d198ce21 domain: primaryImageTag -> imageId end-to-end (phase 4a/4b)
Rust: PlayerMediaItem and MergedMediaItem gain image_id (dual-carry),
populated from primary_image_tag at every construction/conversion site.
Regenerated bindings.

Frontend: all catalog + player + merged readers now use imageId. The
NowPlayingItem->MediaItem bridge (player.ts) properly maps the remote
session's Jellyfin fields (Type, runTimeTicks, primaryImageTag) onto the
neutral kind/durationMs/imageId. Types that are genuinely out of scope
(Person, NowPlayingItem, PlayItemRequest) keep primaryImageTag.

Rust 456, frontend 644, check clean.
2026-07-23 21:40:58 +02:00
dtourolle 2b42b74912 domain: replace user-facing Jellyfin type badge with kind label (phase 3b)
The library detail page showed the raw Jellyfin item_type string
("MusicAlbum") to users. Add utils/mediaKind.ts with kindLabel(), a
presentation-only MediaKind -> human label map, and use it for the badge.
Flip the two remaining .type debug logs to .kind.

This removes the last user-visible Jellyfin vocabulary on the catalog
surface. primaryImageTag -> imageId rename (naming-only, ~40 sites across
catalog + player/merged types needing a Rust round-trip) intentionally
deferred as the lowest-value slice.

Frontend 644 tests, check clean.
2026-07-23 21:31:51 +02:00
dtourolle 7660a33dfc domain: catalog frontend off Jellyfin ticks -> milliseconds (phase 3a)
The catalog surface now speaks milliseconds, the app's neutral time unit.
Ticks no longer reach library/home components.

Rust:
- UserData gains playback_position_ms (dual-carry), populated from ticks
  at the offline mapping seam via domain::ticks_to_ms.

Frontend:
- formatDuration(duration.ts) and the two local copies now take ms, not
  ticks; all callers pass item.durationMs.
- Progress bars (EpisodeRow, EpisodeFocusView, MediaCard, LibraryListView)
  compute playbackPositionMs / durationMs — unit-consistent, no tick math.
- PlaylistDetailView totalDuration sums durationMs.
- duration.test.ts + TrackList.test.ts fixtures updated to ms.

Deferred: player/session/reporting tick math (Queue, SessionCard,
RemoteControls, playbackReporting, playerEvents) — those cross the
storage/Jellyfin command boundary in ticks and need command-signature
changes (phase 3b). Display {item.type} badge -> kind label (phase 4).

Rust 456, frontend 644, check + check:boundary clean.
2026-07-23 21:30:04 +02:00
dtourolle 3e962a202c test: update playerVisibility fixtures to .kind (phase 2a)
isVideoItem now reads item.kind, so the mini-player visibility fixtures
must set kind (track/movie/liveChannel) instead of the old Jellyfin
type strings. Renames channelItem -> liveChannelItem to match its kind.

All 644 frontend tests pass.
2026-07-23 21:16:42 +02:00
dtourolle 43ddc5a889 domain: flip isVideoItem in player store to .kind (phase 2a completion)
The catalog MediaItem check in stores/player.ts isVideoItem() was missed
in the phase 2a sweep (excluded by a player-path filter). It reads the
catalog MediaItem, so it flips like the rest: type Movie/Episode/TvChannel
-> kind movie/episode/liveChannel.

Verified: no catalog .type === "<JellyfinType>" comparisons remain in the
frontend (only stream.type in VideoPlayer, deferred to phase 4). check clean.
2026-07-23 21:14:45 +02:00
dtourolle 772e9ca6d5 domain: flip catalog frontend off Jellyfin item-type strings (phase 2a)
Migrate catalog MediaItem consumers from stringly item.type ("Audio",
"MusicAlbum", …) to the neutral item.kind enum across all classification
logic: home, library detail, player routing, artist/person/related/genre
components, tv store.

Model refinements found during migration (each a real distinction the
flat item_type collapsed):
- MediaKind::LiveChannel — live TV (playable, non-seekable) vs
- MediaKind::ChannelItem — channel VOD leaf (playable, seekable) vs
- MediaKind::Channel — channel container (drill-in).
  TvChannel->LiveChannel, non-folder ChannelFolderItem->ChannelItem.

RelatedItemsSection and GenreTags props migrated from Jellyfin type
strings to MediaKind; MediaKind re-exported from api/types.

Deferred by design: display {item.type} text, ResultsCounter labels,
Person.type (role), stream.type (phase 4), and all runTimeTicks/tick math
(coupled to playbackPositionTicks — phase 3). Old fields still dual-carried
so nothing breaks.

Rust 456 + 7 domain tests, frontend 644 tests, check clean.
2026-07-23 21:12:20 +02:00
dtourolle 55fa26377a domain: introduce provider-neutral media model (phase 1)
Establish src-tauri/src/domain/ as the single source of truth for the
media model, with all Jellyfin translation isolated in from_jellyfin.rs.
Adds MediaKind enum and neutral duration_ms/image_id fields to MediaItem
as additive, defaulted dual-carry alongside the legacy Jellyfin-named
fields, so nothing breaks while the frontend migrates off them.

- domain/media.rs: canonical MediaKind (closed enum, replaces stringly
  item_type), Default = Other so unknown/defaulted items are inert.
- domain/from_jellyfin.rs: total, panic-free item_type -> MediaKind
  classification (all audited types + person subroles) and ticks->ms.
- MediaItem gains kind/duration_ms/image_id, populated at both mapping
  seams (online to_media_item, offline cached_item_to_media_item) and
  the synthesized-album/person sites.
- Regenerated bindings.ts: frontend now HAS the neutral model available.

Phase 1 of docs/specs/frontend-domain-model.md. No frontend behaviour
change yet; wire shape is a superset of before.

Rust 456 tests, frontend 644 tests, check + check:boundary all green.
2026-07-23 20:53:47 +02:00
dtourolle bacb9ca0bb docs(traces): tag episode focus view and library detail with TRACES
Add TRACES comments linking the episode focus view and library detail
page to their existing requirements.

TRACES: UR-035, UR-038, UR-048 | DR-043, DR-061, DR-062
2026-07-23 20:03:16 +02:00
dtourolle cf9472f04f feat(chrome): shared account menu and global app header
Move account actions (Settings, Downloads, Display preferences, Sign
out) out of the library-only header into a shared AccountMenu anchored in
a global AppHeader, available on every authenticated non-immersive
screen. Add a layoutShell helper deciding where chrome shows, expose
serverName/serverUrl auth stores, and a display view-mode preference. The
settings page also gains the UR-053 WiFi-only toggle.

TRACES: UR-054 | DR-075, DR-076, DR-077
2026-07-23 20:03:12 +02:00
dtourolle f25deba824 feat(downloads): browsable downloaded library with on-disk usage
Replace the flat download list with a Downloaded browse surface that
reuses the online grids/cards/detail pages, filtered to on-device media,
plus a demoted Transfers tab. Add repository browse commands
(getDownloadedLibraries/Items, disk usage) with offline/hybrid
implementations, a downloadedCatalog service, formatBytes helper, and
per-item/device disk-usage labels on cards and grids. Regenerated
bindings.

Also carries the inseparable UR-052 offline-filter hunks in
offline.rs/hybrid.rs.

TRACES: UR-055 | DR-081, DR-082, DR-083, DR-084; UR-056 | DR-085
2026-07-23 20:02:55 +02:00
dtourolle 8f4f651bac fix(offline): gate library listing to downloaded-only when offline (#10)
The connectivity store now drives the "downloaded only" view so an
offline library page shows just on-device media, with the server catalog
revealed only when "Show all server media" is toggled.

TRACES: UR-052 | DR-078, DR-079
2026-07-23 20:02:33 +02:00
dtourolle c175378f38 feat(search): context-scoped search with filter chips and group order
Add a search scope (all/music/shows/movies) resolved from the entry
route and adjustable via filter chips, threaded through the library
store's search() into includeItemTypes. Results group by type in a
user-configurable order, editable from settings.

TRACES: UR-049 | DR-063, DR-064, DR-065; UR-050 | DR-066, DR-067
2026-07-23 20:02:15 +02:00
dtourolle e083b53ee8 feat(downloads): WiFi-only network-type-aware download gating
Add a metered/cellular network detector so downloads honour a "WiFi
only" preference. Android reports network type via NetworkTypeMonitor;
Rust exposes it through download/network.rs and holds the queue pump when
on a metered connection, emitting a queue-wide waitingForNetwork event.
The frontend surfaces this via the networkType service and a
waitingForNetwork store flag.

TRACES: UR-053 | DR-074
2026-07-23 20:02:07 +02:00
dtourolle acf1bb200d fix resuming video playback after background audio only mode.
Traceability Validation / Check Requirement Traces (pull_request) Failing after 3h14m1s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Has been cancelled
🏗️ Build and Test JellyTau / Run Tests (pull_request) Has been cancelled
2026-07-22 22:28:07 +02:00
dtourolle 3fbf6afdbc Background-audio handoff for video + repository/player refactor
Hand video playback off to a native audio-only stream when the app is
backgrounded or locked, with no on-device video decode (UR-040). Adds
player_enter/exit_background_audio commands, an audio-only stream URL
for video items across the repository layer, and the frontend handoff
state machine wired into VideoPlayer. Includes accompanying
repository/offline/player refactoring and regenerates the traceability
matrix.
2026-07-22 21:52:07 +02:00
dtourolleandClaude Opus 4.8 1fa5aa46f9 Android picture-in-picture, and fix three dead Android config files
Add PiP for native (ExoPlayer) video on Android. Video renders into a
SurfaceView behind the WebView, so PiP is driven by the Activity shrinking
into a floating window rather than the HTML5 PiP API (which WebKitGTK does
not implement, hence Android-only).

- PictureInPictureManager.kt: enter PiP with the video's aspect ratio
  (clamped to the 1:2.39-2.39:1 range Android accepts, outside which it
  throws), plus a play/pause RemoteAction. Hides the WebView while in PiP -
  it is opaque and sits above the surface, so it would otherwise occlude the
  video entirely - and re-fits the surface on exit.
- MainActivity.kt: onUserLeaveHint auto-PiP, onPictureInPictureModeChanged,
  and an AndroidPictureInPicture JS interface following the existing
  AndroidAudioFocus pattern.
- pictureInPicture.ts + VideoPlayer.svelte: PiP button, rendered only when
  the native bridge reports support.
- proguard: keep rules for @JavascriptInterface methods, which are only
  referenced from JS and would be stripped in minified release builds.

Casting needs no special handling: canEnterPip() checks natively that a
local video surface is attached and playing, which a remote session lacks.

While wiring the manifest, found that three tracked files under
src-tauri/android/ were never reaching any build. Gradle reads only
gen/android/app/src/main/, and sync-android-sources.sh did not copy them:

- src/main/AndroidManifest.xml was a partial <application> fragment written
  as if Tauri merged it. It does not - there is no manifest-merger hook
  here, so its hardwareAccelerated flag never reached an APK. Promoted to
  the complete authoritative manifest (folding in that flag) and synced.
- src/main/res/values/themes.xml (transparent status bar, fitsSystemWindows)
  was never copied; the sync only globbed mipmap-*. Now synced.
- build.gradle.kts was a leftover com.android.library module config with
  stale media3 1.5.1 deps. The live deps are in app/build.gradle.kts at
  1.5.0. Deleted.

Verified: merged manifest now carries hardwareAccelerated,
supportsPictureInPicture, resizeableActivity and the density configChange;
themes.xml compiles into merged resources; Kotlin builds warning-free;
svelte-check clean; 537 frontend tests pass.

Not verified: PiP behaviour on a device, and the release keep rules against
a minified build. assembleUniversalDebug cannot complete in this
environment - the Rust step wants a dev-server addr file that only exists
under `tauri android dev`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:59:39 +02:00
dtourolleandClaude Opus 4.8 2e479d05b3 Navigation up/back split, faster startup, and CI versionCode fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m57s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m13s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m30s
Build & Release / Build Linux (push) Successful in 17m52s
Build & Release / Build Android (push) Failing after 58s
Build & Release / Create Release (push) Has been skipped
Navigation:
- Split conflated "back" into navigateUp (deterministic route parent) and a
  history-safe navigateBack that tracks in-app depth via afterNavigate instead
  of history.length. Fixes the resume-from-background trap where a stale WebView
  stack left the header arrow stuck on the current page.
- /library self-corrects for music/tv/movies (which have dedicated landing
  pages): a leftover currentLibrary no longer forces the inline content-list
  view, so "up"/back shows the libraries overview. Live TV / channels / other
  types still render inline.

Startup (unblock first paint):
- auth.initialize() no longer awaits security-status, player-config, or session
  verification before flipping isInitialized. These run fire-and-forget after the
  session is restored, so the library overview paints without waiting on several
  serial IPC round-trips.

Versioning / CI:
- tauri.conf.json + package.json aligned to 0.0.15 (the tag series had drifted to
  0.1.0, whose formula-derived versionCode 1000 outran the v0.0.x tags).
- Release workflow now pins a monotonic Android versionCode
  (1000 + major*10000 + minor*100 + patch) so tagged builds never downgrade
  below prior installs and always increase in semver order.

Tests: navigation (4), auth (29), playbackMode (23) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:12:36 +02:00
dtourolle 1992a8187d layout and remote fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m31s
Traceability Validation / Check Requirement Traces (push) Successful in 20s
Build & Release / Run Tests (push) Successful in 5m24s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m29s
Build & Release / Build Linux (push) Successful in 17m27s
Build & Release / Build Android (push) Successful in 22m14s
Build & Release / Create Release (push) Successful in 12s
2026-07-16 22:53:03 +02:00
dtourolle 532ffa661a Fix tests
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m29s
Traceability Validation / Check Requirement Traces (push) Successful in 19s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 5m4s
Build & Release / Run Tests (push) Successful in 4m52s
Build & Release / Build Linux (push) Successful in 17m55s
Build & Release / Build Android (push) Successful in 22m13s
Build & Release / Create Release (push) Successful in 13s
2026-07-11 22:09:33 +02:00
dtourolle 2a1f1689b4 Layout and search fix
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 2m4s
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Failing after 2m45s
Build & Release / Build Linux (push) Has been skipped
Build & Release / Build Android (push) Has been skipped
Build & Release / Create Release (push) Has been skipped
2026-07-11 19:55:55 +02:00
dtourolle acb7e5f221 fix offline mode and layout bugs
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m32s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m21s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m25s
Build & Release / Build Linux (push) Successful in 17m31s
Build & Release / Build Android (push) Successful in 22m5s
Build & Release / Create Release (push) Successful in 16s
2026-07-06 20:24:46 +02:00
dtourolle 2d141e5bf4 Fix for offline mode
🏗️ Build and Test JellyTau / Run Tests (pull_request) Successful in 4m21s
Traceability Validation / Check Requirement Traces (pull_request) Successful in 20s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Successful in 4m54s
2026-07-03 19:37:34 +02:00
dtourolleandClaude Opus 4.8 a64e1b1fb4 Introduce PlayerAdapter contract; decision logic shared in Rust backend
Establish a decoupled player boundary so UI and backend interact with video
through one contract, with the HTML5 (Linux/interim-Android) and native
(ExoPlayer) providers as interchangeable primitive-executor adapters.

- PlayerAdapter interface + AdapterHost callback bag (adapters/types.ts): the
  adapter owns only decision-free element PRIMITIVES (seekElement, reloadSource,
  play/pause, setVolume, selectSubtitle); it never branches on strategy.
- Seek/audio-track DECISIONS stay in Rust (player_seek_video / _switch_audio_track
  return a strategy); the facade dispatches the chosen primitive to the active
  adapter. Both providers share the one decision path — logic lives once, in Rust.
- Facade holds the active adapter; a new ControlCommand PlayerStatusEvent lets
  backend control (lockscreen/remote/sleep) drive the webview <video> element.
- Html5PlayerAdapter resolves the LIVE element via the bridge (fixes play/pause
  silently no-opping when the element was re-bound).
- Do not emit a "stopped" player state on natural end-of-video: it flipped the
  player/mode to idle mid-handoff and suppressed next-episode auto-advance under
  a sleep timer. Jellyfin progress reporting is preserved; the backend's
  on_video_playback_ended owns the transition.
- VideoPlayer net -300 lines (strategy/HLS-reload logic relocated to the adapter).
- Adds 20 adapter unit tests; existing suites stay green (vitest 457, cargo 416).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:56:20 +02:00
dtourolle 1f6977cd01 Playback fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m28s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m37s
Build & Release / Run Tests (push) Successful in 4m12s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 18m57s
Build & Release / Create Release (push) Successful in 13s
2026-07-02 18:13:55 +02:00
dtourolle 6af7f7dcca Fix android playback issue
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m13s
Traceability Validation / Check Requirement Traces (push) Successful in 20s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m46s
2026-07-02 00:19:07 +02:00
dtourolle 75014ee00f Fix sleep bug, fix menu return
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m1s
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Successful in 4m7s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m5s
Build & Release / Build Linux (push) Successful in 16m20s
Build & Release / Build Android (push) Successful in 19m12s
Build & Release / Create Release (push) Successful in 8s
2026-07-01 23:49:51 +02:00
dtourolleandClaude Opus 4.8 342f95cac1 Wire up playback reporting, fix duration flash, hide video from audio mini player
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m14s
Traceability Validation / Check Requirement Traces (push) Successful in 21s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 19m3s
Playback reporting (position sync / resume-on-another-device):
- player_configure_jellyfin now builds a PlaybackReporter sharing the player
  controller's Arc, so Start/Progress/Stopped actually reach Jellyfin on every
  auth path (login/restore/reauth); previously they never did.
- The PlaybackReporterWrapper now shares the same Arc the controller and MPV
  progress loop report through, instead of a dead parallel Option.
- Android position callbacks now emit throttled progress reports (30s/item),
  mirroring the MPV backend.

Duration flash on pause:
- resolveDuration() prefers the live store duration for the already-loaded
  track over the runTimeTicks estimate, so pausing no longer clobbers the
  slider's max to 0 when runTimeTicks is missing.

Video leaking into audio mini player:
- isVideoItem() also checks the backend PlayerMediaItem mediaType
  discriminator, so a video started via player_play_item (no Jellyfin `type`,
  mediaType "video") no longer surfaces in the audio mini player.

Middle-truncation of long media names:
- New truncateMiddle util applied to track/episode/card/mini-player titles so
  distinguishing tails (episode numbers, suffixes) stay visible.

Adds regression tests for the duration and mini-player fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:52:27 +02:00
dtourolle dcee342c47 Jray mugshots of actors shown
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 14m55s
Traceability Validation / Check Requirement Traces (push) Successful in 51s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 26m44s
2026-06-28 21:09:06 +02:00
dtourolle 78f5cd9db9 Fix playback regression
🏗️ Build and Test JellyTau / Run Tests (push) Has been cancelled
🏗️ Build and Test JellyTau / Build Android APK (push) Has been cancelled
Traceability Validation / Check Requirement Traces (push) Has been cancelled
2026-06-28 21:07:00 +02:00
dtourolle 0eae81ec59 Add JRay support
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 17m59s
Traceability Validation / Check Requirement Traces (push) Successful in 1m48s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been cancelled
2026-06-28 20:38:58 +02:00